fix(coding-runtime): close Pi extension lifecycle races

This commit is contained in:
2026-08-23 10:13:10 +08:00
parent 3861c3286a
commit 31de325cd8
9 changed files with 197 additions and 14 deletions

View File

@@ -75,4 +75,43 @@ describe('Pi interaction store', () => {
expect(changes.map(({ status }) => status)).toEqual(['cancelled', 'cancelled']);
expect(store.list()).toEqual([]);
});
it('makes response ownership atomic across double submit and generation invalidation', async () => {
const sent: PiRpcCommand[] = [];
const changes: ConversationInteraction[] = [];
let cancelResource = () => undefined;
let releaseSend = () => undefined;
const sendGate = new Promise<void>((resolve) => { releaseSend = resolve; });
const store = new PiInteractionStore({
getState: () => ({
conversationId: 'conversation-a', workerId: 'worker-a', state: 'running', generation: 1,
session: { piSessionId: 'session-a', sessionKey: 'key-a' },
}),
getActiveRun: () => ({ generation: 1, runId: 'run-1' }),
send: async (_conversationId, command) => {
sent.push(command);
await sendGate;
},
trackGenerationResource: (input) => {
cancelResource = input.cancel;
return () => undefined;
},
}, (interaction) => changes.push(interaction));
store.open('conversation-a', 1, 'run-1', {
type: 'extension_ui_request', id: 'question-race', method: 'confirm', title: 'Continue?',
});
const first = store.respond('conversation-a', {
interactionId: 'question-race', confirmed: true,
});
await expect(store.respond('conversation-a', {
interactionId: 'question-race', confirmed: false,
})).rejects.toThrow('already in progress');
cancelResource();
releaseSend();
await expect(first).rejects.toThrow('stale');
expect(sent).toHaveLength(1);
expect(changes.map(({ status }) => status)).toEqual(['cancelled']);
});
});