Skip to content

Commit 90ebe27

Browse files
committed
test(kernel-browser-runtime): Add CapTP infrastructure tests
Add comprehensive tests for the CapTP infrastructure: - background-captp.test.ts: Tests for utility functions and makeBackgroundCapTP - kernel-facade.test.ts: Tests for facade delegation to kernel methods - kernel-captp.test.ts: Tests for makeKernelCapTP factory - captp.integration.test.ts: Full round-trip E() tests with real endoify Configure vitest with inline projects to use different setupFiles: - Unit tests use mock-endoify for isolated testing - Integration tests use real endoify for CapTP/E() functionality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 6483366 commit 90ebe27

File tree

8 files changed

+708
-16
lines changed

8 files changed

+708
-16
lines changed

packages/kernel-browser-runtime/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
},
8484
"devDependencies": {
8585
"@arethetypeswrong/cli": "^0.17.4",
86+
"@endo/eventual-send": "^1.3.4",
8687
"@metamask/auto-changelog": "^5.0.1",
8788
"@metamask/eslint-config": "^14.0.0",
8889
"@metamask/eslint-config-nodejs": "^14.0.0",
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import '@ocap/repo-tools/test-utils/mock-endoify';
2+
3+
import { describe, it, expect, vi, beforeEach } from 'vitest';
4+
5+
import {
6+
isCapTPNotification,
7+
getCapTPMessage,
8+
makeCapTPNotification,
9+
makeBackgroundCapTP,
10+
} from './background-captp.ts';
11+
import type { CapTPMessage, CapTPNotification } from './background-captp.ts';
12+
13+
describe('isCapTPNotification', () => {
14+
it('returns true for valid CapTP notification', () => {
15+
const notification = {
16+
jsonrpc: '2.0',
17+
method: 'captp',
18+
params: [{ type: 'foo' }],
19+
};
20+
expect(isCapTPNotification(notification)).toBe(true);
21+
});
22+
23+
it('returns false when method is not "captp"', () => {
24+
const message = {
25+
jsonrpc: '2.0',
26+
method: 'other',
27+
params: [{ type: 'foo' }],
28+
};
29+
expect(isCapTPNotification(message)).toBe(false);
30+
});
31+
32+
it('returns false when params is not an array', () => {
33+
const message = {
34+
jsonrpc: '2.0',
35+
method: 'captp',
36+
params: { type: 'foo' },
37+
};
38+
expect(isCapTPNotification(message as never)).toBe(false);
39+
});
40+
41+
it('returns false when params is empty', () => {
42+
const message = {
43+
jsonrpc: '2.0',
44+
method: 'captp',
45+
params: [],
46+
};
47+
expect(isCapTPNotification(message)).toBe(false);
48+
});
49+
50+
it('returns false when params has more than one element', () => {
51+
const message = {
52+
jsonrpc: '2.0',
53+
method: 'captp',
54+
params: [{ type: 'foo' }, { type: 'bar' }],
55+
};
56+
expect(isCapTPNotification(message)).toBe(false);
57+
});
58+
59+
it('returns true for JSON-RPC request with id if it matches captp format', () => {
60+
// A request with an id is still a valid captp message format-wise
61+
const request = {
62+
jsonrpc: '2.0',
63+
id: 1,
64+
method: 'captp',
65+
params: [{ type: 'foo' }],
66+
};
67+
expect(isCapTPNotification(request)).toBe(true);
68+
});
69+
});
70+
71+
describe('getCapTPMessage', () => {
72+
it('extracts CapTP message from valid notification', () => {
73+
const captpMessage: CapTPMessage = { type: 'CTP_CALL', methargs: [] };
74+
const notification: CapTPNotification = {
75+
jsonrpc: '2.0',
76+
method: 'captp',
77+
params: [captpMessage],
78+
};
79+
expect(getCapTPMessage(notification)).toStrictEqual(captpMessage);
80+
});
81+
82+
it('throws for non-CapTP notification', () => {
83+
const message = {
84+
jsonrpc: '2.0',
85+
method: 'other',
86+
params: [],
87+
};
88+
expect(() => getCapTPMessage(message)).toThrow('Not a CapTP notification');
89+
});
90+
91+
it('throws when params is empty', () => {
92+
const message = {
93+
jsonrpc: '2.0',
94+
method: 'captp',
95+
params: [],
96+
};
97+
expect(() => getCapTPMessage(message)).toThrow('Not a CapTP notification');
98+
});
99+
});
100+
101+
describe('makeCapTPNotification', () => {
102+
it('wraps CapTP message in JSON-RPC notification', () => {
103+
const captpMessage: CapTPMessage = { type: 'CTP_CALL', target: 'ko1' };
104+
const result = makeCapTPNotification(captpMessage);
105+
106+
expect(result).toStrictEqual({
107+
jsonrpc: '2.0',
108+
method: 'captp',
109+
params: [captpMessage],
110+
});
111+
});
112+
113+
it('creates valid notification that passes isCapTPNotification', () => {
114+
const captpMessage: CapTPMessage = { type: 'CTP_RESOLVE' };
115+
const notification = makeCapTPNotification(captpMessage);
116+
117+
expect(isCapTPNotification(notification)).toBe(true);
118+
});
119+
});
120+
121+
describe('makeBackgroundCapTP', () => {
122+
let sendMock: ReturnType<typeof vi.fn>;
123+
124+
beforeEach(() => {
125+
sendMock = vi.fn();
126+
});
127+
128+
it('returns object with dispatch, getKernel, and abort', () => {
129+
const capTP = makeBackgroundCapTP({ send: sendMock });
130+
131+
expect(capTP).toHaveProperty('dispatch');
132+
expect(capTP).toHaveProperty('getKernel');
133+
expect(capTP).toHaveProperty('abort');
134+
expect(typeof capTP.dispatch).toBe('function');
135+
expect(typeof capTP.getKernel).toBe('function');
136+
expect(typeof capTP.abort).toBe('function');
137+
});
138+
139+
it('getKernel returns a promise', () => {
140+
const capTP = makeBackgroundCapTP({ send: sendMock });
141+
const result = capTP.getKernel();
142+
143+
expect(result).toBeInstanceOf(Promise);
144+
});
145+
146+
it('calls send function when dispatching bootstrap request', () => {
147+
const capTP = makeBackgroundCapTP({ send: sendMock });
148+
149+
// Calling getKernel triggers a bootstrap request (ignore unhandled promise)
150+
capTP.getKernel().catch(() => undefined);
151+
152+
// CapTP should have sent a message to request bootstrap
153+
expect(sendMock).toHaveBeenCalled();
154+
const sentMessage = sendMock.mock.calls[0][0] as CapTPMessage;
155+
expect(sentMessage).toBeDefined();
156+
});
157+
158+
it('dispatch returns boolean', () => {
159+
const capTP = makeBackgroundCapTP({ send: sendMock });
160+
161+
// Dispatch a dummy message (will return false since it's not a valid CapTP message)
162+
const result = capTP.dispatch({ type: 'unknown' });
163+
164+
expect(typeof result).toBe('boolean');
165+
});
166+
});

packages/kernel-browser-runtime/src/index.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import '@ocap/repo-tools/test-utils/mock-endoify';
2+
13
import { describe, expect, it } from 'vitest';
24

35
import * as indexModule from './index.ts';
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// Real endoify needed for CapTP and E() to work properly
2+
// eslint-disable-next-line import-x/no-extraneous-dependencies
3+
import '@metamask/kernel-shims/endoify';
4+
5+
import { E } from '@endo/eventual-send';
6+
import type { ClusterConfig, Kernel } from '@metamask/ocap-kernel';
7+
import { describe, it, expect, vi, beforeEach } from 'vitest';
8+
9+
import { makeKernelCapTP } from './kernel-captp.ts';
10+
import { makeBackgroundCapTP } from '../../background-captp.ts';
11+
import type { CapTPMessage } from '../../background-captp.ts';
12+
13+
/**
14+
* Integration tests for CapTP communication between background and kernel endpoints.
15+
*
16+
* These tests validate that the two CapTP endpoints can communicate correctly
17+
* and that E() works properly with the kernel facade remote presence.
18+
*/
19+
describe('CapTP Integration', () => {
20+
let mockKernel: Kernel;
21+
let kernelCapTP: ReturnType<typeof makeKernelCapTP>;
22+
let backgroundCapTP: ReturnType<typeof makeBackgroundCapTP>;
23+
24+
beforeEach(() => {
25+
// Create mock kernel with method implementations
26+
mockKernel = {
27+
launchSubcluster: vi.fn().mockResolvedValue({
28+
body: '#{"rootKref":"ko1"}',
29+
slots: ['ko1'],
30+
}),
31+
terminateSubcluster: vi.fn().mockResolvedValue(undefined),
32+
queueMessage: vi.fn().mockResolvedValue({
33+
body: '#{"result":"message-sent"}',
34+
slots: [],
35+
}),
36+
getStatus: vi.fn().mockResolvedValue({
37+
vats: [{ id: 'v1', name: 'test-vat' }],
38+
subclusters: ['sc1'],
39+
remoteComms: false,
40+
}),
41+
pingVat: vi.fn().mockResolvedValue({
42+
pingVatResult: 'pong',
43+
roundTripMs: 5,
44+
}),
45+
} as unknown as Kernel;
46+
47+
// Wire up CapTP endpoints to dispatch messages synchronously to each other
48+
// This simulates direct message passing for testing
49+
50+
// Kernel-side: exposes facade as bootstrap
51+
kernelCapTP = makeKernelCapTP({
52+
kernel: mockKernel,
53+
send: (message: CapTPMessage) => {
54+
// Dispatch synchronously for testing
55+
backgroundCapTP.dispatch(message);
56+
},
57+
});
58+
59+
// Background-side: gets remote presence of kernel
60+
backgroundCapTP = makeBackgroundCapTP({
61+
send: (message: CapTPMessage) => {
62+
// Dispatch synchronously for testing
63+
kernelCapTP.dispatch(message);
64+
},
65+
});
66+
});
67+
68+
describe('bootstrap', () => {
69+
it('background can get kernel remote presence via getKernel', async () => {
70+
// Request the kernel facade - with synchronous dispatch, this resolves immediately
71+
const kernel = await backgroundCapTP.getKernel();
72+
expect(kernel).toBeDefined();
73+
});
74+
});
75+
76+
describe('ping', () => {
77+
it('e(kernel).ping() returns "pong"', async () => {
78+
// Get kernel remote presence
79+
const kernel = await backgroundCapTP.getKernel();
80+
81+
// Call ping via E()
82+
const result = await E(kernel).ping();
83+
expect(result).toBe('pong');
84+
});
85+
});
86+
87+
describe('getStatus', () => {
88+
it('e(kernel).getStatus() returns status from mock kernel', async () => {
89+
// Get kernel remote presence
90+
const kernel = await backgroundCapTP.getKernel();
91+
92+
// Call getStatus via E()
93+
const result = await E(kernel).getStatus();
94+
expect(result).toStrictEqual({
95+
vats: [{ id: 'v1', name: 'test-vat' }],
96+
subclusters: ['sc1'],
97+
remoteComms: false,
98+
});
99+
100+
expect(mockKernel.getStatus).toHaveBeenCalled();
101+
});
102+
});
103+
104+
describe('launchSubcluster', () => {
105+
it('e(kernel).launchSubcluster() passes arguments correctly', async () => {
106+
const config: ClusterConfig = {
107+
bootstrap: 'v1',
108+
vats: {
109+
v1: {
110+
bundleSpec: 'test-source',
111+
},
112+
},
113+
};
114+
115+
// Get kernel remote presence
116+
const kernel = await backgroundCapTP.getKernel();
117+
118+
// Call launchSubcluster via E()
119+
const result = await E(kernel).launchSubcluster(config);
120+
expect(result).toStrictEqual({
121+
body: '#{"rootKref":"ko1"}',
122+
slots: ['ko1'],
123+
});
124+
125+
expect(mockKernel.launchSubcluster).toHaveBeenCalledWith(config);
126+
});
127+
});
128+
129+
describe('terminateSubcluster', () => {
130+
it('e(kernel).terminateSubcluster() delegates to kernel', async () => {
131+
// Get kernel remote presence
132+
const kernel = await backgroundCapTP.getKernel();
133+
134+
// Call terminateSubcluster via E()
135+
await E(kernel).terminateSubcluster('sc1');
136+
expect(mockKernel.terminateSubcluster).toHaveBeenCalledWith('sc1');
137+
});
138+
});
139+
140+
describe('queueMessage', () => {
141+
it('e(kernel).queueMessage() passes arguments correctly', async () => {
142+
const target = 'ko1';
143+
const method = 'doSomething';
144+
const args = ['arg1', { nested: 'value' }];
145+
146+
// Get kernel remote presence
147+
const kernel = await backgroundCapTP.getKernel();
148+
149+
// Call queueMessage via E()
150+
const result = await E(kernel).queueMessage(target, method, args);
151+
expect(result).toStrictEqual({
152+
body: '#{"result":"message-sent"}',
153+
slots: [],
154+
});
155+
156+
expect(mockKernel.queueMessage).toHaveBeenCalledWith(
157+
target,
158+
method,
159+
args,
160+
);
161+
});
162+
});
163+
164+
describe('pingVat', () => {
165+
it('e(kernel).pingVat() delegates to kernel', async () => {
166+
// Get kernel remote presence
167+
const kernel = await backgroundCapTP.getKernel();
168+
169+
// Call pingVat via E()
170+
const result = await E(kernel).pingVat('v1');
171+
expect(result).toStrictEqual({
172+
pingVatResult: 'pong',
173+
roundTripMs: 5,
174+
});
175+
176+
expect(mockKernel.pingVat).toHaveBeenCalledWith('v1');
177+
});
178+
});
179+
180+
describe('error propagation', () => {
181+
it('errors from kernel methods propagate to background', async () => {
182+
const error = new Error('Kernel operation failed');
183+
vi.mocked(mockKernel.getStatus).mockRejectedValueOnce(error);
184+
185+
// Get kernel remote presence
186+
const kernel = await backgroundCapTP.getKernel();
187+
188+
// Call getStatus which will fail
189+
await expect(E(kernel).getStatus()).rejects.toThrow(
190+
'Kernel operation failed',
191+
);
192+
});
193+
});
194+
});

0 commit comments

Comments
 (0)