open-design/apps/daemon/tests/structured-streams.test.ts
RyanCheng77 b76e7196db
fix(daemon): dedupe Claude stream wrappers (#3334)
* fix(daemon): dedupe Claude stream wrappers

* fix(daemon): split Claude stream dedupe state

---------

Co-authored-by: 116405 <116405@ky-tech.com.cn>
2026-05-30 06:12:29 +00:00

321 lines
10 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { createClaudeStreamHandler } from '../src/claude-stream.js';
import { createCopilotStreamHandler } from '../src/copilot-stream.js';
import { mapPiRpcEvent } from '../src/pi-rpc.js';
describe('structured agent stream fixtures', () => {
it('emits TodoWrite tool_use from Claude Code stream JSON', () => {
const events: unknown[] = [];
const handler = createClaudeStreamHandler((event: unknown) => events.push(event));
handler.feed(`${JSON.stringify({
type: 'assistant',
message: {
id: 'msg-1',
content: [
{
type: 'tool_use',
id: 'toolu-1',
name: 'TodoWrite',
input: {
todos: [{ content: 'Run QA', status: 'pending' }],
},
},
],
},
})}\n`);
handler.flush();
expect(events).toContainEqual({
type: 'tool_use',
id: 'toolu-1',
name: 'TodoWrite',
input: {
todos: [{ content: 'Run QA', status: 'pending' }],
},
});
});
it('preserves streamed Claude Code tool input_json_delta payloads', () => {
const events: unknown[] = [];
const handler = createClaudeStreamHandler((event: unknown) => events.push(event));
handler.feed(`${JSON.stringify({
type: 'stream_event',
event: { type: 'message_start', message: { id: 'msg-1' } },
})}\n${JSON.stringify({
type: 'stream_event',
event: {
type: 'content_block_start',
index: 0,
content_block: { type: 'tool_use', id: 'toolu-1', name: 'Write' },
},
})}\n${JSON.stringify({
type: 'stream_event',
event: {
type: 'content_block_delta',
index: 0,
delta: { type: 'input_json_delta', partial_json: '{"file_path":"admin-dashboard.html",' },
},
})}\n${JSON.stringify({
type: 'stream_event',
event: {
type: 'content_block_delta',
index: 0,
delta: { type: 'input_json_delta', partial_json: '"content":"<html></html>"}' },
},
})}\n${JSON.stringify({
type: 'stream_event',
event: { type: 'content_block_stop', index: 0 },
})}\n${JSON.stringify({
type: 'assistant',
message: {
id: 'msg-1',
content: [{ type: 'tool_use', id: 'toolu-1', name: 'Write', input: {} }],
},
})}\n`);
handler.flush();
const toolUses = events.filter((event) => typeof event === 'object' && event !== null && (event as { type?: string }).type === 'tool_use');
expect(toolUses).toHaveLength(1);
expect(toolUses).toContainEqual({
type: 'tool_use',
id: 'toolu-1',
name: 'Write',
input: {
file_path: 'admin-dashboard.html',
content: '<html></html>',
},
});
});
it('does not duplicate streamed Claude Code text or thinking when final assistant wrapper has no id', () => {
const events: unknown[] = [];
const handler = createClaudeStreamHandler((event: unknown) => events.push(event));
handler.feed(`${JSON.stringify({
type: 'stream_event',
event: { type: 'message_start', message: { id: 'msg-1' } },
})}\n${JSON.stringify({
type: 'stream_event',
event: {
type: 'content_block_start',
index: 0,
content_block: { type: 'thinking' },
},
})}\n${JSON.stringify({
type: 'stream_event',
event: {
type: 'content_block_delta',
index: 0,
delta: { type: 'thinking_delta', thinking: 'Plan once.' },
},
})}\n${JSON.stringify({
type: 'stream_event',
event: { type: 'content_block_stop', index: 0 },
})}\n${JSON.stringify({
type: 'stream_event',
event: {
type: 'content_block_start',
index: 1,
content_block: { type: 'text' },
},
})}\n${JSON.stringify({
type: 'stream_event',
event: {
type: 'content_block_delta',
index: 1,
delta: { type: 'text_delta', text: 'Write once.' },
},
})}\n${JSON.stringify({
type: 'stream_event',
event: { type: 'content_block_stop', index: 1 },
})}\n${JSON.stringify({
type: 'assistant',
message: {
content: [
{ type: 'thinking', thinking: 'Plan once.' },
{ type: 'text', text: 'Write once.' },
],
},
})}\n`);
handler.flush();
expect(events.filter((event) => (
typeof event === 'object'
&& event !== null
&& (event as { type?: string }).type === 'thinking_delta'
))).toEqual([{ type: 'thinking_delta', delta: 'Plan once.' }]);
expect(events.filter((event) => (
typeof event === 'object'
&& event !== null
&& (event as { type?: string }).type === 'text_delta'
))).toEqual([{ type: 'text_delta', delta: 'Write once.' }]);
});
it('does not suppress later wrapper-only Claude Code text without an id after streamed output', () => {
const events: unknown[] = [];
const handler = createClaudeStreamHandler((event: unknown) => events.push(event));
handler.feed(`${JSON.stringify({
type: 'stream_event',
event: { type: 'message_start', message: { id: 'msg-1' } },
})}\n${JSON.stringify({
type: 'stream_event',
event: {
type: 'content_block_delta',
index: 0,
delta: { type: 'text_delta', text: 'Streamed once.' },
},
})}\n${JSON.stringify({
type: 'assistant',
message: {
content: [{ type: 'text', text: 'Streamed once.' }],
},
})}\n${JSON.stringify({
type: 'assistant',
message: {
content: [{ type: 'text', text: 'Wrapper only.' }],
},
})}\n`);
handler.flush();
expect(events.filter((event) => (
typeof event === 'object'
&& event !== null
&& (event as { type?: string }).type === 'text_delta'
))).toEqual([
{ type: 'text_delta', delta: 'Streamed once.' },
{ type: 'text_delta', delta: 'Wrapper only.' },
]);
});
it('keeps wrapper-only Claude Code text after streamed thinking without an id', () => {
const events: unknown[] = [];
const handler = createClaudeStreamHandler((event: unknown) => events.push(event));
handler.feed(`${JSON.stringify({
type: 'stream_event',
event: { type: 'message_start', message: { id: 'msg-1' } },
})}\n${JSON.stringify({
type: 'stream_event',
event: {
type: 'content_block_delta',
index: 0,
delta: { type: 'thinking_delta', thinking: 'Plan streamed.' },
},
})}\n${JSON.stringify({
type: 'assistant',
message: {
content: [
{ type: 'thinking', thinking: 'Plan streamed.' },
{ type: 'text', text: 'Answer from wrapper.' },
],
},
})}\n`);
handler.flush();
expect(events.filter((event) => (
typeof event === 'object'
&& event !== null
&& (event as { type?: string }).type === 'thinking_delta'
))).toEqual([{ type: 'thinking_delta', delta: 'Plan streamed.' }]);
expect(events.filter((event) => (
typeof event === 'object'
&& event !== null
&& (event as { type?: string }).type === 'text_delta'
))).toEqual([{ type: 'text_delta', delta: 'Answer from wrapper.' }]);
});
it('keeps wrapper-only Claude Code thinking after streamed text without an id', () => {
const events: unknown[] = [];
const handler = createClaudeStreamHandler((event: unknown) => events.push(event));
handler.feed(`${JSON.stringify({
type: 'stream_event',
event: { type: 'message_start', message: { id: 'msg-1' } },
})}\n${JSON.stringify({
type: 'stream_event',
event: {
type: 'content_block_delta',
index: 0,
delta: { type: 'text_delta', text: 'Answer streamed.' },
},
})}\n${JSON.stringify({
type: 'assistant',
message: {
content: [
{ type: 'text', text: 'Answer streamed.' },
{ type: 'thinking', thinking: 'Plan from wrapper.' },
],
},
})}\n`);
handler.flush();
expect(events.filter((event) => (
typeof event === 'object'
&& event !== null
&& (event as { type?: string }).type === 'text_delta'
))).toEqual([{ type: 'text_delta', delta: 'Answer streamed.' }]);
expect(events.filter((event) => (
typeof event === 'object'
&& event !== null
&& (event as { type?: string }).type === 'thinking_delta'
))).toEqual([{ type: 'thinking_delta', delta: 'Plan from wrapper.' }]);
});
it('emits TodoWrite tool_use from Pi RPC tool_execution events', () => {
const events: unknown[] = [];
const send = (_channel: string, payload: unknown) => { events.push(payload); };
const ctx = { runStartedAt: Date.now(), sentFirstToken: { value: false } };
mapPiRpcEvent(
{ type: 'tool_execution_start', toolCallId: 'pi-call-1', toolName: 'TodoWrite', args: { todos: [{ content: 'Run QA', status: 'pending' }] } },
send,
ctx,
);
mapPiRpcEvent(
{ type: 'tool_execution_end', toolCallId: 'pi-call-1', toolName: 'TodoWrite', result: { content: [{ type: 'text', text: 'written' }] }, isError: false },
send,
ctx,
);
expect(events).toContainEqual({
type: 'tool_use',
id: 'pi-call-1',
name: 'TodoWrite',
input: { todos: [{ content: 'Run QA', status: 'pending' }] },
});
expect(events).toContainEqual({
type: 'tool_result',
toolUseId: 'pi-call-1',
content: 'written',
isError: false,
});
});
it('emits TodoWrite tool_use from GitHub Copilot CLI JSON stream', () => {
const events: unknown[] = [];
const handler = createCopilotStreamHandler((event: unknown) => events.push(event));
handler.feed(`${JSON.stringify({
type: 'tool.execution_start',
data: {
toolCallId: 'call-1',
toolName: 'TodoWrite',
arguments: {
todos: [{ content: 'Run QA', status: 'pending' }],
},
},
})}\n`);
handler.flush();
expect(events).toContainEqual({
type: 'tool_use',
id: 'call-1',
name: 'TodoWrite',
input: {
todos: [{ content: 'Run QA', status: 'pending' }],
},
});
});
});