Skip to content

Commit fd045f4

Browse files
sirtimidclaude
andcommitted
test(ocap-kernel): tighten endowment tests from final review
- VatSupervisor: add positive-path assertion (init returns a result) alongside the existing negation to prevent vacuous pass - endowments: replace globalThis.fetch mutation with vi.stubGlobal + unstubAllGlobals in afterEach; removes the require-atomic-updates disable and avoids cross-test bleed - endowments: assert console.error fallback in the notify-swallow test so a silent-swallow regression would fail - network-caveat: add data:/blob: scheme cases to pin the empty- hostname rejection path; rename $scheme it.each label to $label for accuracy Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 412994f commit fd045f4

3 files changed

Lines changed: 60 additions & 33 deletions

File tree

packages/ocap-kernel/src/vats/VatSupervisor.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,15 @@ describe('VatSupervisor', () => {
369369
});
370370
await delay(50);
371371

372+
// With makeLiveSlots mocked, init completes and dispatch receives
373+
// the delivery result — assert that positive shape directly so the
374+
// test doesn't pass vacuously if init were to short-circuit.
375+
expect(dispatch).toHaveBeenCalledWith(
376+
expect.objectContaining({
377+
id: 'test-init',
378+
result: expect.anything(),
379+
}),
380+
);
372381
expect(dispatch).not.toHaveBeenCalledWith(
373382
expect.objectContaining({
374383
id: 'test-init',

packages/ocap-kernel/src/vats/endowments.test.ts

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ describe('createDefaultEndowments', () => {
3333
// frozen mock, and the resulting error surfaces on the NEXT test.
3434
afterEach(() => {
3535
state.override = null;
36+
vi.unstubAllGlobals();
37+
vi.restoreAllMocks();
3638
});
3739

3840
it('produces the expected global names', () => {
@@ -187,11 +189,15 @@ describe('createDefaultEndowments', () => {
187189
});
188190
});
189191

190-
it('swallows logger errors inside notify so fetch is not blocked', async () => {
192+
it('swallows logger errors inside notify and surfaces them via console.error', async () => {
191193
const logger = makeLogger();
194+
const transportError = new Error('transport broke');
192195
vi.spyOn(logger, 'debug').mockImplementation(() => {
193-
throw new Error('transport broke');
196+
throw transportError;
194197
});
198+
const consoleErrorSpy = vi
199+
.spyOn(console, 'error')
200+
.mockImplementation(() => undefined);
195201
let capturedNotify:
196202
| ((n: { method: string; params: unknown }) => Promise<void>)
197203
| undefined;
@@ -215,6 +221,11 @@ describe('createDefaultEndowments', () => {
215221
params: { source: 'fetch' },
216222
}),
217223
).toBeUndefined();
224+
225+
expect(consoleErrorSpy).toHaveBeenCalledWith(
226+
'[ocap-kernel] network endowment logger transport failed',
227+
transportError,
228+
);
218229
});
219230

220231
it('teardown aborts in-flight fetches started by the network factory', async () => {
@@ -223,40 +234,36 @@ describe('createDefaultEndowments', () => {
223234
// than rejects — the vat-visible fetch promise once teardown runs, so
224235
// the meaningful assertions are (1) the abort signal propagated into
225236
// the base fetch and (2) teardown returns cleanly without hanging on
226-
// the open connection.
227-
const originalFetch = globalThis.fetch;
237+
// the open connection. `vi.stubGlobal` + `unstubAllGlobals` in afterEach
238+
// keeps the global mutation scoped to this test.
228239
let abortReceived = false;
229-
globalThis.fetch = (async (_input, init) => {
230-
return await new Promise((_resolve, reject) => {
231-
const signal = init?.signal;
232-
signal?.addEventListener('abort', () => {
233-
abortReceived = true;
234-
reject(new Error('aborted'));
235-
});
236-
});
237-
}) as typeof fetch;
240+
vi.stubGlobal(
241+
'fetch',
242+
(async (_input: unknown, init?: RequestInit) =>
243+
await new Promise<Response>((_resolve, reject) => {
244+
init?.signal?.addEventListener('abort', () => {
245+
abortReceived = true;
246+
reject(new Error('aborted'));
247+
});
248+
})) as typeof fetch,
249+
);
238250

239-
try {
240-
const { globals, teardown } = createDefaultEndowments({
241-
logger: makeLogger(),
242-
});
243-
const fetchFn = globals.fetch as typeof fetch;
251+
const { globals, teardown } = createDefaultEndowments({
252+
logger: makeLogger(),
253+
});
254+
const fetchFn = globals.fetch as typeof fetch;
244255

245-
const inFlight = fetchFn('https://example.test/slow');
246-
// Suppress unhandled-rejection noise — the Snaps factory drops this
247-
// promise on teardown (neither resolves nor rejects it).
248-
inFlight.catch(() => undefined);
256+
const inFlight = fetchFn('https://example.test/slow');
257+
// Suppress unhandled-rejection noise — the Snaps factory drops this
258+
// promise on teardown (neither resolves nor rejects it).
259+
inFlight.catch(() => undefined);
249260

250-
// Give the factory a microtask to register the open connection.
251-
await new Promise((resolve) => setTimeout(resolve, 0));
261+
// Give the factory a microtask to register the open connection.
262+
await new Promise((resolve) => setTimeout(resolve, 0));
252263

253-
await teardown();
264+
await teardown();
254265

255-
expect(abortReceived).toBe(true);
256-
} finally {
257-
// eslint-disable-next-line require-atomic-updates
258-
globalThis.fetch = originalFetch;
259-
}
266+
expect(abortReceived).toBe(true);
260267
});
261268

262269
it('teardown cancels pending timers', async () => {

packages/ocap-kernel/src/vats/network-caveat.test.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,26 @@ describe('makeHostCaveat', () => {
4545
});
4646

4747
it.each([
48-
{ scheme: 'file:', input: 'file:///etc/passwd' },
49-
{ scheme: 'file: via Request', input: new Request('file:///etc/passwd') },
50-
])('rejects $scheme URLs with an fs-capability hint', async ({ input }) => {
48+
{ label: 'file: string input', input: 'file:///etc/passwd' },
49+
{ label: 'file: Request input', input: new Request('file:///etc/passwd') },
50+
])('rejects $label with an fs-capability hint', async ({ input }) => {
5151
const caveat = makeHostCaveat(['example.test']);
5252
await expect(caveat(input)).rejects.toThrow(
5353
/fetch cannot target file:\/\/ URLs.*fs platform capability/u,
5454
);
5555
});
5656

57+
it.each([
58+
{ label: 'data:', input: 'data:text/plain,hello' },
59+
{ label: 'blob:', input: 'blob:https://example.test/abc123' },
60+
])(
61+
'rejects $label URLs via the hostname check (opaque origin has empty hostname)',
62+
async ({ input }) => {
63+
const caveat = makeHostCaveat(['example.test']);
64+
await expect(caveat(input)).rejects.toThrow('Invalid host:');
65+
},
66+
);
67+
5768
it.each([
5869
{
5970
name: 'Request objects',

0 commit comments

Comments
 (0)