mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-28 05:43:57 +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');
|
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