fix(coding-runtime): close Pi extension lifecycle races
This commit is contained in:
@@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user