Files
makelore/tests/unit/pi-interaction.test.ts

130 lines
5.4 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it } from 'vitest';
import type { ConversationInteraction } from '../../electron/coding-runtime/contracts';
import { PiInteractionStore } from '../../electron/coding-runtime/pi/interaction';
import type { PiRpcCommand } from '../../electron/coding-runtime/pi/rpc-client';
import type { PiGenerationResourceInput } from '../../electron/coding-runtime/pi/worker-pool';
describe('Pi interaction store', () => {
it('responds by exact option or custom text, then rejects stale generation responses', async () => {
const sent: PiRpcCommand[] = [];
const changes: ConversationInteraction[] = [];
const resources = new Map<string, () => void>();
let generation = 1;
let runId = 'run-1';
const store = new PiInteractionStore({
getState: () => ({
conversationId: 'conversation-a', workerId: 'worker-a', state: 'running', generation,
session: { piSessionId: 'session-a', sessionKey: 'key-a' },
}),
getActiveRun: () => ({ generation, runId }),
send: async (_conversationId, command) => { sent.push(command); },
trackGenerationResource: (input: PiGenerationResourceInput) => {
resources.set(input.id, input.cancel);
return () => resources.delete(input.id);
},
}, (interaction) => changes.push(interaction));
const opened = store.open('conversation-a', 1, 'run-1', {
type: 'extension_ui_request', id: 'question-1', method: 'select', title: 'Choose',
options: ['Alpha', 'Beta'],
});
expect(opened?.status).toBe('pending');
await store.respond('conversation-a', {
interactionId: 'question-1', optionId: 'question-1:option:1',
});
expect(sent).toEqual([{
type: 'extension_ui_response', id: 'question-1', value: 'Beta',
}]);
expect(changes.at(-1)?.status).toBe('answered');
await expect(store.respond('conversation-a', {
interactionId: 'question-1', optionId: 'question-1:option:0',
})).rejects.toThrow('not pending');
store.open('conversation-a', 1, 'run-1', {
type: 'extension_ui_request', id: 'question-custom', method: 'select', title: 'Choose',
options: ['Alpha', 'Beta'],
});
await store.respond('conversation-a', {
interactionId: 'question-custom', value: ' Something else ',
});
expect(sent.at(-1)).toEqual({
type: 'extension_ui_response', id: 'question-custom', value: 'Something else',
});
expect(changes.at(-1)?.status).toBe('answered');
store.open('conversation-a', 1, 'run-1', {
type: 'extension_ui_request', id: 'question-2', method: 'confirm', title: 'Continue?', message: 'Proceed',
});
generation = 2;
runId = 'run-2';
await expect(store.respond('conversation-a', {
interactionId: 'question-2', confirmed: true,
})).rejects.toThrow('stale');
expect(changes.at(-1)?.status).toBe('cancelled');
});
it('cancels every pending dialog on abort', async () => {
const sent: PiRpcCommand[] = [];
const changes: ConversationInteraction[] = [];
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); },
trackGenerationResource: () => () => undefined,
}, (interaction) => changes.push(interaction));
for (const id of ['question-1', 'question-2']) {
store.open('conversation-a', 1, 'run-1', {
type: 'extension_ui_request', id, method: 'input', title: id,
});
}
await store.cancelRun('conversation-a', 'run-1', true);
expect(sent).toHaveLength(2);
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']);
});
});