Skip to content

Commit bad09bc

Browse files
rekmarksclaude
andcommitted
test(kernel-browser-runtime): Add unit tests for kref-presence and convertKrefsToStandins
- Move convertKrefsToStandins from kernel-facade.ts to kref-presence.ts for better organization - Export convertKrefsToStandins for use by kernel-facade - Add comprehensive unit tests for convertKrefsToStandins (20 tests covering kref conversion, arrays, objects, primitives) - Add unit tests for makePresenceManager (3 tests for kref resolution and memoization) - Add integration test in kernel-facade.test.ts verifying kref conversion in queueMessage Co-Authored-By: Claude <[email protected]>
1 parent ca7a10d commit bad09bc

File tree

4 files changed

+360
-31
lines changed

4 files changed

+360
-31
lines changed

packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-facade.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,37 @@ describe('makeKernelFacade', () => {
122122
expect(mockKernel.queueMessage).toHaveBeenCalledTimes(1);
123123
});
124124

125+
it('converts kref strings in args to standins', async () => {
126+
const target: KRef = 'ko1';
127+
const method = 'sendTo';
128+
// Use ko refs only - kp refs become promise standins with different structure
129+
const args = ['ko42', { target: 'ko99', data: 'hello' }];
130+
131+
await facade.queueMessage(target, method, args);
132+
133+
// Verify the call was made
134+
expect(mockKernel.queueMessage).toHaveBeenCalledTimes(1);
135+
136+
// Get the actual args passed to kernel
137+
const [, , processedArgs] = vi.mocked(mockKernel.queueMessage).mock
138+
.calls[0]!;
139+
140+
// First arg should be a standin with getKref method
141+
expect(processedArgs[0]).toHaveProperty('getKref');
142+
expect((processedArgs[0] as { getKref: () => string }).getKref()).toBe(
143+
'ko42',
144+
);
145+
146+
// Second arg should be an object with converted kref
147+
const secondArg = processedArgs[1] as {
148+
target: { getKref: () => string };
149+
data: string;
150+
};
151+
expect(secondArg.target).toHaveProperty('getKref');
152+
expect(secondArg.target.getKref()).toBe('ko99');
153+
expect(secondArg.data).toBe('hello');
154+
});
155+
125156
it('returns result from kernel', async () => {
126157
const expectedResult = { body: '#{"answer":42}', slots: [] };
127158
vi.mocked(mockKernel.queueMessage).mockResolvedValueOnce(expectedResult);

packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-facade.ts

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,11 @@
11
import { makeDefaultExo } from '@metamask/kernel-utils/exo';
22
import type { Kernel, ClusterConfig, KRef, VatId } from '@metamask/ocap-kernel';
3-
import { kslot } from '@metamask/ocap-kernel';
43

4+
import { convertKrefsToStandins } from '../../kref-presence.ts';
55
import type { KernelFacade, LaunchResult } from '../../types.ts';
66

77
export type { KernelFacade } from '../../types.ts';
88

9-
/**
10-
* Recursively convert kref strings in a value to kernel standins.
11-
*
12-
* When the background sends kref strings as arguments, we need to convert
13-
* them to standin objects that kernel-marshal can serialize properly.
14-
*
15-
* @param value - The value to convert.
16-
* @returns The value with kref strings converted to standins.
17-
*/
18-
function convertKrefsToStandins(value: unknown): unknown {
19-
// Check if it's a kref string (ko* or kp*)
20-
if (typeof value === 'string' && /^k[op]\d+$/u.test(value)) {
21-
return kslot(value);
22-
}
23-
// Recursively process arrays
24-
if (Array.isArray(value)) {
25-
return value.map(convertKrefsToStandins);
26-
}
27-
// Recursively process plain objects
28-
if (typeof value === 'object' && value !== null) {
29-
const result: Record<string, unknown> = {};
30-
for (const [key, val] of Object.entries(value)) {
31-
result[key] = convertKrefsToStandins(val);
32-
}
33-
return result;
34-
}
35-
// Return primitives as-is
36-
return value;
37-
}
38-
399
/**
4010
* Create the kernel facade exo that exposes kernel methods via CapTP.
4111
*
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
import { passStyleOf } from '@endo/marshal';
2+
import { krefOf as kernelKrefOf } from '@metamask/ocap-kernel';
3+
import type { SlotValue } from '@metamask/ocap-kernel';
4+
import { describe, it, expect, vi, beforeEach } from 'vitest';
5+
6+
import type { PresenceManager } from './kref-presence.ts';
7+
import {
8+
convertKrefsToStandins,
9+
makePresenceManager,
10+
} from './kref-presence.ts';
11+
import type { KernelFacade } from './types.ts';
12+
13+
// EHandler type definition (copied to avoid import issues with mocking)
14+
type EHandler = {
15+
get?: (target: object, prop: PropertyKey) => Promise<unknown>;
16+
applyMethod?: (
17+
target: object,
18+
prop: PropertyKey,
19+
args: unknown[],
20+
) => Promise<unknown>;
21+
applyFunction?: (target: object, args: unknown[]) => Promise<unknown>;
22+
};
23+
24+
// Hoisted mock setup - these must be defined before vi.mock() is hoisted
25+
const { MockHandledPromise, mockE } = vi.hoisted(() => {
26+
/**
27+
* Mock HandledPromise that supports resolveWithPresence.
28+
*/
29+
class MockHandledPromiseImpl<TResult> extends Promise<TResult> {
30+
constructor(
31+
executor: (
32+
resolve: (value: TResult | PromiseLike<TResult>) => void,
33+
reject: (reason?: unknown) => void,
34+
resolveWithPresence: (handler: EHandler) => object,
35+
) => void,
36+
_handler?: EHandler,
37+
) {
38+
let presence: object | undefined;
39+
40+
const resolveWithPresence = (handler: EHandler): object => {
41+
// Create a simple presence object that can receive E() calls
42+
presence = new Proxy(
43+
{},
44+
{
45+
get(_target, prop) {
46+
if (prop === Symbol.toStringTag) {
47+
return 'Alleged: VatObject';
48+
}
49+
// Return a function that calls the handler
50+
return async (...args: unknown[]) => {
51+
if (typeof prop === 'string') {
52+
return handler.applyMethod?.(presence!, prop, args);
53+
}
54+
return undefined;
55+
};
56+
},
57+
},
58+
);
59+
return presence;
60+
};
61+
62+
super((resolve, reject) => {
63+
executor(resolve, reject, resolveWithPresence);
64+
});
65+
}
66+
}
67+
68+
// Mock E() to intercept calls on presences
69+
const mockEImpl = (target: object) => {
70+
return new Proxy(
71+
{},
72+
{
73+
get(_proxyTarget, prop) {
74+
if (typeof prop === 'string') {
75+
// Return a function that, when called, invokes the presence's method
76+
return (...args: unknown[]) => {
77+
const method = (target as Record<string, unknown>)[prop];
78+
if (typeof method === 'function') {
79+
return (method as (...a: unknown[]) => unknown)(...args);
80+
}
81+
// Try to get it from the proxy
82+
return (target as Record<string, (...a: unknown[]) => unknown>)[
83+
prop
84+
]?.(...args);
85+
};
86+
}
87+
return undefined;
88+
},
89+
},
90+
);
91+
};
92+
93+
return {
94+
MockHandledPromise: MockHandledPromiseImpl,
95+
mockE: mockEImpl,
96+
};
97+
});
98+
99+
// Apply mocks
100+
vi.mock('@endo/eventual-send', () => ({
101+
E: mockE,
102+
HandledPromise: MockHandledPromise,
103+
}));
104+
105+
describe('convertKrefsToStandins', () => {
106+
describe('kref string conversion', () => {
107+
it('converts ko kref string to standin', () => {
108+
const result = convertKrefsToStandins('ko123') as SlotValue;
109+
110+
expect(passStyleOf(result)).toBe('remotable');
111+
expect(kernelKrefOf(result)).toBe('ko123');
112+
});
113+
114+
it('converts kp kref string to standin promise', () => {
115+
const result = convertKrefsToStandins('kp456');
116+
117+
expect(passStyleOf(result)).toBe('promise');
118+
expect(kernelKrefOf(result as Promise<unknown>)).toBe('kp456');
119+
});
120+
121+
it('does not convert non-kref strings', () => {
122+
expect(convertKrefsToStandins('hello')).toBe('hello');
123+
expect(convertKrefsToStandins('k123')).toBe('k123');
124+
expect(convertKrefsToStandins('kox')).toBe('kox');
125+
expect(convertKrefsToStandins('ko')).toBe('ko');
126+
expect(convertKrefsToStandins('kp')).toBe('kp');
127+
expect(convertKrefsToStandins('ko123x')).toBe('ko123x');
128+
});
129+
});
130+
131+
describe('array processing', () => {
132+
it('recursively converts krefs in arrays', () => {
133+
const result = convertKrefsToStandins(['ko1', 'ko2']) as unknown[];
134+
135+
expect(result).toHaveLength(2);
136+
expect(kernelKrefOf(result[0] as SlotValue)).toBe('ko1');
137+
expect(kernelKrefOf(result[1] as SlotValue)).toBe('ko2');
138+
});
139+
140+
it('handles mixed arrays with krefs and primitives', () => {
141+
const result = convertKrefsToStandins([
142+
'ko1',
143+
42,
144+
'hello',
145+
true,
146+
]) as unknown[];
147+
148+
expect(result).toHaveLength(4);
149+
expect(kernelKrefOf(result[0] as SlotValue)).toBe('ko1');
150+
expect(result[1]).toBe(42);
151+
expect(result[2]).toBe('hello');
152+
expect(result[3]).toBe(true);
153+
});
154+
155+
it('handles empty arrays', () => {
156+
const result = convertKrefsToStandins([]);
157+
expect(result).toStrictEqual([]);
158+
});
159+
160+
it('handles nested arrays', () => {
161+
const result = convertKrefsToStandins([['ko1'], ['ko2']]) as unknown[][];
162+
163+
expect(kernelKrefOf(result[0]![0] as SlotValue)).toBe('ko1');
164+
expect(kernelKrefOf(result[1]![0] as SlotValue)).toBe('ko2');
165+
});
166+
});
167+
168+
describe('object processing', () => {
169+
it('recursively converts krefs in objects', () => {
170+
const result = convertKrefsToStandins({
171+
target: 'ko1',
172+
promise: 'kp2',
173+
}) as Record<string, unknown>;
174+
175+
expect(kernelKrefOf(result.target as SlotValue)).toBe('ko1');
176+
expect(kernelKrefOf(result.promise as Promise<unknown>)).toBe('kp2');
177+
});
178+
179+
it('handles nested objects', () => {
180+
const result = convertKrefsToStandins({
181+
outer: {
182+
inner: 'ko42',
183+
},
184+
}) as Record<string, Record<string, unknown>>;
185+
186+
expect(kernelKrefOf(result.outer!.inner as SlotValue)).toBe('ko42');
187+
});
188+
189+
it('handles empty objects', () => {
190+
const result = convertKrefsToStandins({});
191+
expect(result).toStrictEqual({});
192+
});
193+
194+
it('handles objects with mixed values', () => {
195+
const result = convertKrefsToStandins({
196+
kref: 'ko1',
197+
number: 123,
198+
string: 'text',
199+
boolean: false,
200+
nullValue: null,
201+
}) as Record<string, unknown>;
202+
203+
expect(kernelKrefOf(result.kref as SlotValue)).toBe('ko1');
204+
expect(result.number).toBe(123);
205+
expect(result.string).toBe('text');
206+
expect(result.boolean).toBe(false);
207+
expect(result.nullValue).toBeNull();
208+
});
209+
});
210+
211+
describe('primitive handling', () => {
212+
it('passes through numbers unchanged', () => {
213+
expect(convertKrefsToStandins(42)).toBe(42);
214+
expect(convertKrefsToStandins(0)).toBe(0);
215+
expect(convertKrefsToStandins(-1)).toBe(-1);
216+
});
217+
218+
it('passes through booleans unchanged', () => {
219+
expect(convertKrefsToStandins(true)).toBe(true);
220+
expect(convertKrefsToStandins(false)).toBe(false);
221+
});
222+
223+
it('passes through null unchanged', () => {
224+
expect(convertKrefsToStandins(null)).toBeNull();
225+
});
226+
227+
it('passes through undefined unchanged', () => {
228+
expect(convertKrefsToStandins(undefined)).toBeUndefined();
229+
});
230+
});
231+
});
232+
233+
describe('makePresenceManager', () => {
234+
let mockKernelFacade: KernelFacade;
235+
let presenceManager: PresenceManager;
236+
237+
beforeEach(() => {
238+
mockKernelFacade = {
239+
ping: vi.fn(),
240+
launchSubcluster: vi.fn(),
241+
terminateSubcluster: vi.fn(),
242+
queueMessage: vi.fn(),
243+
getStatus: vi.fn(),
244+
pingVat: vi.fn(),
245+
getVatRoot: vi.fn(),
246+
} as unknown as KernelFacade;
247+
248+
presenceManager = makePresenceManager({
249+
kernelFacade: mockKernelFacade,
250+
});
251+
});
252+
253+
describe('resolveKref', () => {
254+
it('returns a presence object for a kref', () => {
255+
const presence = presenceManager.resolveKref('ko42');
256+
257+
expect(presence).toBeDefined();
258+
expect(typeof presence).toBe('object');
259+
});
260+
261+
it('returns the same presence for the same kref (memoization)', () => {
262+
const presence1 = presenceManager.resolveKref('ko42');
263+
const presence2 = presenceManager.resolveKref('ko42');
264+
265+
expect(presence1).toBe(presence2);
266+
});
267+
268+
it('returns different presences for different krefs', () => {
269+
const presence1 = presenceManager.resolveKref('ko1');
270+
const presence2 = presenceManager.resolveKref('ko2');
271+
272+
expect(presence1).not.toBe(presence2);
273+
});
274+
});
275+
276+
describe('krefOf', () => {
277+
it('returns the kref for a known presence', () => {
278+
const presence = presenceManager.resolveKref('ko42');
279+
const kref = presenceManager.krefOf(presence);
280+
281+
expect(kref).toBe('ko42');
282+
});
283+
284+
it('returns undefined for an unknown object', () => {
285+
const unknownObject = { foo: 'bar' };
286+
const kref = presenceManager.krefOf(unknownObject);
287+
288+
expect(kref).toBeUndefined();
289+
});
290+
});
291+
292+
// Note: fromCapData and E() handler tests require the full Endo runtime
293+
// environment with proper SES lockdown. These behaviors are tested in
294+
// captp.integration.test.ts which runs with the real Endo setup.
295+
// Unit tests here focus on the kref↔presence mapping functionality.
296+
});

0 commit comments

Comments
 (0)