Skip to content

Commit eff4da0

Browse files
committed
add KernelQueue test and some more for types
1 parent f2cca64 commit eff4da0

File tree

3 files changed

+377
-5
lines changed

3 files changed

+377
-5
lines changed
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import type { Message, VatOneResolution } from '@agoric/swingset-liveslots';
2+
import type { CapData } from '@endo/marshal';
3+
import { makePromiseKit } from '@endo/promise-kit';
4+
import { describe, it, expect, vi, beforeEach } from 'vitest';
5+
import type { MockInstance } from 'vitest';
6+
7+
import { KernelQueue } from './KernelQueue.ts';
8+
import type { KernelStore } from './store/index.ts';
9+
import type {
10+
KRef,
11+
RunQueueItem,
12+
RunQueueItemNotify,
13+
RunQueueItemSend,
14+
} from './types.ts';
15+
16+
vi.mock('./services/garbage-collection.ts', () => ({
17+
processGCActionSet: vi.fn().mockReturnValue(null),
18+
}));
19+
20+
vi.mock('@endo/promise-kit', () => ({
21+
makePromiseKit: vi.fn(),
22+
}));
23+
24+
describe('KernelQueue', () => {
25+
let kernelStore: KernelStore;
26+
let kernelQueue: KernelQueue;
27+
let mockPromiseKit: ReturnType<typeof makePromiseKit>;
28+
29+
beforeEach(() => {
30+
mockPromiseKit = {
31+
promise: Promise.resolve(),
32+
resolve: vi.fn(),
33+
reject: vi.fn(),
34+
};
35+
(makePromiseKit as unknown as MockInstance).mockReturnValue(mockPromiseKit);
36+
kernelStore = {
37+
nextTerminatedVatCleanup: vi.fn(),
38+
collectGarbage: vi.fn(),
39+
runQueueLength: vi.fn(),
40+
dequeueRun: vi.fn(),
41+
enqueueRun: vi.fn(),
42+
initKernelPromise: vi.fn().mockReturnValue(['kp1']),
43+
incrementRefCount: vi.fn(),
44+
getKernelPromise: vi.fn(),
45+
resolveKernelPromise: vi.fn(),
46+
nextReapAction: vi.fn().mockReturnValue(null),
47+
getGCActions: vi.fn().mockReturnValue([]),
48+
} as unknown as KernelStore;
49+
50+
kernelQueue = new KernelQueue(kernelStore);
51+
});
52+
53+
describe('run', () => {
54+
it('processes items from the run queue and performs cleanup', async () => {
55+
const mockItem: RunQueueItem = {
56+
type: 'send',
57+
target: 'ko123',
58+
message: {} as Message,
59+
};
60+
(
61+
kernelStore.runQueueLength as unknown as MockInstance
62+
).mockReturnValueOnce(1);
63+
(kernelStore.dequeueRun as unknown as MockInstance).mockReturnValue(
64+
mockItem,
65+
);
66+
const deliverError = new Error('stop');
67+
const deliver = vi.fn().mockRejectedValue(deliverError);
68+
await expect(kernelQueue.run(deliver)).rejects.toBe(deliverError);
69+
expect(kernelStore.nextTerminatedVatCleanup).toHaveBeenCalled();
70+
expect(deliver).toHaveBeenCalledWith(mockItem);
71+
});
72+
});
73+
74+
describe('enqueueMessage', () => {
75+
it('creates a message, enqueues it, and returns a promise for the result', async () => {
76+
const target = 'ko123';
77+
const method = 'test';
78+
const args = ['arg1', { key: 'value' }];
79+
const resultValue = { body: 'result', slots: [] };
80+
let resolvePromise = (_value: CapData<KRef>): void => {
81+
// do nothing
82+
};
83+
const resultPromiseRaw = new Promise<CapData<KRef>>((resolve) => {
84+
resolvePromise = resolve;
85+
});
86+
const successPromiseKit = {
87+
promise: resultPromiseRaw,
88+
resolve: resolvePromise,
89+
};
90+
(makePromiseKit as unknown as MockInstance).mockReturnValueOnce(
91+
successPromiseKit,
92+
);
93+
const resultPromise = kernelQueue.enqueueMessage(target, method, args);
94+
expect(kernelStore.initKernelPromise).toHaveBeenCalled();
95+
expect(kernelStore.incrementRefCount).toHaveBeenCalledWith(
96+
target,
97+
'queue|target',
98+
);
99+
expect(kernelStore.incrementRefCount).toHaveBeenCalledWith(
100+
'kp1',
101+
'queue|result',
102+
);
103+
expect(kernelStore.enqueueRun).toHaveBeenCalledWith({
104+
type: 'send',
105+
target,
106+
message: expect.objectContaining({
107+
methargs: expect.anything(),
108+
result: 'kp1',
109+
}),
110+
});
111+
expect(kernelQueue.subscriptions.has('kp1')).toBe(true);
112+
const handler = kernelQueue.subscriptions.get('kp1');
113+
expect(handler).toBeDefined();
114+
resolvePromise(resultValue);
115+
const result = await resultPromise;
116+
expect(result).toStrictEqual(resultValue);
117+
});
118+
});
119+
120+
describe('enqueueRun', () => {
121+
it('adds an item to the run queue', () => {
122+
const item: RunQueueItemSend = {
123+
type: 'send',
124+
target: 'ko123',
125+
message: {} as Message,
126+
};
127+
(kernelStore.runQueueLength as unknown as MockInstance).mockReturnValue(
128+
0,
129+
);
130+
kernelQueue.enqueueRun(item);
131+
expect(kernelStore.enqueueRun).toHaveBeenCalledWith(item);
132+
});
133+
});
134+
135+
describe('enqueueNotify', () => {
136+
it('creates a notify item and adds it to the run queue', () => {
137+
const vatId = 'v1';
138+
const kpid = 'kp123';
139+
kernelQueue.enqueueNotify(vatId, kpid);
140+
const expectedNotifyItem: RunQueueItemNotify = {
141+
type: 'notify',
142+
vatId,
143+
kpid,
144+
};
145+
expect(kernelStore.enqueueRun).toHaveBeenCalledWith(expectedNotifyItem);
146+
expect(kernelStore.incrementRefCount).toHaveBeenCalledWith(
147+
kpid,
148+
'notify',
149+
);
150+
});
151+
});
152+
153+
describe('resolvePromises', () => {
154+
it('resolves kernel promises and notifies subscribers', () => {
155+
const vatId = 'v1';
156+
const kpid = 'kp123';
157+
const resolution: VatOneResolution = [
158+
kpid,
159+
false,
160+
{ body: 'resolved value', slots: ['slot1'] } as CapData<KRef>,
161+
];
162+
(kernelStore.getKernelPromise as unknown as MockInstance).mockReturnValue(
163+
{
164+
state: 'unresolved',
165+
decider: vatId,
166+
subscribers: ['v2', 'v3'],
167+
},
168+
);
169+
const resolveHandler = vi.fn();
170+
kernelQueue.subscriptions.set(kpid, resolveHandler);
171+
kernelQueue.resolvePromises(vatId, [resolution]);
172+
expect(kernelStore.incrementRefCount).toHaveBeenCalledWith(
173+
kpid,
174+
'resolve|kpid',
175+
);
176+
expect(kernelStore.incrementRefCount).toHaveBeenCalledWith(
177+
'slot1',
178+
'resolve|slot',
179+
);
180+
expect(kernelStore.enqueueRun).toHaveBeenCalledWith({
181+
type: 'notify',
182+
vatId: 'v2',
183+
kpid,
184+
});
185+
expect(kernelStore.enqueueRun).toHaveBeenCalledWith({
186+
type: 'notify',
187+
vatId: 'v3',
188+
kpid,
189+
});
190+
expect(kernelStore.resolveKernelPromise).toHaveBeenCalledWith(
191+
kpid,
192+
false,
193+
{ body: 'resolved value', slots: ['slot1'] },
194+
);
195+
expect(resolveHandler).toHaveBeenCalledWith({
196+
body: 'resolved value',
197+
slots: ['slot1'],
198+
});
199+
expect(kernelQueue.subscriptions.has(kpid)).toBe(false);
200+
});
201+
202+
it('throws error if a promise is already resolved', () => {
203+
const vatId = 'v1';
204+
const kpid = 'kp123';
205+
const resolution: VatOneResolution = [
206+
kpid,
207+
false,
208+
{ body: 'resolved value', slots: [] } as CapData<KRef>,
209+
];
210+
(kernelStore.getKernelPromise as unknown as MockInstance).mockReturnValue(
211+
{
212+
state: 'fulfilled',
213+
decider: vatId,
214+
},
215+
);
216+
expect(() => kernelQueue.resolvePromises(vatId, [resolution])).toThrow(
217+
'"kp123" was already resolved',
218+
);
219+
});
220+
221+
it('throws error if the resolver is not the decider', () => {
222+
const vatId = 'v1';
223+
const wrongVatId = 'v2';
224+
const kpid = 'kp123';
225+
const resolution: VatOneResolution = [
226+
kpid,
227+
false,
228+
{ body: 'resolved value', slots: [] } as CapData<KRef>,
229+
];
230+
(kernelStore.getKernelPromise as unknown as MockInstance).mockReturnValue(
231+
{
232+
state: 'unresolved',
233+
decider: wrongVatId,
234+
},
235+
);
236+
expect(() => kernelQueue.resolvePromises(vatId, [resolution])).toThrow(
237+
'"v1" not permitted to resolve "kp123" because "its decider is v2"',
238+
);
239+
});
240+
});
241+
});

packages/kernel/src/types.test.ts

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import { describe, it, expect } from 'vitest';
22

3-
import { isVatConfig } from './types.ts';
3+
import {
4+
isVatConfig,
5+
insistMessage,
6+
queueTypeFromActionType,
7+
isGCActionType,
8+
insistGCActionType,
9+
isGCAction,
10+
RunQueueItemType,
11+
isVatMessageId,
12+
} from './types.ts';
413

514
describe('isVatConfig', () => {
615
it.each([
@@ -86,3 +95,125 @@ describe('isVatConfig', () => {
8695
expect(isVatConfig(value)).toBe(false);
8796
});
8897
});
98+
99+
describe('insistMessage', () => {
100+
it('does not throw for valid message objects', () => {
101+
const validMessage = {
102+
methargs: { body: 'body content', slots: [] },
103+
result: 'kp1',
104+
};
105+
106+
expect(() => insistMessage(validMessage)).not.toThrow();
107+
});
108+
109+
it.each([
110+
{ name: 'empty object', value: {} },
111+
{ name: 'incomplete methargs', value: { methargs: {} } },
112+
{ name: 'missing slots', value: { methargs: { body: 'body' } } },
113+
{ name: 'missing methargs', value: { result: 'kp1' } },
114+
])('throws for $name', ({ value }) => {
115+
expect(() => insistMessage(value)).toThrow('not a valid message');
116+
});
117+
});
118+
119+
describe('queueTypeFromActionType', () => {
120+
it('maps GC action types to queue event types', () => {
121+
expect(queueTypeFromActionType.get('dropExport')).toBe(
122+
RunQueueItemType.dropExports,
123+
);
124+
expect(queueTypeFromActionType.get('retireExport')).toBe(
125+
RunQueueItemType.retireExports,
126+
);
127+
expect(queueTypeFromActionType.get('retireImport')).toBe(
128+
RunQueueItemType.retireImports,
129+
);
130+
expect(queueTypeFromActionType.size).toBe(3);
131+
});
132+
});
133+
134+
describe('isGCActionType', () => {
135+
it.each(['dropExport', 'retireExport', 'retireImport'])(
136+
'returns true for valid GC action type %s',
137+
(value) => {
138+
expect(isGCActionType(value)).toBe(true);
139+
},
140+
);
141+
142+
it.each([
143+
{ name: 'invalid string', value: 'invalidAction' },
144+
{ name: 'empty string', value: '' },
145+
{ name: 'number', value: 123 },
146+
{ name: 'null', value: null },
147+
{ name: 'undefined', value: undefined },
148+
])('returns false for $name', ({ value }) => {
149+
expect(isGCActionType(value)).toBe(false);
150+
});
151+
});
152+
153+
describe('insistGCActionType', () => {
154+
it.each(['dropExport', 'retireExport', 'retireImport'])(
155+
'does not throw for valid GC action type %s',
156+
(value) => {
157+
expect(() => insistGCActionType(value)).not.toThrow();
158+
},
159+
);
160+
161+
it.each([
162+
{ name: 'invalid string', value: 'invalidAction' },
163+
{ name: 'empty string', value: '' },
164+
{ name: 'number', value: 123 },
165+
{ name: 'null', value: null },
166+
{ name: 'undefined', value: undefined },
167+
])('throws for $name', ({ value }) => {
168+
expect(() => insistGCActionType(value)).toThrow('not a valid GCActionType');
169+
});
170+
});
171+
172+
describe('isGCAction', () => {
173+
it.each([
174+
'v1 dropExport ko123',
175+
'v2 retireExport ko456',
176+
'v3 retireImport ko789',
177+
])('returns true for valid GC action %s', (value) => {
178+
expect(isGCAction(value)).toBe(true);
179+
});
180+
181+
it.each([
182+
{ name: 'invalid vatId', value: 'invalid dropExport ko123' },
183+
{ name: 'invalid action type', value: 'v1 invalidAction ko123' },
184+
{ name: 'invalid kref', value: 'v1 dropExport invalid' },
185+
{ name: 'number', value: 123 },
186+
{ name: 'null', value: null },
187+
{ name: 'undefined', value: undefined },
188+
{ name: 'missing spaces', value: 'v1dropExportko123' },
189+
])('returns false for $name', ({ value }) => {
190+
expect(isGCAction(value)).toBe(false);
191+
});
192+
});
193+
194+
describe('isVatMessageId', () => {
195+
it.each(['m0', 'm1', 'm42', 'm123456789'])(
196+
'returns true for valid message ID %s',
197+
(id) => {
198+
expect(isVatMessageId(id)).toBe(true);
199+
},
200+
);
201+
202+
it.each([
203+
{ name: 'wrong prefix x', value: 'x1' },
204+
{ name: 'wrong prefix n', value: 'n42' },
205+
{ name: 'missing number part', value: 'm' },
206+
{ name: 'non-numeric suffix (a)', value: 'ma' },
207+
{ name: 'non-numeric suffix (1a)', value: 'm1a' },
208+
{ name: 'non-numeric suffix (42x)', value: 'm42x' },
209+
{ name: 'reversed format', value: '1m' },
210+
{ name: 'double prefix', value: 'mm1' },
211+
{ name: 'number', value: 123 },
212+
{ name: 'null', value: null },
213+
{ name: 'undefined', value: undefined },
214+
{ name: 'object', value: {} },
215+
{ name: 'array', value: [] },
216+
])('returns false for $name', ({ value }) => {
217+
expect(isVatMessageId(value)).toBe(false);
218+
});
219+
});

vitest.config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,10 @@ export default defineConfig({
8888
lines: 78.25,
8989
},
9090
'packages/kernel/**': {
91-
statements: 87.8,
92-
functions: 92.21,
93-
branches: 74.01,
94-
lines: 87.76,
91+
statements: 88.87,
92+
functions: 93.44,
93+
branches: 77.06,
94+
lines: 88.84,
9595
},
9696
'packages/nodejs/**': {
9797
statements: 72.91,

0 commit comments

Comments
 (0)