Files
multica/packages/views/common/task-transcript/build-timeline.test.ts
Bohan Jiang 72179d1145 refactor(transcript): reuse payload helper + cover coalesce timestamps (MUL-3174) (#3958)
* refactor(transcript): reuse taskMessageToPayload in WS broadcast

The ReportTaskMessages WebSocket broadcast hand-built the payload and
duplicated the created_at formatting that taskMessageToPayload already
does. Reuse the helper with the just-inserted row, which carries the
same redacted values and the DB-assigned timestamp.

Co-authored-by: multica-agent <github@multica.ai>

* test(transcript): cover coalesce created_at behavior

Lock in that coalescing streaming fragments carries the latest
created_at, and falls back to the previous timestamp when the merged
fragment has none.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-10 12:15:50 +08:00

105 lines
3.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { TaskMessagePayload } from "@multica/core/types/events";
import { appendTimelineItem, buildTimeline, coalesceTimelineItems, type TimelineItem } from "./build-timeline";
function message(seq: number, type: TaskMessagePayload["type"], content?: string): TaskMessagePayload {
return {
task_id: "task-1",
issue_id: "issue-1",
seq,
type,
content,
};
}
describe("task transcript timeline", () => {
it("merges adjacent text and thinking fragments split by streaming flushes", () => {
const items = buildTimeline([
message(2, "text", "world"),
message(1, "text", "hello "),
message(3, "thinking", "step "),
message(4, "thinking", "one"),
]);
expect(items).toEqual([
expect.objectContaining({ seq: 1, type: "text", content: "hello world" }),
expect.objectContaining({ seq: 3, type: "thinking", content: "step one" }),
]);
});
it("does not merge across tool or error boundaries", () => {
const items = coalesceTimelineItems([
{ seq: 1, type: "text", content: "before" },
{ seq: 2, type: "tool_use", tool: "bash" },
{ seq: 3, type: "text", content: "after" },
{ seq: 4, type: "error", content: "failed" },
{ seq: 5, type: "text", content: "done" },
]);
expect(items.map((item) => item.content ?? item.tool)).toEqual([
"before",
"bash",
"after",
"failed",
"done",
]);
});
it("coalesces newly appended live text with the previous text item", () => {
const existing: TimelineItem[] = [{ seq: 1, type: "text", content: "hello" }];
const items = appendTimelineItem(existing, { seq: 2, type: "text", content: " world" });
expect(items).toEqual([
expect.objectContaining({ seq: 1, type: "text", content: "hello world" }),
]);
});
it("coalesces out-of-order raw text by sequence", () => {
const existing: TimelineItem[] = [
{ seq: 1, type: "text", content: "A" },
{ seq: 3, type: "text", content: "C" },
];
const items = appendTimelineItem(existing, { seq: 2, type: "text", content: "B" });
expect(items).toEqual([
expect.objectContaining({ seq: 1, type: "text", content: "ABC" }),
]);
});
it("redacts secrets after adjacent chunks are coalesced", () => {
const items = buildTimeline([
message(1, "text", "Authorization: Bearer abc123xyz."),
message(2, "text", "def456"),
]);
expect(items[0]?.content).toBe("Authorization: Bearer [REDACTED]");
expect(items[0]?.content).not.toContain("abc123xyz");
expect(items[0]?.content).not.toContain("def456");
});
it("keeps the latest created_at when coalescing streaming fragments", () => {
const items = coalesceTimelineItems([
{ seq: 1, type: "text", content: "hello ", created_at: "2026-06-09T09:00:00.000Z" },
{ seq: 2, type: "text", content: "world", created_at: "2026-06-09T09:00:05.000Z" },
]);
expect(items).toEqual([
expect.objectContaining({
seq: 1,
type: "text",
content: "hello world",
created_at: "2026-06-09T09:00:05.000Z",
}),
]);
});
it("falls back to the previous created_at when the merged fragment has none", () => {
const items = coalesceTimelineItems([
{ seq: 1, type: "text", content: "hello ", created_at: "2026-06-09T09:00:00.000Z" },
{ seq: 2, type: "text", content: "world" },
]);
expect(items[0]?.created_at).toBe("2026-06-09T09:00:00.000Z");
});
});