mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 21:39:54 +02:00
* 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>
105 lines
3.5 KiB
TypeScript
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");
|
|
});
|
|
});
|