Skip to content

Commit 6086dce

Browse files
sirtimidclaude
andcommitted
fix(ocap-kernel): log the result promise a failed delivery leaves settled
Review follow-up. The caller then gets the endpoint's answer rather than `DELIVERY_FAILED`, and nothing else said so. The regression test runs the crank's reference count audit, and `KernelRouter.test.ts`'s shared store mock reports an unresolved promise by default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ea2bdea commit 6086dce

3 files changed

Lines changed: 21 additions & 13 deletions

File tree

‎packages/ocap-kernel/src/KernelRouter.result-promise.test.ts‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/nodejs';
2+
import type { Logger } from '@metamask/logger';
23
import { describe, it, expect, vi } from 'vitest';
34

45
import { KernelQueue } from './KernelQueue.ts';
@@ -9,8 +10,7 @@ import type { EndpointHandle, KRef, RunQueueItem } from './types.ts';
910

1011
/**
1112
* `KernelRouter`'s own tests mock `resolvePromises`, so nothing there can see a
12-
* second resolution of one promise — which is a `Fail`, and reaches the run
13-
* loop from inside the very catch meant to contain the delivery's failure.
13+
* second resolution of one promise. These run against a real store and queue.
1414
*/
1515
describe('a result promise whose delivery fails', () => {
1616
/**
@@ -22,10 +22,12 @@ describe('a result promise whose delivery fails', () => {
2222
async function makeFixture(deliverMessage: () => Promise<never>): Promise<{
2323
kernelStore: ReturnType<typeof makeKernelStore>;
2424
kernelQueue: KernelQueue;
25+
logger: Logger;
2526
runCrank: () => Promise<void>;
2627
}> {
2728
const kdb = await makeSQLKernelDatabase({ dbFilename: ':memory:' });
2829
const kernelStore = makeKernelStore(kdb);
30+
kernelStore.setRefCountAuditing(true);
2931
const kernelQueue = new KernelQueue(kernelStore, async () => undefined);
3032
const endpoint = {
3133
deliverMessage: vi.fn(deliverMessage),
@@ -35,11 +37,13 @@ describe('a result promise whose delivery fails', () => {
3537
deliverRetireImports: vi.fn(),
3638
deliverBringOutYourDead: vi.fn(),
3739
} as unknown as EndpointHandle;
40+
const logger = { error: vi.fn(), log: vi.fn() } as unknown as Logger;
3841
const kernelRouter = new KernelRouter(
3942
kernelStore,
4043
kernelQueue,
4144
() => endpoint,
4245
() => undefined,
46+
logger,
4347
);
4448

4549
// The run loop's crank, minus the run loop.
@@ -50,12 +54,13 @@ describe('a result promise whose delivery fails', () => {
5054
const item = kernelStore.dequeueRun() as RunQueueItem;
5155
await kernelRouter.deliver(item);
5256
kernelStore.collectGarbage();
57+
kernelStore.assertRefCountsIfAuditing();
5358
} finally {
5459
kernelStore.endCrank();
5560
}
5661
};
5762

58-
return { kernelStore, kernelQueue, runCrank };
63+
return { kernelStore, kernelQueue, logger, runCrank };
5964
}
6065

6166
/**
@@ -93,9 +98,6 @@ describe('a result promise whose delivery fails', () => {
9398
expect(kernelStore.getKernelPromise(result).state).toBe('rejected');
9499
});
95100

96-
// A vat that resolves the result and then loses its stream is one way in; a
97-
// stream that dies mid-delivery is the other, since retiring the vat rejects
98-
// every promise it was deciding and this one's decider was just set.
99101
it.each([
100102
{ how: 'fulfilled', rejected: false, state: 'fulfilled' },
101103
{ how: 'rejected', rejected: true, state: 'rejected' },
@@ -105,7 +107,7 @@ describe('a result promise whose delivery fails', () => {
105107
// Held in an object so the endpoint can reach it before the queue that
106108
// settles the promise exists.
107109
const endpointDoes = { settleTheResult: (): void => undefined };
108-
const { kernelStore, kernelQueue, runCrank } = await makeFixture(
110+
const { kernelStore, kernelQueue, logger, runCrank } = await makeFixture(
109111
async () => {
110112
endpointDoes.settleTheResult();
111113
throw new Error('stream closed');
@@ -117,12 +119,15 @@ describe('a result promise whose delivery fails', () => {
117119
[result, rejected, kser('settled by the endpoint')],
118120
]);
119121

120-
expect(await runCrank()).toBeUndefined();
122+
await runCrank();
121123

122124
const settled = kernelStore.getKernelPromise(result);
123125
expect(settled.state).toBe(state);
124126
// The endpoint's own settlement, not the delivery's `DELIVERY_FAILED`.
125127
expect(settled.value?.body).toContain('settled by the endpoint');
128+
expect(logger.error).toHaveBeenCalledWith(
129+
expect.stringContaining(`already ${state}`),
130+
);
126131
},
127132
);
128133
});

‎packages/ocap-kernel/src/KernelRouter.test.ts‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ describe('KernelRouter', () => {
4646
kernelStore = {
4747
getOwner: vi.fn(),
4848
isRevoked: vi.fn(),
49-
getKernelPromise: vi.fn(),
49+
// Unresolved is the default so that reading a promise's state is never
50+
// a `TypeError` in a test that does not care about it.
51+
getKernelPromise: vi.fn().mockReturnValue({ state: 'unresolved' }),
5052
decrementRefCount: vi.fn(),
5153
setPromiseDecider: vi.fn(),
5254
translateRefKtoE: vi.fn(
@@ -490,10 +492,6 @@ describe('KernelRouter', () => {
490492
(
491493
endpointHandle.deliverMessage as unknown as MockInstance
492494
).mockRejectedValueOnce(new Error('queue full'));
493-
(
494-
kernelStore.getKernelPromise as unknown as MockInstance
495-
).mockReturnValueOnce({ state: 'unresolved' });
496-
497495
const message: Message = {
498496
methargs: { body: 'method args', slots: [] },
499497
result: 'kp1',

‎packages/ocap-kernel/src/KernelRouter.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,11 @@ export class KernelRouter {
212212
): void {
213213
const { state } = this.#kernelStore.getKernelPromise(kpid);
214214
if (state !== 'unresolved') {
215+
// The caller gets the endpoint's answer, not the delivery's failure, and
216+
// the `error` above is the only other trace of either.
217+
this.#logger?.error(
218+
`Result ${kpid} of the failed delivery to ${endpointId} is already ${state}; leaving it alone`,
219+
);
215220
return;
216221
}
217222
this.#kernelQueue.resolvePromises(endpointId, [[kpid, true, failure]]);

0 commit comments

Comments
 (0)