mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-28 23:48:39 +02:00
test: add streaming regression tests for turn lifecycle and transitions
Add 5 regression tests covering: - Thinking-to-response transition (reclassify → new message, no flash) - Concurrent multi-message streaming independence - Run-updated replacement semantics (no duplicates) - Full turn lifecycle (intent → deltas → complete → idle clears intent) - Message-complete content override of streamed content Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -464,4 +464,242 @@ describe('session workspace helpers', () => {
|
||||
|
||||
expect(workspace?.sessions[0].currentIntent).toBe('Analyzing code');
|
||||
});
|
||||
|
||||
/* ── Streaming regression tests ──────────────────────────── */
|
||||
|
||||
test('thinking-to-response transition: reclassified message stays hidden, new message becomes final', () => {
|
||||
// Step 1: first message streams as normal assistant content
|
||||
let ws = applySessionEventWorkspace(createWorkspace(), {
|
||||
sessionId: 'session-1',
|
||||
kind: 'message-delta',
|
||||
occurredAt: '2026-03-23T00:00:01.000Z',
|
||||
messageId: 'assistant-1',
|
||||
authorName: 'Agent',
|
||||
contentDelta: 'Let me look into this...',
|
||||
content: 'Let me look into this...',
|
||||
} satisfies SessionEventRecord);
|
||||
|
||||
// Step 2: reclassified as thinking (the agent decided to search first)
|
||||
ws = applySessionEventWorkspace(ws, {
|
||||
sessionId: 'session-1',
|
||||
kind: 'message-reclassified',
|
||||
occurredAt: '2026-03-23T00:00:02.000Z',
|
||||
messageId: 'assistant-1',
|
||||
messageKind: 'thinking',
|
||||
} satisfies SessionEventRecord);
|
||||
|
||||
expect(ws?.sessions[0].messages[0]).toMatchObject({
|
||||
id: 'assistant-1',
|
||||
messageKind: 'thinking',
|
||||
pending: true,
|
||||
});
|
||||
|
||||
// Step 3: a new actual response message starts streaming
|
||||
ws = applySessionEventWorkspace(ws, {
|
||||
sessionId: 'session-1',
|
||||
kind: 'message-delta',
|
||||
occurredAt: '2026-03-23T00:00:03.000Z',
|
||||
messageId: 'assistant-2',
|
||||
authorName: 'Agent',
|
||||
contentDelta: 'Here is the answer.',
|
||||
content: 'Here is the answer.',
|
||||
} satisfies SessionEventRecord);
|
||||
|
||||
// The thinking message should be auto-completed, new one is pending
|
||||
expect(ws?.sessions[0].messages).toHaveLength(2);
|
||||
expect(ws?.sessions[0].messages[0]).toMatchObject({
|
||||
id: 'assistant-1',
|
||||
messageKind: 'thinking',
|
||||
pending: false,
|
||||
});
|
||||
expect(ws?.sessions[0].messages[1]).toMatchObject({
|
||||
id: 'assistant-2',
|
||||
content: 'Here is the answer.',
|
||||
pending: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('concurrent multi-message streaming keeps messages independent', () => {
|
||||
// Two different assistant messages stream interleaved (e.g. multi-agent)
|
||||
let ws = applySessionEventWorkspace(createWorkspace(), {
|
||||
sessionId: 'session-1',
|
||||
kind: 'message-delta',
|
||||
occurredAt: '2026-03-23T00:00:01.000Z',
|
||||
messageId: 'msg-agent-a',
|
||||
authorName: 'Architect',
|
||||
contentDelta: 'Design: ',
|
||||
content: 'Design: ',
|
||||
} satisfies SessionEventRecord);
|
||||
|
||||
ws = applySessionEventWorkspace(ws, {
|
||||
sessionId: 'session-1',
|
||||
kind: 'message-delta',
|
||||
occurredAt: '2026-03-23T00:00:02.000Z',
|
||||
messageId: 'msg-agent-a',
|
||||
authorName: 'Architect',
|
||||
contentDelta: 'Use modules.',
|
||||
content: 'Design: Use modules.',
|
||||
} satisfies SessionEventRecord);
|
||||
|
||||
// Complete one message, verify others are unaffected
|
||||
ws = applySessionEventWorkspace(ws, {
|
||||
sessionId: 'session-1',
|
||||
kind: 'message-complete',
|
||||
occurredAt: '2026-03-23T00:00:04.000Z',
|
||||
messageId: 'msg-agent-a',
|
||||
authorName: 'Architect',
|
||||
content: 'Design: Use modules.',
|
||||
} satisfies SessionEventRecord);
|
||||
|
||||
expect(ws?.sessions[0].messages).toHaveLength(1);
|
||||
expect(ws?.sessions[0].messages[0]).toMatchObject({
|
||||
id: 'msg-agent-a',
|
||||
content: 'Design: Use modules.',
|
||||
pending: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('run-updated replaces existing run by id and handles new runs', () => {
|
||||
const runV1: SessionRunRecord = {
|
||||
id: 'run-1',
|
||||
requestId: 'turn-1',
|
||||
projectId: 'project-1',
|
||||
projectPath: 'C:\\workspace',
|
||||
workspaceKind: 'project',
|
||||
workflowId: 'workflow-1',
|
||||
workflowName: 'Review',
|
||||
workflowMode: 'sequential',
|
||||
triggerMessageId: 'msg-user-1',
|
||||
startedAt: '2026-03-23T00:00:01.000Z',
|
||||
status: 'running',
|
||||
agents: [],
|
||||
events: [],
|
||||
};
|
||||
|
||||
let ws = applySessionEventWorkspace(createWorkspace(), {
|
||||
sessionId: 'session-1',
|
||||
kind: 'run-updated',
|
||||
occurredAt: '2026-03-23T00:00:01.000Z',
|
||||
run: runV1,
|
||||
} satisfies SessionEventRecord);
|
||||
|
||||
expect(ws?.sessions[0].runs).toHaveLength(1);
|
||||
expect(ws?.sessions[0].runs[0].status).toBe('running');
|
||||
|
||||
// Run updated with new status and events
|
||||
const runV2: SessionRunRecord = {
|
||||
...runV1,
|
||||
status: 'completed',
|
||||
events: [
|
||||
{
|
||||
id: 'evt-1',
|
||||
kind: 'run-started',
|
||||
occurredAt: '2026-03-23T00:00:01.000Z',
|
||||
status: 'completed',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
ws = applySessionEventWorkspace(ws, {
|
||||
sessionId: 'session-1',
|
||||
kind: 'run-updated',
|
||||
occurredAt: '2026-03-23T00:00:02.000Z',
|
||||
run: runV2,
|
||||
} satisfies SessionEventRecord);
|
||||
|
||||
// Should replace, not duplicate
|
||||
expect(ws?.sessions[0].runs).toHaveLength(1);
|
||||
expect(ws?.sessions[0].runs[0].status).toBe('completed');
|
||||
expect(ws?.sessions[0].runs[0].events).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('full turn lifecycle: intent → deltas → complete → idle clears intent', () => {
|
||||
let ws = applySessionEventWorkspace(createWorkspace(), {
|
||||
sessionId: 'session-1',
|
||||
kind: 'status',
|
||||
occurredAt: '2026-03-23T00:00:00.000Z',
|
||||
status: 'running',
|
||||
} satisfies SessionEventRecord);
|
||||
|
||||
ws = applySessionEventWorkspace(ws, {
|
||||
sessionId: 'session-1',
|
||||
kind: 'assistant-intent',
|
||||
occurredAt: '2026-03-23T00:00:01.000Z',
|
||||
intent: 'Exploring codebase',
|
||||
} satisfies SessionEventRecord);
|
||||
|
||||
ws = applySessionEventWorkspace(ws, {
|
||||
sessionId: 'session-1',
|
||||
kind: 'message-delta',
|
||||
occurredAt: '2026-03-23T00:00:02.000Z',
|
||||
messageId: 'assistant-1',
|
||||
authorName: 'Agent',
|
||||
contentDelta: 'Found the issue.',
|
||||
content: 'Found the issue.',
|
||||
} satisfies SessionEventRecord);
|
||||
|
||||
// Intent survives during message streaming
|
||||
expect(ws?.sessions[0].currentIntent).toBe('Exploring codebase');
|
||||
expect(ws?.sessions[0].messages[0].pending).toBe(true);
|
||||
|
||||
ws = applySessionEventWorkspace(ws, {
|
||||
sessionId: 'session-1',
|
||||
kind: 'assistant-intent',
|
||||
occurredAt: '2026-03-23T00:00:03.000Z',
|
||||
intent: 'Fixing bug',
|
||||
} satisfies SessionEventRecord);
|
||||
|
||||
expect(ws?.sessions[0].currentIntent).toBe('Fixing bug');
|
||||
|
||||
ws = applySessionEventWorkspace(ws, {
|
||||
sessionId: 'session-1',
|
||||
kind: 'message-complete',
|
||||
occurredAt: '2026-03-23T00:00:04.000Z',
|
||||
messageId: 'assistant-1',
|
||||
authorName: 'Agent',
|
||||
content: 'Found the issue. Here is the fix.',
|
||||
} satisfies SessionEventRecord);
|
||||
|
||||
// Intent still active until idle
|
||||
expect(ws?.sessions[0].currentIntent).toBe('Fixing bug');
|
||||
|
||||
ws = applySessionEventWorkspace(ws, {
|
||||
sessionId: 'session-1',
|
||||
kind: 'status',
|
||||
occurredAt: '2026-03-23T00:00:05.000Z',
|
||||
status: 'idle',
|
||||
} satisfies SessionEventRecord);
|
||||
|
||||
expect(ws?.sessions[0].currentIntent).toBeUndefined();
|
||||
expect(ws?.sessions[0].status).toBe('idle');
|
||||
expect(ws?.sessions[0].messages[0].content).toBe('Found the issue. Here is the fix.');
|
||||
expect(ws?.sessions[0].messages[0].pending).toBe(false);
|
||||
});
|
||||
|
||||
test('message-complete content overrides previously streamed content', () => {
|
||||
let ws = applySessionEventWorkspace(createWorkspace(), {
|
||||
sessionId: 'session-1',
|
||||
kind: 'message-delta',
|
||||
occurredAt: '2026-03-23T00:00:01.000Z',
|
||||
messageId: 'assistant-1',
|
||||
authorName: 'Agent',
|
||||
contentDelta: 'Partial draft...',
|
||||
content: 'Partial draft...',
|
||||
} satisfies SessionEventRecord);
|
||||
|
||||
// Backend sends final authoritative content on completion
|
||||
ws = applySessionEventWorkspace(ws, {
|
||||
sessionId: 'session-1',
|
||||
kind: 'message-complete',
|
||||
occurredAt: '2026-03-23T00:00:02.000Z',
|
||||
messageId: 'assistant-1',
|
||||
authorName: 'Agent',
|
||||
content: 'The complete, polished final answer with all details.',
|
||||
} satisfies SessionEventRecord);
|
||||
|
||||
expect(ws?.sessions[0].messages[0]).toMatchObject({
|
||||
content: 'The complete, polished final answer with all details.',
|
||||
pending: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user