refactor(webui): extract event projection helpers
This commit is contained in:
@@ -0,0 +1,600 @@
|
||||
{
|
||||
"cases": [
|
||||
{
|
||||
"name": "reasoning_then_streamed_answer",
|
||||
"chat_id": "fixture-reasoning",
|
||||
"initial_messages": [
|
||||
{
|
||||
"id": "fixture-user-reasoning",
|
||||
"role": "user",
|
||||
"content": "Explain event projection.",
|
||||
"turnId": "turn-reasoning",
|
||||
"turnPhase": "user",
|
||||
"turnSeq": 1,
|
||||
"createdAt": 1700000000000
|
||||
}
|
||||
],
|
||||
"live_events": [
|
||||
{
|
||||
"event": "reasoning_delta",
|
||||
"chat_id": "fixture-reasoning",
|
||||
"text": "Compare ",
|
||||
"turn_id": "turn-reasoning",
|
||||
"turn_phase": "reasoning",
|
||||
"turn_seq": 2
|
||||
},
|
||||
{
|
||||
"event": "reasoning_delta",
|
||||
"chat_id": "fixture-reasoning",
|
||||
"text": "state.",
|
||||
"turn_id": "turn-reasoning",
|
||||
"turn_phase": "reasoning",
|
||||
"turn_seq": 3
|
||||
},
|
||||
{
|
||||
"event": "reasoning_end",
|
||||
"chat_id": "fixture-reasoning",
|
||||
"turn_id": "turn-reasoning",
|
||||
"turn_phase": "reasoning",
|
||||
"turn_seq": 4
|
||||
},
|
||||
{
|
||||
"event": "delta",
|
||||
"chat_id": "fixture-reasoning",
|
||||
"text": "Use one ",
|
||||
"turn_id": "turn-reasoning",
|
||||
"turn_phase": "answer",
|
||||
"turn_seq": 5
|
||||
},
|
||||
{
|
||||
"event": "delta",
|
||||
"chat_id": "fixture-reasoning",
|
||||
"text": "fold.",
|
||||
"turn_id": "turn-reasoning",
|
||||
"turn_phase": "answer",
|
||||
"turn_seq": 6
|
||||
},
|
||||
{
|
||||
"event": "stream_end",
|
||||
"chat_id": "fixture-reasoning",
|
||||
"turn_id": "turn-reasoning",
|
||||
"turn_phase": "answer",
|
||||
"turn_seq": 7
|
||||
},
|
||||
{
|
||||
"event": "turn_end",
|
||||
"chat_id": "fixture-reasoning",
|
||||
"latency_ms": 42,
|
||||
"turn_id": "turn-reasoning",
|
||||
"turn_phase": "complete",
|
||||
"turn_seq": 8
|
||||
}
|
||||
],
|
||||
"transcript": [
|
||||
{
|
||||
"event": "user",
|
||||
"chat_id": "fixture-reasoning",
|
||||
"text": "Explain event projection.",
|
||||
"turn_id": "turn-reasoning",
|
||||
"turn_phase": "user",
|
||||
"turn_seq": 1,
|
||||
"created_at_ms": 1700000000000
|
||||
},
|
||||
{
|
||||
"event": "reasoning_delta",
|
||||
"chat_id": "fixture-reasoning",
|
||||
"text": "Compare ",
|
||||
"turn_id": "turn-reasoning",
|
||||
"turn_phase": "reasoning",
|
||||
"turn_seq": 2
|
||||
},
|
||||
{
|
||||
"event": "reasoning_delta",
|
||||
"chat_id": "fixture-reasoning",
|
||||
"text": "state.",
|
||||
"turn_id": "turn-reasoning",
|
||||
"turn_phase": "reasoning",
|
||||
"turn_seq": 3
|
||||
},
|
||||
{
|
||||
"event": "reasoning_end",
|
||||
"chat_id": "fixture-reasoning",
|
||||
"turn_id": "turn-reasoning",
|
||||
"turn_phase": "reasoning",
|
||||
"turn_seq": 4
|
||||
},
|
||||
{
|
||||
"event": "delta",
|
||||
"chat_id": "fixture-reasoning",
|
||||
"text": "Use one ",
|
||||
"turn_id": "turn-reasoning",
|
||||
"turn_phase": "answer",
|
||||
"turn_seq": 5
|
||||
},
|
||||
{
|
||||
"event": "delta",
|
||||
"chat_id": "fixture-reasoning",
|
||||
"text": "fold.",
|
||||
"turn_id": "turn-reasoning",
|
||||
"turn_phase": "answer",
|
||||
"turn_seq": 6
|
||||
},
|
||||
{
|
||||
"event": "stream_end",
|
||||
"chat_id": "fixture-reasoning",
|
||||
"turn_id": "turn-reasoning",
|
||||
"turn_phase": "answer",
|
||||
"turn_seq": 7
|
||||
},
|
||||
{
|
||||
"event": "turn_end",
|
||||
"chat_id": "fixture-reasoning",
|
||||
"latency_ms": 42,
|
||||
"turn_id": "turn-reasoning",
|
||||
"turn_phase": "complete",
|
||||
"turn_seq": 8
|
||||
}
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Explain event projection.",
|
||||
"turnId": "turn-reasoning",
|
||||
"turnPhase": "user",
|
||||
"turnSeq": 1
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Use one fold.",
|
||||
"reasoning": "Compare state.",
|
||||
"activitySegmentId": "segment-1",
|
||||
"latencyMs": 42,
|
||||
"turnId": "turn-reasoning",
|
||||
"turnPhase": "answer",
|
||||
"turnSeq": 6
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "length_recovery_merges_answer_segments",
|
||||
"chat_id": "fixture-length",
|
||||
"initial_messages": [
|
||||
{
|
||||
"id": "fixture-user-length",
|
||||
"role": "user",
|
||||
"content": "Continue after the limit.",
|
||||
"turnId": "turn-length",
|
||||
"turnPhase": "user",
|
||||
"turnSeq": 1,
|
||||
"createdAt": 1700000001000
|
||||
}
|
||||
],
|
||||
"live_events": [
|
||||
{
|
||||
"event": "delta",
|
||||
"chat_id": "fixture-length",
|
||||
"text": "first ",
|
||||
"turn_id": "turn-length",
|
||||
"turn_phase": "answer",
|
||||
"turn_seq": 2
|
||||
},
|
||||
{
|
||||
"event": "stream_end",
|
||||
"chat_id": "fixture-length",
|
||||
"text": "first ",
|
||||
"resuming": true,
|
||||
"merge_next": true,
|
||||
"turn_id": "turn-length",
|
||||
"turn_phase": "answer",
|
||||
"turn_seq": 3
|
||||
},
|
||||
{
|
||||
"event": "delta",
|
||||
"chat_id": "fixture-length",
|
||||
"text": "second",
|
||||
"turn_id": "turn-length",
|
||||
"turn_phase": "answer",
|
||||
"turn_seq": 4
|
||||
},
|
||||
{
|
||||
"event": "stream_end",
|
||||
"chat_id": "fixture-length",
|
||||
"turn_id": "turn-length",
|
||||
"turn_phase": "answer",
|
||||
"turn_seq": 5
|
||||
},
|
||||
{
|
||||
"event": "turn_end",
|
||||
"chat_id": "fixture-length",
|
||||
"latency_ms": 17,
|
||||
"turn_id": "turn-length",
|
||||
"turn_phase": "complete",
|
||||
"turn_seq": 6
|
||||
}
|
||||
],
|
||||
"transcript": [
|
||||
{
|
||||
"event": "user",
|
||||
"chat_id": "fixture-length",
|
||||
"text": "Continue after the limit.",
|
||||
"turn_id": "turn-length",
|
||||
"turn_phase": "user",
|
||||
"turn_seq": 1,
|
||||
"created_at_ms": 1700000001000
|
||||
},
|
||||
{
|
||||
"event": "delta",
|
||||
"chat_id": "fixture-length",
|
||||
"text": "first ",
|
||||
"turn_id": "turn-length",
|
||||
"turn_phase": "answer",
|
||||
"turn_seq": 2
|
||||
},
|
||||
{
|
||||
"event": "stream_end",
|
||||
"chat_id": "fixture-length",
|
||||
"text": "first ",
|
||||
"resuming": true,
|
||||
"merge_next": true,
|
||||
"turn_id": "turn-length",
|
||||
"turn_phase": "answer",
|
||||
"turn_seq": 3
|
||||
},
|
||||
{
|
||||
"event": "delta",
|
||||
"chat_id": "fixture-length",
|
||||
"text": "second",
|
||||
"turn_id": "turn-length",
|
||||
"turn_phase": "answer",
|
||||
"turn_seq": 4
|
||||
},
|
||||
{
|
||||
"event": "stream_end",
|
||||
"chat_id": "fixture-length",
|
||||
"turn_id": "turn-length",
|
||||
"turn_phase": "answer",
|
||||
"turn_seq": 5
|
||||
},
|
||||
{
|
||||
"event": "turn_end",
|
||||
"chat_id": "fixture-length",
|
||||
"latency_ms": 17,
|
||||
"turn_id": "turn-length",
|
||||
"turn_phase": "complete",
|
||||
"turn_seq": 6
|
||||
}
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Continue after the limit.",
|
||||
"turnId": "turn-length",
|
||||
"turnPhase": "user",
|
||||
"turnSeq": 1
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "first second",
|
||||
"latencyMs": 17,
|
||||
"turnId": "turn-length",
|
||||
"turnPhase": "answer",
|
||||
"turnSeq": 4
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tool_activity_then_complete_answer",
|
||||
"chat_id": "fixture-activity",
|
||||
"initial_messages": [
|
||||
{
|
||||
"id": "fixture-user-activity",
|
||||
"role": "user",
|
||||
"content": "Inspect the projection code.",
|
||||
"turnId": "turn-activity",
|
||||
"turnPhase": "user",
|
||||
"turnSeq": 1,
|
||||
"createdAt": 1700000002000
|
||||
}
|
||||
],
|
||||
"live_events": [
|
||||
{
|
||||
"event": "message",
|
||||
"chat_id": "fixture-activity",
|
||||
"kind": "tool_hint",
|
||||
"text": "search projection helpers",
|
||||
"turn_id": "turn-activity",
|
||||
"turn_phase": "activity",
|
||||
"turn_seq": 2
|
||||
},
|
||||
{
|
||||
"event": "reasoning_delta",
|
||||
"chat_id": "fixture-activity",
|
||||
"text": "Review results.",
|
||||
"turn_id": "turn-activity",
|
||||
"turn_phase": "reasoning",
|
||||
"turn_seq": 4
|
||||
},
|
||||
{
|
||||
"event": "reasoning_end",
|
||||
"chat_id": "fixture-activity",
|
||||
"turn_id": "turn-activity",
|
||||
"turn_phase": "reasoning",
|
||||
"turn_seq": 5
|
||||
},
|
||||
{
|
||||
"event": "message",
|
||||
"chat_id": "fixture-activity",
|
||||
"text": "Projection matches.",
|
||||
"turn_id": "turn-activity",
|
||||
"turn_phase": "answer",
|
||||
"turn_seq": 6
|
||||
},
|
||||
{
|
||||
"event": "turn_end",
|
||||
"chat_id": "fixture-activity",
|
||||
"latency_ms": 8,
|
||||
"turn_id": "turn-activity",
|
||||
"turn_phase": "complete",
|
||||
"turn_seq": 7
|
||||
}
|
||||
],
|
||||
"transcript": [
|
||||
{
|
||||
"event": "user",
|
||||
"chat_id": "fixture-activity",
|
||||
"text": "Inspect the projection code.",
|
||||
"turn_id": "turn-activity",
|
||||
"turn_phase": "user",
|
||||
"turn_seq": 1,
|
||||
"created_at_ms": 1700000002000
|
||||
},
|
||||
{
|
||||
"event": "message",
|
||||
"chat_id": "fixture-activity",
|
||||
"kind": "tool_hint",
|
||||
"text": "search projection helpers",
|
||||
"turn_id": "turn-activity",
|
||||
"turn_phase": "activity",
|
||||
"turn_seq": 2
|
||||
},
|
||||
{
|
||||
"event": "reasoning_delta",
|
||||
"chat_id": "fixture-activity",
|
||||
"text": "Review results.",
|
||||
"turn_id": "turn-activity",
|
||||
"turn_phase": "reasoning",
|
||||
"turn_seq": 4
|
||||
},
|
||||
{
|
||||
"event": "reasoning_end",
|
||||
"chat_id": "fixture-activity",
|
||||
"turn_id": "turn-activity",
|
||||
"turn_phase": "reasoning",
|
||||
"turn_seq": 5
|
||||
},
|
||||
{
|
||||
"event": "message",
|
||||
"chat_id": "fixture-activity",
|
||||
"text": "Projection matches.",
|
||||
"turn_id": "turn-activity",
|
||||
"turn_phase": "answer",
|
||||
"turn_seq": 6
|
||||
},
|
||||
{
|
||||
"event": "turn_end",
|
||||
"chat_id": "fixture-activity",
|
||||
"latency_ms": 8,
|
||||
"turn_id": "turn-activity",
|
||||
"turn_phase": "complete",
|
||||
"turn_seq": 7
|
||||
}
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Inspect the projection code.",
|
||||
"turnId": "turn-activity",
|
||||
"turnPhase": "user",
|
||||
"turnSeq": 1
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "search projection helpers",
|
||||
"kind": "trace",
|
||||
"traces": [
|
||||
"search projection helpers"
|
||||
],
|
||||
"activitySegmentId": "segment-1",
|
||||
"turnId": "turn-activity",
|
||||
"turnPhase": "activity",
|
||||
"turnSeq": 2
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Projection matches.",
|
||||
"reasoning": "Review results.",
|
||||
"activitySegmentId": "segment-1",
|
||||
"latencyMs": 8,
|
||||
"turnId": "turn-activity",
|
||||
"turnPhase": "answer",
|
||||
"turnSeq": 6
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "file_edit_lifecycle_merges_by_call_and_path",
|
||||
"chat_id": "fixture-file-edit",
|
||||
"initial_messages": [
|
||||
{
|
||||
"id": "fixture-user-file-edit",
|
||||
"role": "user",
|
||||
"content": "Update app.py.",
|
||||
"turnId": "turn-file-edit",
|
||||
"turnPhase": "user",
|
||||
"turnSeq": 1,
|
||||
"createdAt": 1700000003000
|
||||
}
|
||||
],
|
||||
"live_events": [
|
||||
{
|
||||
"event": "file_edit",
|
||||
"chat_id": "fixture-file-edit",
|
||||
"edits": [
|
||||
{
|
||||
"version": 1,
|
||||
"call_id": "call-edit",
|
||||
"tool": "edit_file",
|
||||
"path": "app.py",
|
||||
"phase": "start",
|
||||
"added": 0,
|
||||
"deleted": 0,
|
||||
"status": "editing"
|
||||
}
|
||||
],
|
||||
"turn_id": "turn-file-edit",
|
||||
"turn_phase": "activity",
|
||||
"turn_seq": 2
|
||||
},
|
||||
{
|
||||
"event": "file_edit",
|
||||
"chat_id": "fixture-file-edit",
|
||||
"edits": [
|
||||
{
|
||||
"version": 1,
|
||||
"call_id": "call-edit",
|
||||
"tool": "edit_file",
|
||||
"path": "app.py",
|
||||
"phase": "end",
|
||||
"added": 3,
|
||||
"deleted": 1,
|
||||
"status": "done"
|
||||
}
|
||||
],
|
||||
"turn_id": "turn-file-edit",
|
||||
"turn_phase": "activity",
|
||||
"turn_seq": 3
|
||||
},
|
||||
{
|
||||
"event": "message",
|
||||
"chat_id": "fixture-file-edit",
|
||||
"text": "Updated app.py.",
|
||||
"turn_id": "turn-file-edit",
|
||||
"turn_phase": "answer",
|
||||
"turn_seq": 4
|
||||
},
|
||||
{
|
||||
"event": "turn_end",
|
||||
"chat_id": "fixture-file-edit",
|
||||
"latency_ms": 9,
|
||||
"turn_id": "turn-file-edit",
|
||||
"turn_phase": "complete",
|
||||
"turn_seq": 5
|
||||
}
|
||||
],
|
||||
"transcript": [
|
||||
{
|
||||
"event": "user",
|
||||
"chat_id": "fixture-file-edit",
|
||||
"text": "Update app.py.",
|
||||
"turn_id": "turn-file-edit",
|
||||
"turn_phase": "user",
|
||||
"turn_seq": 1,
|
||||
"created_at_ms": 1700000003000
|
||||
},
|
||||
{
|
||||
"event": "file_edit",
|
||||
"chat_id": "fixture-file-edit",
|
||||
"edits": [
|
||||
{
|
||||
"version": 1,
|
||||
"call_id": "call-edit",
|
||||
"tool": "edit_file",
|
||||
"path": "app.py",
|
||||
"phase": "start",
|
||||
"added": 0,
|
||||
"deleted": 0,
|
||||
"status": "editing"
|
||||
}
|
||||
],
|
||||
"turn_id": "turn-file-edit",
|
||||
"turn_phase": "activity",
|
||||
"turn_seq": 2
|
||||
},
|
||||
{
|
||||
"event": "file_edit",
|
||||
"chat_id": "fixture-file-edit",
|
||||
"edits": [
|
||||
{
|
||||
"version": 1,
|
||||
"call_id": "call-edit",
|
||||
"tool": "edit_file",
|
||||
"path": "app.py",
|
||||
"phase": "end",
|
||||
"added": 3,
|
||||
"deleted": 1,
|
||||
"status": "done"
|
||||
}
|
||||
],
|
||||
"turn_id": "turn-file-edit",
|
||||
"turn_phase": "activity",
|
||||
"turn_seq": 3
|
||||
},
|
||||
{
|
||||
"event": "message",
|
||||
"chat_id": "fixture-file-edit",
|
||||
"text": "Updated app.py.",
|
||||
"turn_id": "turn-file-edit",
|
||||
"turn_phase": "answer",
|
||||
"turn_seq": 4
|
||||
},
|
||||
{
|
||||
"event": "turn_end",
|
||||
"chat_id": "fixture-file-edit",
|
||||
"latency_ms": 9,
|
||||
"turn_id": "turn-file-edit",
|
||||
"turn_phase": "complete",
|
||||
"turn_seq": 5
|
||||
}
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Update app.py.",
|
||||
"turnId": "turn-file-edit",
|
||||
"turnPhase": "user",
|
||||
"turnSeq": 1
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "",
|
||||
"kind": "trace",
|
||||
"traces": [],
|
||||
"fileEdits": [
|
||||
{
|
||||
"version": 1,
|
||||
"call_id": "call-edit",
|
||||
"tool": "edit_file",
|
||||
"path": "app.py",
|
||||
"phase": "end",
|
||||
"added": 3,
|
||||
"deleted": 1,
|
||||
"status": "done"
|
||||
}
|
||||
],
|
||||
"activitySegmentId": "segment-1",
|
||||
"turnId": "turn-file-edit",
|
||||
"turnPhase": "activity",
|
||||
"turnSeq": 3
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Updated app.py.",
|
||||
"latencyMs": 9,
|
||||
"turnId": "turn-file-edit",
|
||||
"turnPhase": "answer",
|
||||
"turnSeq": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4,10 +4,67 @@ import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { useNanobotStream } from "@/hooks/useNanobotStream";
|
||||
import type { StreamError } from "@/lib/nanobot-client";
|
||||
import type { ConnectionStatus, InboundEvent, GoalStateWsPayload } from "@/lib/types";
|
||||
import type {
|
||||
ConnectionStatus,
|
||||
GoalStateWsPayload,
|
||||
InboundEvent,
|
||||
UIMessage,
|
||||
} from "@/lib/types";
|
||||
import { ClientProvider } from "@/providers/ClientProvider";
|
||||
import projectionFixture from "./fixtures/live-replay-event-projection.json";
|
||||
|
||||
const EMPTY_MESSAGES: import("@/lib/types").UIMessage[] = [];
|
||||
const EMPTY_MESSAGES: UIMessage[] = [];
|
||||
|
||||
interface ProjectionFixtureCase {
|
||||
name: string;
|
||||
chat_id: string;
|
||||
initial_messages: UIMessage[];
|
||||
live_events: InboundEvent[];
|
||||
expected: Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
const PROJECTION_FIXTURE_CASES = (
|
||||
projectionFixture as unknown as { cases: ProjectionFixtureCase[] }
|
||||
).cases;
|
||||
const SEMANTIC_MESSAGE_FIELDS = [
|
||||
"role",
|
||||
"content",
|
||||
"kind",
|
||||
"traces",
|
||||
"toolEvents",
|
||||
"fileEdits",
|
||||
"images",
|
||||
"media",
|
||||
"cliApps",
|
||||
"mcpPresets",
|
||||
"sessionMentions",
|
||||
"reasoning",
|
||||
"latencyMs",
|
||||
"source",
|
||||
"turnId",
|
||||
"turnPhase",
|
||||
"turnSeq",
|
||||
] as const satisfies ReadonlyArray<keyof UIMessage>;
|
||||
|
||||
function normalizeProjection(messages: UIMessage[]): Array<Record<string, unknown>> {
|
||||
const segmentAliases = new Map<string, string>();
|
||||
return messages.map((message) => {
|
||||
const row: Record<string, unknown> = {};
|
||||
for (const field of SEMANTIC_MESSAGE_FIELDS) {
|
||||
const value = message[field];
|
||||
if (value !== undefined && value !== null) row[field] = value;
|
||||
}
|
||||
if (message.activitySegmentId) {
|
||||
let alias = segmentAliases.get(message.activitySegmentId);
|
||||
if (!alias) {
|
||||
alias = `segment-${segmentAliases.size + 1}`;
|
||||
segmentAliases.set(message.activitySegmentId, alias);
|
||||
}
|
||||
row.activitySegmentId = alias;
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
function fakeClient() {
|
||||
const handlers = new Map<string, Set<(ev: InboundEvent) => void>>();
|
||||
@@ -2841,3 +2898,21 @@ describe("useNanobotStream", () => {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("live/replay projection before canonical-event revision migration", () => {
|
||||
it.each(PROJECTION_FIXTURE_CASES)("matches the shared $name fixture", (fixtureCase) => {
|
||||
const fake = fakeClient();
|
||||
const { result } = renderHook(
|
||||
() => useNanobotStream(fixtureCase.chat_id, fixtureCase.initial_messages),
|
||||
{ wrapper: wrap(fake.client) },
|
||||
);
|
||||
|
||||
for (const event of fixtureCase.live_events) {
|
||||
act(() => {
|
||||
fake.emit(fixtureCase.chat_id, event);
|
||||
});
|
||||
}
|
||||
|
||||
expect(normalizeProjection(result.current.messages)).toEqual(fixtureCase.expected);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user