mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-24 05:28:46 +02:00
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>
706 lines
23 KiB
TypeScript
706 lines
23 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
|
|
|
import { applySessionEventWorkspace } from '@renderer/lib/sessionWorkspace';
|
|
import type { SessionEventRecord } from '@shared/domain/event';
|
|
import type { SessionRunRecord } from '@shared/domain/runTimeline';
|
|
import { createWorkspaceSettings } from '@shared/domain/tooling';
|
|
import type { WorkspaceState } from '@shared/domain/workspace';
|
|
|
|
describe('session workspace helpers', () => {
|
|
function createWorkspace(): WorkspaceState {
|
|
return {
|
|
projects: [],
|
|
workflows: [],
|
|
workflowTemplates: [],
|
|
settings: createWorkspaceSettings(),
|
|
sessions: [
|
|
{
|
|
id: 'session-1',
|
|
projectId: 'project-1',
|
|
workflowId: 'workflow-1',
|
|
title: 'Session',
|
|
createdAt: '2026-03-23T00:00:00.000Z',
|
|
updatedAt: '2026-03-23T00:00:00.000Z',
|
|
status: 'idle',
|
|
messages: [],
|
|
runs: [],
|
|
},
|
|
],
|
|
selectedSessionId: 'session-1',
|
|
lastUpdatedAt: '2026-03-23T00:00:00.000Z',
|
|
};
|
|
}
|
|
|
|
test('applies message deltas by creating and appending assistant messages', () => {
|
|
const created = applySessionEventWorkspace(createWorkspace(), {
|
|
sessionId: 'session-1',
|
|
kind: 'message-delta',
|
|
occurredAt: '2026-03-23T00:00:01.000Z',
|
|
messageId: 'assistant-1',
|
|
authorName: 'Architect',
|
|
contentDelta: 'Hello',
|
|
content: 'Hello',
|
|
} satisfies SessionEventRecord);
|
|
|
|
expect(created?.sessions[0].messages).toEqual([
|
|
{
|
|
id: 'assistant-1',
|
|
role: 'assistant',
|
|
authorName: 'Architect',
|
|
content: 'Hello',
|
|
createdAt: '2026-03-23T00:00:01.000Z',
|
|
pending: true,
|
|
},
|
|
]);
|
|
|
|
const appended = applySessionEventWorkspace(created, {
|
|
sessionId: 'session-1',
|
|
kind: 'message-delta',
|
|
occurredAt: '2026-03-23T00:00:02.000Z',
|
|
messageId: 'assistant-1',
|
|
authorName: 'Architect',
|
|
contentDelta: ' world',
|
|
content: 'Hello world',
|
|
} satisfies SessionEventRecord);
|
|
|
|
expect(appended?.sessions[0].messages[0]).toMatchObject({
|
|
authorName: 'Architect',
|
|
content: 'Hello world',
|
|
pending: true,
|
|
});
|
|
});
|
|
|
|
test('marks streamed messages complete when the session event arrives', () => {
|
|
const workspace = applySessionEventWorkspace(createWorkspace(), {
|
|
sessionId: 'session-1',
|
|
kind: 'message-delta',
|
|
occurredAt: '2026-03-23T00:00:01.000Z',
|
|
messageId: 'assistant-1',
|
|
authorName: 'Implementer',
|
|
contentDelta: 'Done',
|
|
content: 'Done',
|
|
} satisfies SessionEventRecord);
|
|
|
|
const completed = applySessionEventWorkspace(workspace, {
|
|
sessionId: 'session-1',
|
|
kind: 'message-complete',
|
|
occurredAt: '2026-03-23T00:00:02.000Z',
|
|
messageId: 'assistant-1',
|
|
authorName: 'Implementer',
|
|
content: 'Done',
|
|
} satisfies SessionEventRecord);
|
|
|
|
expect(completed?.sessions[0].messages[0]).toMatchObject({
|
|
authorName: 'Implementer',
|
|
content: 'Done',
|
|
pending: false,
|
|
});
|
|
});
|
|
|
|
test('keeps snapshot-like streamed updates readable while a message is pending', () => {
|
|
const first = applySessionEventWorkspace(createWorkspace(), {
|
|
sessionId: 'session-1',
|
|
kind: 'message-delta',
|
|
occurredAt: '2026-03-23T00:00:01.000Z',
|
|
messageId: 'assistant-1',
|
|
authorName: 'Writer',
|
|
contentDelta: 'How about',
|
|
content: 'How about',
|
|
} satisfies SessionEventRecord);
|
|
|
|
const second = applySessionEventWorkspace(first, {
|
|
sessionId: 'session-1',
|
|
kind: 'message-delta',
|
|
occurredAt: '2026-03-23T00:00:02.000Z',
|
|
messageId: 'assistant-1',
|
|
authorName: 'Writer',
|
|
contentDelta: 'The **Ashen Crown** feels',
|
|
content: 'How about The **Ashen Crown** feels',
|
|
} satisfies SessionEventRecord);
|
|
|
|
const third = applySessionEventWorkspace(second, {
|
|
sessionId: 'session-1',
|
|
kind: 'message-delta',
|
|
occurredAt: '2026-03-23T00:00:03.000Z',
|
|
messageId: 'assistant-1',
|
|
authorName: 'Writer',
|
|
contentDelta: 'classic and timeless.',
|
|
content: 'How about The **Ashen Crown** feels classic and timeless.',
|
|
} satisfies SessionEventRecord);
|
|
|
|
expect(third?.sessions[0].messages[0]).toMatchObject({
|
|
authorName: 'Writer',
|
|
content: 'How about The **Ashen Crown** feels classic and timeless.',
|
|
pending: true,
|
|
});
|
|
});
|
|
|
|
test('applies final content from completion events without waiting for a workspace refresh', () => {
|
|
const workspace = applySessionEventWorkspace(createWorkspace(), {
|
|
sessionId: 'session-1',
|
|
kind: 'message-delta',
|
|
occurredAt: '2026-03-23T00:00:01.000Z',
|
|
messageId: 'assistant-1',
|
|
authorName: 'Reviewer',
|
|
contentDelta: 'Draft',
|
|
content: 'Draft',
|
|
} satisfies SessionEventRecord);
|
|
|
|
const completed = applySessionEventWorkspace(workspace, {
|
|
sessionId: 'session-1',
|
|
kind: 'message-complete',
|
|
occurredAt: '2026-03-23T00:00:02.000Z',
|
|
messageId: 'assistant-1',
|
|
authorName: 'Reviewer',
|
|
content: 'Draft polished into the final review.',
|
|
} satisfies SessionEventRecord);
|
|
|
|
expect(completed?.sessions[0].messages[0]).toMatchObject({
|
|
authorName: 'Reviewer',
|
|
content: 'Draft polished into the final review.',
|
|
pending: false,
|
|
});
|
|
});
|
|
|
|
test('updates session status and error state from session events', () => {
|
|
const running = applySessionEventWorkspace(createWorkspace(), {
|
|
sessionId: 'session-1',
|
|
kind: 'status',
|
|
occurredAt: '2026-03-23T00:00:01.000Z',
|
|
status: 'running',
|
|
} satisfies SessionEventRecord);
|
|
|
|
expect(running?.sessions[0]).toMatchObject({
|
|
status: 'running',
|
|
lastError: undefined,
|
|
});
|
|
|
|
const failed = applySessionEventWorkspace(running, {
|
|
sessionId: 'session-1',
|
|
kind: 'error',
|
|
occurredAt: '2026-03-23T00:00:02.000Z',
|
|
error: 'Boom',
|
|
} satisfies SessionEventRecord);
|
|
|
|
expect(failed?.sessions[0]).toMatchObject({
|
|
status: 'error',
|
|
lastError: 'Boom',
|
|
});
|
|
});
|
|
|
|
test('applies live run snapshots without waiting for a workspace refresh', () => {
|
|
const run: SessionRunRecord = {
|
|
id: 'run-1',
|
|
requestId: 'turn-1',
|
|
projectId: 'project-1',
|
|
projectPath: 'C:\\workspace\\alpha',
|
|
workspaceKind: 'project',
|
|
workflowId: 'workflow-1',
|
|
workflowName: 'Sequential Review',
|
|
workflowMode: 'sequential',
|
|
triggerMessageId: 'msg-user-1',
|
|
startedAt: '2026-03-23T00:00:01.000Z',
|
|
status: 'running',
|
|
agents: [
|
|
{
|
|
agentId: 'agent-1',
|
|
agentName: 'Writer',
|
|
model: 'gpt-5.4',
|
|
},
|
|
],
|
|
events: [
|
|
{
|
|
id: 'run-event-1',
|
|
kind: 'run-started',
|
|
occurredAt: '2026-03-23T00:00:01.000Z',
|
|
status: 'completed',
|
|
messageId: 'msg-user-1',
|
|
},
|
|
],
|
|
};
|
|
|
|
const updated = applySessionEventWorkspace(createWorkspace(), {
|
|
sessionId: 'session-1',
|
|
kind: 'run-updated',
|
|
occurredAt: '2026-03-23T00:00:01.000Z',
|
|
run,
|
|
} satisfies SessionEventRecord);
|
|
|
|
expect(updated?.sessions[0].runs).toEqual([run]);
|
|
});
|
|
|
|
test('auto-completes previous pending assistant messages when a new message starts', () => {
|
|
const first = applySessionEventWorkspace(createWorkspace(), {
|
|
sessionId: 'session-1',
|
|
kind: 'message-delta',
|
|
occurredAt: '2026-03-23T00:00:01.000Z',
|
|
messageId: 'assistant-1',
|
|
authorName: 'Primary Agent',
|
|
contentDelta: 'Exploring the codebase...',
|
|
content: 'Exploring the codebase...',
|
|
} satisfies SessionEventRecord);
|
|
|
|
expect(first?.sessions[0].messages).toHaveLength(1);
|
|
expect(first?.sessions[0].messages[0]).toMatchObject({
|
|
id: 'assistant-1',
|
|
pending: true,
|
|
});
|
|
|
|
const second = applySessionEventWorkspace(first, {
|
|
sessionId: 'session-1',
|
|
kind: 'message-delta',
|
|
occurredAt: '2026-03-23T00:00:02.000Z',
|
|
messageId: 'assistant-2',
|
|
authorName: 'Primary Agent',
|
|
contentDelta: 'Still exploring...',
|
|
content: 'Still exploring...',
|
|
} satisfies SessionEventRecord);
|
|
|
|
expect(second?.sessions[0].messages).toHaveLength(2);
|
|
expect(second?.sessions[0].messages[0]).toMatchObject({
|
|
id: 'assistant-1',
|
|
content: 'Exploring the codebase...',
|
|
pending: false,
|
|
});
|
|
expect(second?.sessions[0].messages[1]).toMatchObject({
|
|
id: 'assistant-2',
|
|
content: 'Still exploring...',
|
|
pending: true,
|
|
});
|
|
});
|
|
|
|
test('ignores events for unknown sessions', () => {
|
|
const workspace = createWorkspace();
|
|
expect(
|
|
applySessionEventWorkspace(workspace, {
|
|
sessionId: 'missing',
|
|
kind: 'message-delta',
|
|
occurredAt: '2026-03-23T00:00:01.000Z',
|
|
messageId: 'assistant-1',
|
|
authorName: 'Architect',
|
|
contentDelta: 'Hello',
|
|
} satisfies SessionEventRecord),
|
|
).toBe(workspace);
|
|
});
|
|
|
|
test('reclassifies a message as thinking when message-reclassified event arrives', () => {
|
|
const workspace = applySessionEventWorkspace(createWorkspace(), {
|
|
sessionId: 'session-1',
|
|
kind: 'message-delta',
|
|
occurredAt: '2026-03-23T00:00:01.000Z',
|
|
messageId: 'assistant-1',
|
|
authorName: 'Primary Agent',
|
|
contentDelta: 'Let me search...',
|
|
content: 'Let me search...',
|
|
} satisfies SessionEventRecord);
|
|
|
|
const reclassified = applySessionEventWorkspace(workspace, {
|
|
sessionId: 'session-1',
|
|
kind: 'message-reclassified',
|
|
occurredAt: '2026-03-23T00:00:02.000Z',
|
|
messageId: 'assistant-1',
|
|
messageKind: 'thinking',
|
|
} satisfies SessionEventRecord);
|
|
|
|
expect(reclassified?.sessions[0].messages[0]).toMatchObject({
|
|
id: 'assistant-1',
|
|
messageKind: 'thinking',
|
|
});
|
|
});
|
|
|
|
test('ignores message-reclassified for an already reclassified message', () => {
|
|
let workspace = applySessionEventWorkspace(createWorkspace(), {
|
|
sessionId: 'session-1',
|
|
kind: 'message-delta',
|
|
occurredAt: '2026-03-23T00:00:01.000Z',
|
|
messageId: 'assistant-1',
|
|
authorName: 'Primary Agent',
|
|
contentDelta: 'Let me search...',
|
|
content: 'Let me search...',
|
|
} satisfies SessionEventRecord);
|
|
|
|
workspace = applySessionEventWorkspace(workspace, {
|
|
sessionId: 'session-1',
|
|
kind: 'message-reclassified',
|
|
occurredAt: '2026-03-23T00:00:02.000Z',
|
|
messageId: 'assistant-1',
|
|
messageKind: 'thinking',
|
|
} satisfies SessionEventRecord);
|
|
|
|
const duplicate = applySessionEventWorkspace(workspace, {
|
|
sessionId: 'session-1',
|
|
kind: 'message-reclassified',
|
|
occurredAt: '2026-03-23T00:00:03.000Z',
|
|
messageId: 'assistant-1',
|
|
messageKind: 'thinking',
|
|
} satisfies SessionEventRecord);
|
|
|
|
// Should return the same reference (no change)
|
|
expect(duplicate).toBe(workspace);
|
|
});
|
|
|
|
test('ignores message-reclassified for unknown message ids', () => {
|
|
const workspace = createWorkspace();
|
|
const result = applySessionEventWorkspace(workspace, {
|
|
sessionId: 'session-1',
|
|
kind: 'message-reclassified',
|
|
occurredAt: '2026-03-23T00:00:01.000Z',
|
|
messageId: 'nonexistent',
|
|
messageKind: 'thinking',
|
|
} satisfies SessionEventRecord);
|
|
|
|
expect(result).toBe(workspace);
|
|
});
|
|
|
|
test('uses resolved content from main process and falls back to simple append', () => {
|
|
// When content is present (main process resolved it), use it directly
|
|
const first = applySessionEventWorkspace(createWorkspace(), {
|
|
sessionId: 'session-1',
|
|
kind: 'message-delta',
|
|
occurredAt: '2026-03-23T00:00:01.000Z',
|
|
messageId: 'assistant-1',
|
|
authorName: 'Agent',
|
|
contentDelta: 'Hello',
|
|
content: 'Hello',
|
|
} satisfies SessionEventRecord);
|
|
|
|
// Main process sends full resolved content on every delta
|
|
const second = applySessionEventWorkspace(first, {
|
|
sessionId: 'session-1',
|
|
kind: 'message-delta',
|
|
occurredAt: '2026-03-23T00:00:02.000Z',
|
|
messageId: 'assistant-1',
|
|
authorName: 'Agent',
|
|
contentDelta: ' world',
|
|
content: 'Hello world',
|
|
} satisfies SessionEventRecord);
|
|
|
|
expect(second?.sessions[0].messages[0].content).toBe('Hello world');
|
|
|
|
// Backward compat: if content is missing, simple append of contentDelta
|
|
const fallback = applySessionEventWorkspace(second, {
|
|
sessionId: 'session-1',
|
|
kind: 'message-delta',
|
|
occurredAt: '2026-03-23T00:00:03.000Z',
|
|
messageId: 'assistant-1',
|
|
authorName: 'Agent',
|
|
contentDelta: '!',
|
|
} satisfies SessionEventRecord);
|
|
|
|
expect(fallback?.sessions[0].messages[0].content).toBe('Hello world!');
|
|
});
|
|
|
|
test('sets currentIntent from assistant-intent events', () => {
|
|
const workspace = createWorkspace();
|
|
const updated = applySessionEventWorkspace(workspace, {
|
|
sessionId: 'session-1',
|
|
kind: 'assistant-intent',
|
|
occurredAt: '2026-03-23T00:00:01.000Z',
|
|
intent: 'Exploring codebase',
|
|
} satisfies SessionEventRecord);
|
|
|
|
expect(updated?.sessions[0].currentIntent).toBe('Exploring codebase');
|
|
});
|
|
|
|
test('clears currentIntent when session transitions to idle', () => {
|
|
let workspace = applySessionEventWorkspace(createWorkspace(), {
|
|
sessionId: 'session-1',
|
|
kind: 'status',
|
|
occurredAt: '2026-03-23T00:00:01.000Z',
|
|
status: 'running',
|
|
} satisfies SessionEventRecord);
|
|
|
|
workspace = applySessionEventWorkspace(workspace, {
|
|
sessionId: 'session-1',
|
|
kind: 'assistant-intent',
|
|
occurredAt: '2026-03-23T00:00:02.000Z',
|
|
intent: 'Writing tests',
|
|
} satisfies SessionEventRecord);
|
|
|
|
expect(workspace?.sessions[0].currentIntent).toBe('Writing tests');
|
|
|
|
workspace = applySessionEventWorkspace(workspace, {
|
|
sessionId: 'session-1',
|
|
kind: 'status',
|
|
occurredAt: '2026-03-23T00:00:03.000Z',
|
|
status: 'idle',
|
|
} satisfies SessionEventRecord);
|
|
|
|
expect(workspace?.sessions[0].currentIntent).toBeUndefined();
|
|
});
|
|
|
|
test('ignores duplicate assistant-intent events', () => {
|
|
const workspace = applySessionEventWorkspace(createWorkspace(), {
|
|
sessionId: 'session-1',
|
|
kind: 'assistant-intent',
|
|
occurredAt: '2026-03-23T00:00:01.000Z',
|
|
intent: 'Exploring codebase',
|
|
} satisfies SessionEventRecord);
|
|
|
|
const duplicate = applySessionEventWorkspace(workspace, {
|
|
sessionId: 'session-1',
|
|
kind: 'assistant-intent',
|
|
occurredAt: '2026-03-23T00:00:02.000Z',
|
|
intent: 'Exploring codebase',
|
|
} satisfies SessionEventRecord);
|
|
|
|
expect(duplicate).toBe(workspace);
|
|
});
|
|
|
|
test('preserves currentIntent when status changes to running', () => {
|
|
let workspace = applySessionEventWorkspace(createWorkspace(), {
|
|
sessionId: 'session-1',
|
|
kind: 'assistant-intent',
|
|
occurredAt: '2026-03-23T00:00:01.000Z',
|
|
intent: 'Analyzing code',
|
|
} satisfies SessionEventRecord);
|
|
|
|
workspace = applySessionEventWorkspace(workspace, {
|
|
sessionId: 'session-1',
|
|
kind: 'status',
|
|
occurredAt: '2026-03-23T00:00:02.000Z',
|
|
status: 'running',
|
|
} satisfies SessionEventRecord);
|
|
|
|
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,
|
|
});
|
|
});
|
|
});
|