import assert from "node:assert/strict"; import test from "node:test"; import { buildTranscriptDisplayBlocks, flattenDisplayBlocks, formatTurnSetupLabel, getDisplayBlockKey, } from "./agentSessionTranscriptGrouping.ts"; import { isObserverEventAfter } from "../observerRelayStore.ts"; const baseTimestamp = "2026-06-14T22:20:23.000Z"; function lifecycle(id, title, acpSource, turnId, text = "") { return { id, type: "lifecycle", title, text, timestamp: baseTimestamp, acpSource, turnId, sessionId: "sess-1", channelId: "channel-1", }; } function userPrompt(id, text, turnId) { return { id, type: "message", role: "user", title: "Buzz event", text, timestamp: baseTimestamp, acpSource: "session/prompt:user", turnId, sessionId: "sess-1", channelId: "channel-1", }; } function promptContext(id, turnId) { return { id, type: "metadata", title: "Prompt context", sections: [{ title: "Channel", body: "general" }], timestamp: baseTimestamp, acpSource: "session/prompt:context", turnId, sessionId: "sess-1", channelId: "channel-1", }; } function assistantMessage(id, text, turnId) { return { id, type: "message", role: "assistant", title: "Assistant", text, timestamp: "2026-06-14T22:20:47.000Z", acpSource: "agent_message_chunk", turnId, sessionId: "sess-1", channelId: "channel-1", }; } function toolCall(id, turnId) { return { id, type: "tool", title: "Shell", toolName: "buzz-dev-mcp__shell", buzzToolName: null, status: "completed", args: {}, result: "ok", isError: false, timestamp: "2026-06-14T22:20:47.000Z", startedAt: "2026-06-14T22:20:47.000Z", completedAt: "2026-06-14T22:20:47.400Z", acpSource: "tool_call_update", turnId, sessionId: "sess-1", channelId: "channel-1", }; } test("buildTranscriptDisplayBlocks bundles user prompt, setup, and context together", () => { const rawItems = [ lifecycle( "turn", "Turn started", "turn_started", "turn-1", "Triggered by 1 event.", ), lifecycle("session", "Session ready", "session_resolved", "turn-1"), userPrompt("prompt", "@Ned deliberate, wider pass", "turn-1"), promptContext("context", "turn-1"), assistantMessage("assistant", "Thinking out loud.", "turn-1"), toolCall("tool", "turn-1"), ]; const blocks = buildTranscriptDisplayBlocks(rawItems); const displayOrder = flattenDisplayBlocks(blocks).map((item) => item.id); assert.deepEqual(displayOrder, [ "prompt", "turn", "session", "context", "assistant", "tool", ]); const turnBlock = blocks[0]; assert.equal(turnBlock?.kind, "turn"); assert.equal(turnBlock.segments[0]?.kind, "prompt"); const promptSegment = turnBlock.segments[0]; assert.equal(promptSegment.user.id, "prompt"); assert.equal(promptSegment.context?.id, "context"); assert.equal(promptSegment.setup.length, 2); assert.equal(turnBlock.segments[1]?.kind, "item"); assert.equal(turnBlock.segments[2]?.kind, "item"); }); test("buildTranscriptDisplayBlocks collapses setup lifecycle inside prompt bundle", () => { const rawItems = [ lifecycle("turn", "Turn started", "turn_started", "turn-1"), lifecycle("session", "Session ready", "session_resolved", "turn-1"), userPrompt("prompt", "hello", "turn-1"), ]; const blocks = buildTranscriptDisplayBlocks(rawItems); assert.equal(blocks.length, 1); assert.equal(blocks[0]?.kind, "turn"); const turnBlock = blocks[0]; assert.equal(turnBlock.segments.length, 1); assert.equal(turnBlock.segments[0]?.kind, "prompt"); assert.equal( formatTurnSetupLabel(turnBlock.segments[0].setup), "Turn started · Session ready", ); }); test("buildTranscriptDisplayBlocks hides setup and context when prompt is missing", () => { const rawItems = [ lifecycle("turn", "Turn started", "turn_started", "turn-1"), lifecycle("session", "Session ready", "session_resolved", "turn-1"), promptContext("context", "turn-1"), toolCall("tool", "turn-1"), ]; const blocks = buildTranscriptDisplayBlocks(rawItems); const displayOrder = flattenDisplayBlocks(blocks).map((item) => item.id); assert.deepEqual(displayOrder, ["tool"]); }); test("buildTranscriptDisplayBlocks drops setup-and-context-only turns", () => { const rawItems = [ lifecycle("turn", "Turn started", "turn_started", "turn-1"), lifecycle("session", "Session ready", "session_resolved", "turn-1"), promptContext("context", "turn-1"), ]; const blocks = buildTranscriptDisplayBlocks(rawItems); assert.deepEqual(blocks, []); }); test("buildTranscriptDisplayBlocks leaves error lifecycle prominent outside prompt bundle", () => { const rawItems = [ lifecycle("turn", "Turn started", "turn_started", "turn-1"), userPrompt("prompt", "hello", "turn-1"), lifecycle( "error", "Turn error", "turn_error", "turn-1", "timeout: agent hung", ), ]; const blocks = buildTranscriptDisplayBlocks(rawItems); const displayOrder = flattenDisplayBlocks(blocks).map((item) => item.id); assert.deepEqual(displayOrder, ["prompt", "turn", "error"]); assert.equal(blocks[0]?.segments[0]?.kind, "prompt"); assert.equal(blocks[0]?.segments[1]?.kind, "item"); assert.equal(blocks[0]?.segments[1]?.item.id, "error"); }); test("buildTranscriptDisplayBlocks passes through items without turnId", () => { const orphan = { id: "orphan", type: "lifecycle", title: "Wire parse error", text: "bad json", timestamp: baseTimestamp, acpSource: "acp_parse_error", channelId: "channel-1", }; const blocks = buildTranscriptDisplayBlocks([orphan]); assert.equal(blocks.length, 1); assert.equal(blocks[0]?.kind, "single"); assert.equal(blocks[0]?.item.id, "orphan"); }); test("buildTranscriptDisplayBlocks groups same-kind tool runs within a turn", () => { const items = [1, 2, 3].map((index) => ({ id: `tool:${index}`, type: "tool", renderClass: "file-read", descriptor: { renderClass: "file-read", label: "Read file", preview: `file-${index}.ts`, groupKey: "read_file", }, title: "read_file", toolName: "read_file", buzzToolName: null, status: "completed", args: { path: `file-${index}.ts` }, result: "", isError: false, timestamp: "2026-06-18T00:00:00Z", startedAt: "2026-06-18T00:00:00Z", completedAt: "2026-06-18T00:00:01Z", turnId: "turn-1", sessionId: "sess-1", channelId: "chan-1", })); const [block] = buildTranscriptDisplayBlocks(items); assert.equal(block.kind, "turn"); assert.equal(block.segments.length, 1); assert.equal(block.segments[0].kind, "summary"); assert.equal(block.segments[0].summary.label, "Read 3 files"); }); test("buildTranscriptDisplayBlocks groups consecutive file edit tool runs", () => { const items = [1, 2].map((index) => ({ id: `edit:${index}`, type: "tool", renderClass: "file-edit", descriptor: { renderClass: "file-edit", label: "Edited file", preview: `src/file-${index}.ts`, groupKey: "file-edit:str_replace", }, title: "str_replace", toolName: "str_replace", buzzToolName: null, status: "completed", args: { path: `src/file-${index}.ts` }, result: "", isError: false, timestamp: "2026-06-18T00:00:00Z", startedAt: "2026-06-18T00:00:00Z", completedAt: "2026-06-18T00:00:01Z", turnId: "turn-1", sessionId: "sess-1", channelId: "chan-1", })); const [block] = buildTranscriptDisplayBlocks(items); assert.equal(block.kind, "turn"); assert.equal(block.segments.length, 1); assert.equal(block.segments[0].kind, "summary"); assert.equal(block.segments[0].summary.label, "Edited 2 files"); assert.equal(block.segments[0].summary.renderClass, "file-edit"); assert.deepEqual( block.segments[0].summary.items.map((item) => item.id), ["edit:1", "edit:2"], ); }); test("buildTranscriptDisplayBlocks groups mixed consecutive eligible tool runs", () => { const [block] = buildTranscriptDisplayBlocks([ mkTool("read-1", "Read file", "file-read", "read_file"), mkTool("shell-1", "Ran command", "shell", "shell:command"), mkTool("read-2", "Read file", "file-read", "read_file"), mkTool("read-3", "Read file", "file-read", "read_file"), ]); assert.equal(block.kind, "turn"); assert.equal(block.segments.length, 1); assert.equal(block.segments[0].kind, "summary"); assert.equal(block.segments[0].summary.variant, "mixed"); assert.equal(block.segments[0].summary.label, "Ran 4 tool calls"); assert.deepEqual( block.segments[0].summary.items.map((item) => item.id), ["read-1", "shell-1", "read-2", "read-3"], ); }); test("buildTranscriptDisplayBlocks groups tool bursts at threshold 2", () => { const [block] = buildTranscriptDisplayBlocks([ mkTool("read-1", "Read file", "file-read", "read_file"), mkTool("shell-1", "Ran command", "shell", "shell:command"), ]); assert.equal(block.kind, "turn"); assert.equal(block.segments.length, 1); assert.equal(block.segments[0].kind, "summary"); assert.equal(block.segments[0].summary.variant, "mixed"); assert.equal(block.segments[0].summary.label, "Ran 2 tool calls"); }); test("buildTranscriptDisplayBlocks keeps a lone eligible tool row expanded", () => { const [block] = buildTranscriptDisplayBlocks([ mkTool("read-1", "Read file", "file-read", "read_file"), ]); assert.equal(block.kind, "turn"); assert.deepEqual( block.segments.map((segment) => segment.kind), ["item"], ); }); test("buildTranscriptDisplayBlocks nests same-kind summaries inside tool bursts", () => { const [block] = buildTranscriptDisplayBlocks([ mkTool("read-1", "Read file", "file-read", "read_file"), mkTool("read-2", "Read file", "file-read", "read_file"), mkTool("read-3", "Read file", "file-read", "read_file"), mkTool("shell-1", "Ran command", "shell", "shell:command"), mkTool("skill-1", "Read skill", "skill-read", "skill:load"), ]); assert.equal(block.kind, "turn"); assert.equal(block.segments.length, 1); assert.equal(block.segments[0].kind, "summary"); assert.equal(block.segments[0].summary.variant, "mixed"); assert.equal(block.segments[0].summary.label, "Ran 5 tool calls"); // Mixed summaries are the only visible burst summary; nested same-kind // summaries flatten back to leaf rows to avoid redundant rows such as // "Ran 16 tool calls" → "Ran 12 commands". assert.deepEqual( block.segments[0].summary.segments.map((child) => child.kind === "item" ? child.item.id : child.summary.label, ), ["read-1", "read-2", "read-3", "shell-1", "skill-1"], ); // Flat leaf items preserve original order. assert.deepEqual( block.segments[0].summary.items.map((item) => item.id), ["read-1", "read-2", "read-3", "shell-1", "skill-1"], ); }); test("buildTranscriptDisplayBlocks collapses alternating search/read bursts into one summary", () => { const [block] = buildTranscriptDisplayBlocks([ mkTool("shell-1", "Ran command", "shell", "shell:command"), mkTool("read-1", "Read file", "file-read", "read_file"), mkTool("read-2", "Read file", "file-read", "read_file"), mkTool("read-3", "Read file", "file-read", "read_file"), mkTool("shell-2", "Ran command", "shell", "shell:command"), mkTool("read-4", "Read file", "file-read", "read_file"), mkTool("read-5", "Read file", "file-read", "read_file"), mkTool("read-6", "Read file", "file-read", "read_file"), ]); assert.equal(block.kind, "turn"); assert.equal(block.segments.length, 1); assert.equal(block.segments[0].kind, "summary"); assert.equal(block.segments[0].summary.variant, "mixed"); assert.equal(block.segments[0].summary.label, "Ran 8 tool calls"); assert.deepEqual( block.segments[0].summary.segments.map((child) => child.kind === "summary" ? child.summary.label : child.item.id, ), [ "shell-1", "read-1", "read-2", "read-3", "shell-2", "read-4", "read-5", "read-6", ], ); }); test("buildTranscriptDisplayBlocks keeps messages out of mixed tool runs", () => { const [block] = buildTranscriptDisplayBlocks([ mkTool("read-1", "Read file", "file-read", "read_file"), mkTool("shell-1", "Ran command", "shell", "shell:command"), assistantMessage("assistant", "Here is what I found.", "turn-1"), mkTool("read-2", "Read file", "file-read", "read_file"), mkTool("shell-2", "Ran command", "shell", "shell:command"), ]); assert.equal(block.kind, "turn"); assert.deepEqual( block.segments.map((segment) => segment.kind), ["summary", "item", "summary"], ); assert.equal(block.segments[1].item.id, "assistant"); assert.deepEqual( block.segments[0].summary.items.map((item) => item.id), ["read-1", "shell-1"], ); assert.deepEqual( block.segments[2].summary.items.map((item) => item.id), ["read-2", "shell-2"], ); }); test("buildTranscriptDisplayBlocks breaks failed tools out of mixed tool runs", () => { const failed = { ...mkTool("shell-fail", "Ran command failed", "error", "shell:command"), isError: true, }; const [block] = buildTranscriptDisplayBlocks([ mkTool("read-1", "Read file", "file-read", "read_file"), mkTool("shell-1", "Ran command", "shell", "shell:command"), mkTool("skill-1", "Read skill", "skill-read", "skill:load"), failed, mkTool("read-2", "Read file", "file-read", "read_file"), mkTool("shell-2", "Ran command", "shell", "shell:command"), mkTool("image-1", "Viewed image", "image", "view_image"), ]); assert.equal(block.kind, "turn"); assert.deepEqual( block.segments.map((segment) => segment.kind), ["summary", "item", "summary"], ); assert.equal(block.segments[0].summary.variant, "mixed"); assert.equal(block.segments[0].summary.label, "Ran 3 tool calls"); assert.equal(block.segments[1].item.id, "shell-fail"); assert.equal(block.segments[2].summary.variant, "mixed"); assert.deepEqual( block.segments[2].summary.items.map((item) => item.id), ["read-2", "shell-2", "image-1"], ); }); test("flattenDisplayBlocks preserves child order through mixed summaries", () => { const blocks = buildTranscriptDisplayBlocks([ mkTool("read-1", "Read file", "file-read", "read_file"), mkTool("shell-1", "Ran command", "shell", "shell:command"), mkTool("edit-1", "Edited file", "file-edit", "file-edit:str_replace"), ]); assert.deepEqual( flattenDisplayBlocks(blocks).map((item) => item.id), ["read-1", "shell-1", "edit-1"], ); }); test("buildTranscriptDisplayBlocks never same-kind groups failed tools", () => { const mkFailed = (id) => ({ ...mkTool(id, "Ran command failed", "error", "shell:command"), isError: true, }); const [block] = buildTranscriptDisplayBlocks([ mkFailed("fail-1"), mkFailed("fail-2"), mkFailed("fail-3"), ]); assert.equal(block.kind, "turn"); assert.deepEqual( block.segments.map((segment) => segment.kind), ["item", "item", "item"], ); }); test("buildTranscriptDisplayBlocks never same-kind groups status tool rows", () => { const [block] = buildTranscriptDisplayBlocks([ mkTool("status-1", "Context compacted", "status", "status:post-compact"), mkTool("status-2", "Context compacted", "status", "status:post-compact"), mkTool("status-3", "Context compacted", "status", "status:post-compact"), ]); assert.equal(block.kind, "turn"); assert.deepEqual( block.segments.map((segment) => segment.kind), ["item", "item", "item"], ); }); test("buildTranscriptDisplayBlocks never same-kind groups suppressed tool rows", () => { const [block] = buildTranscriptDisplayBlocks([ mkTool("stop-1", "Checked todos", "suppressed", "suppressed:stop-hook"), mkTool("stop-2", "Checked todos", "suppressed", "suppressed:stop-hook"), mkTool("stop-3", "Checked todos", "suppressed", "suppressed:stop-hook"), ]); assert.equal(block.kind, "turn"); assert.deepEqual( block.segments.map((segment) => segment.kind), ["item", "item", "item"], ); }); test("buildTranscriptDisplayBlocks breaks same-kind runs on an ineligible row", () => { const failed = { ...mkTool("fail-1", "Read file failed", "error", "read_file"), isError: true, }; const [block] = buildTranscriptDisplayBlocks([ mkTool("read-1", "Read file", "file-read", "read_file"), mkTool("read-2", "Read file", "file-read", "read_file"), failed, mkTool("read-3", "Read file", "file-read", "read_file"), mkTool("read-4", "Read file", "file-read", "read_file"), ]); assert.equal(block.kind, "turn"); assert.deepEqual( block.segments.map((segment) => segment.kind), ["summary", "item", "summary"], ); assert.equal(block.segments[1].item.id, "fail-1"); assert.deepEqual( block.segments[0].summary.items.map((item) => item.id), ["read-1", "read-2"], ); assert.deepEqual( block.segments[2].summary.items.map((item) => item.id), ["read-3", "read-4"], ); }); test("buildTranscriptDisplayBlocks bundles steer message with steer context behind the prompt segment", () => { const steerMessage = { id: "steer:chan-1:turn-1", type: "message", role: "user", title: "Buzz event", text: "@Bart new steer instruction", timestamp: baseTimestamp, acpSource: "session/steer:user", turnId: "turn-1", sessionId: "sess-1", channelId: "chan-1", }; const steerContext = { id: "steer-context:chan-1:turn-1", type: "metadata", title: "Prompt context", sections: [{ title: "Thread history", body: "prior messages" }], timestamp: baseTimestamp, acpSource: "session/steer:context", turnId: "turn-1", sessionId: "sess-1", channelId: "chan-1", }; const [block] = buildTranscriptDisplayBlocks([ assistantMessage("assistant", "Working on it.", "turn-1"), steerMessage, steerContext, toolCall("tool", "turn-1"), ]); assert.equal(block.kind, "turn"); assert.deepEqual( block.segments.map((segment) => segment.kind), ["item", "prompt", "item"], ); const steerSegment = block.segments[1]; assert.equal(steerSegment.user.id, "steer:chan-1:turn-1"); assert.equal(steerSegment.context?.id, "steer-context:chan-1:turn-1"); assert.deepEqual(steerSegment.setup, []); // No standalone "Prompt context" metadata row leaks into the feed. assert.ok( !block.segments.some( (segment) => segment.kind === "item" && segment.item.type === "metadata", ), ); }); test("buildTranscriptDisplayBlocks keeps orphan steer context visible when no steer message exists", () => { const steerContext = { id: "steer-context:chan-1:turn-1", type: "metadata", title: "Prompt context", sections: [{ title: "Thread history", body: "prior messages" }], timestamp: baseTimestamp, acpSource: "session/steer:context", turnId: "turn-1", sessionId: "sess-1", channelId: "chan-1", }; const [block] = buildTranscriptDisplayBlocks([ steerContext, toolCall("tool", "turn-1"), ]); assert.equal(block.kind, "turn"); const flattened = flattenDisplayBlocks([block]).map((item) => item.id); assert.ok(flattened.includes("steer-context:chan-1:turn-1")); }); function mkTool(id, label, renderClass = "generic", groupKey = label) { return { id, type: "tool", renderClass, descriptor: { renderClass, label, preview: id, source: "harness", groupKey, }, title: label, toolName: label, buzzToolName: null, status: "completed", args: {}, result: "", isError: false, timestamp: "2026-06-18T00:00:00Z", startedAt: "2026-06-18T00:00:00Z", completedAt: "2026-06-18T00:00:01Z", turnId: "turn-1", sessionId: "sess-1", channelId: "chan-1", }; } // ── Session-run splitting and session-boundary blocks ────────────────────────── /** * Build a minimal tool-call item stamped with a specific session. */ function sessionItem(id, sessionId, ts = "2026-07-08T00:00:00.000Z") { return { id, type: "tool", renderClass: "generic", descriptor: { renderClass: "generic", label: id, preview: id, source: "harness", groupKey: id, }, title: id, toolName: id, buzzToolName: null, status: "completed", args: {}, result: "", isError: false, timestamp: ts, startedAt: ts, completedAt: ts, turnId: `turn-${id}`, sessionId, channelId: "chan-1", }; } // ── Single session — no boundary injected ────────────────────────────────────── test("buildTranscriptDisplayBlocks_singleSession_noBoundaryBlock", () => { const items = [sessionItem("a", "sess-1"), sessionItem("b", "sess-1")]; const blocks = buildTranscriptDisplayBlocks(items); const boundaryBlocks = blocks.filter((b) => b.kind === "session-boundary"); assert.equal( boundaryBlocks.length, 0, "no session-boundary blocks for a single session", ); }); // ── Two sessions — one boundary between them ─────────────────────────────────── test("buildTranscriptDisplayBlocks_twoSessions_oneBoundaryBetween", () => { // items ordered oldest-first: sess-1 then sess-2 const items = [ sessionItem("a", "sess-1", "2026-07-08T00:00:01.000Z"), sessionItem("b", "sess-2", "2026-07-08T00:00:02.000Z"), ]; const blocks = buildTranscriptDisplayBlocks(items); const boundaryBlocks = blocks.filter((b) => b.kind === "session-boundary"); assert.equal( boundaryBlocks.length, 1, "exactly one boundary for two sessions", ); // The boundary is inserted BEFORE the newer run (sess-2). const boundaryIndex = blocks.indexOf(boundaryBlocks[0]); const prevBlock = blocks[boundaryIndex - 1]; const nextBlock = blocks[boundaryIndex + 1]; // Previous content block belongs to sess-1 items, next to sess-2. const flatPrev = flattenDisplayBlocks([prevBlock]).map((i) => i.id); const flatNext = flattenDisplayBlocks([nextBlock]).map((i) => i.id); assert.ok(flatPrev.includes("a"), "content before boundary is sess-1"); assert.ok(flatNext.includes("b"), "content after boundary is sess-2"); }); // ── Newest session labeled correctly relative to latestLiveSessionId ────────── test("buildTranscriptDisplayBlocks_newestMatchesLive_labelStateCurrent", () => { const items = [ sessionItem("old", "sess-1", "2026-07-08T00:00:01.000Z"), sessionItem("new", "sess-2", "2026-07-08T00:00:02.000Z"), ]; const blocks = buildTranscriptDisplayBlocks(items, "sess-2"); const boundary = blocks.find((b) => b.kind === "session-boundary"); assert.ok(boundary, "boundary present"); assert.equal( boundary.labelState, "current", "labelState=current when newest session matches live id", ); assert.equal(boundary.sessionId, "sess-2", "boundary sessionId is sess-2"); }); test("buildTranscriptDisplayBlocks_newestNoLive_labelStateMostRecent", () => { const items = [ sessionItem("old", "sess-1", "2026-07-08T00:00:01.000Z"), sessionItem("new", "sess-2", "2026-07-08T00:00:02.000Z"), ]; // latestLiveSessionId is null → newest session is "most-recent" const blocksNoLive = buildTranscriptDisplayBlocks(items, null); const boundaryNoLive = blocksNoLive.find( (b) => b.kind === "session-boundary", ); assert.equal( boundaryNoLive.labelState, "most-recent", "labelState=most-recent when no live id (archived-only view)", ); // latestLiveSessionId is a DIFFERENT session → newest is still "most-recent" const blocksDiffLive = buildTranscriptDisplayBlocks(items, "sess-other"); const boundaryDiff = blocksDiffLive.find( (b) => b.kind === "session-boundary", ); assert.equal( boundaryDiff.labelState, "most-recent", "labelState=most-recent when live id differs from newest visible", ); }); // ── Three sessions — two boundaries ──────────────────────────────────────────── test("buildTranscriptDisplayBlocks_threeSessions_twoBoundaries", () => { const items = [ sessionItem("a", "sess-1", "2026-07-08T00:00:01.000Z"), sessionItem("b", "sess-2", "2026-07-08T00:00:02.000Z"), sessionItem("c", "sess-3", "2026-07-08T00:00:03.000Z"), ]; const blocks = buildTranscriptDisplayBlocks(items); const boundaryBlocks = blocks.filter((b) => b.kind === "session-boundary"); assert.equal(boundaryBlocks.length, 2, "two boundaries for three sessions"); // With no live id: newest (sess-3) boundary = "most-recent"; older = "earlier". const newestBoundary = boundaryBlocks[boundaryBlocks.length - 1]; const olderBoundary = boundaryBlocks[0]; assert.equal( newestBoundary.labelState, "most-recent", "newest boundary is most-recent when latestLiveSessionId is null", ); assert.equal( olderBoundary.labelState, "earlier", "older boundary is earlier when latestLiveSessionId is null", ); }); // ── Null sessionId items stay in the current run ─────────────────────────────── test("buildTranscriptDisplayBlocks_nullSessionId_staysInCurrentRun", () => { // An item with null sessionId should not start a new run. const items = [ sessionItem("a", "sess-1", "2026-07-08T00:00:01.000Z"), // null sessionId — stays in sess-1 run { ...sessionItem("b", null, "2026-07-08T00:00:02.000Z"), sessionId: null }, sessionItem("c", "sess-1", "2026-07-08T00:00:03.000Z"), ]; const blocks = buildTranscriptDisplayBlocks(items); const boundaryBlocks = blocks.filter((b) => b.kind === "session-boundary"); assert.equal( boundaryBlocks.length, 0, "no boundary when null-sessionId items are present within a single session", ); }); // ── flattenDisplayBlocks skips session-boundary blocks ──────────────────────── test("flattenDisplayBlocks_skipsSessionBoundaryBlocks", () => { const items = [ sessionItem("a", "sess-1", "2026-07-08T00:00:01.000Z"), sessionItem("b", "sess-2", "2026-07-08T00:00:02.000Z"), ]; const blocks = buildTranscriptDisplayBlocks(items); assert.ok( blocks.some((b) => b.kind === "session-boundary"), "test setup: boundary must be present", ); const flat = flattenDisplayBlocks(blocks); const ids = flat.map((i) => i.id); assert.ok(ids.includes("a"), "item a is in flattened output"); assert.ok(ids.includes("b"), "item b is in flattened output"); assert.equal( flat.filter((i) => i.kind === "session-boundary").length, 0, "session-boundary items are excluded from flatten", ); }); // ── isObserverEventAfter — latest-live ordering ────────────────────────────── test("isObserverEventAfter returns true when candidate has later timestamp", () => { const stored = { timestamp: "2026-07-08T00:00:01.000Z", seq: 5 }; const candidate = { timestamp: "2026-07-08T00:00:02.000Z", seq: 1 }; assert.ok(isObserverEventAfter(candidate, stored)); }); test("isObserverEventAfter returns false when candidate has earlier timestamp", () => { const stored = { timestamp: "2026-07-08T00:00:02.000Z", seq: 5 }; const candidate = { timestamp: "2026-07-08T00:00:01.000Z", seq: 10 }; assert.ok(!isObserverEventAfter(candidate, stored)); }); test("isObserverEventAfter returns true for same timestamp, higher seq — session B advances over session A", () => { // This is the tiebreak case: timestamp equal, seq tiebreak must mirror // compareObserverEvents so latest-live never drifts from transcript order. const stored = { timestamp: "2026-07-08T00:00:01.000Z", seq: 3 }; const candidate = { timestamp: "2026-07-08T00:00:01.000Z", seq: 7 }; assert.ok(isObserverEventAfter(candidate, stored)); }); test("isObserverEventAfter returns false for same timestamp, same seq", () => { const stored = { timestamp: "2026-07-08T00:00:01.000Z", seq: 3 }; const candidate = { timestamp: "2026-07-08T00:00:01.000Z", seq: 3 }; assert.ok(!isObserverEventAfter(candidate, stored)); }); test("isObserverEventAfter returns false for same timestamp, lower seq", () => { const stored = { timestamp: "2026-07-08T00:00:01.000Z", seq: 7 }; const candidate = { timestamp: "2026-07-08T00:00:01.000Z", seq: 3 }; assert.ok(!isObserverEventAfter(candidate, stored)); }); // ── Pre-resolution null-session deferral (regression for first-turn bundle) ─── /** * Builds the exact wire sequence the harness emits on the first turn: * turn_started(null) → session/new(null) → session_resolved(sess) → session/prompt(sess) * * After processTranscriptEvent the TranscriptItems produced are: * - lifecycle turn_started: sessionId=null, turnId="turn-001" * - metadata session/new (system prompt): sessionId=null, turnId=null * - lifecycle session_resolved: sessionId="session-001", turnId="turn-001" * - message session/prompt:user: sessionId="session-001", turnId="turn-001" */ function firstTurnSequence() { const ts = "2026-07-08T10:00:00.000Z"; return [ // turn_started: sessionId null, turnId present { id: "turn-started", type: "lifecycle", renderClass: "lifecycle", title: "Turn started", text: "", timestamp: ts, acpSource: "turn_started", turnId: "turn-001", sessionId: null, channelId: "chan-1", }, // session/new system prompt: sessionId null, turnId null (processTranscriptEvent forces turnId null) { id: "system-prompt:chan-1", type: "metadata", renderClass: "raw-rail", title: "System prompt", sections: [ { title: "Base", body: "You are a helpful AI assistant." }, { title: "System", body: "You are Observer Agent." }, ], timestamp: ts, acpSource: "session/new", turnId: null, sessionId: null, channelId: "chan-1", }, // session_resolved: first item with a non-null sessionId { id: "session-resolved", type: "lifecycle", renderClass: "lifecycle", title: "Session ready", text: "", timestamp: ts, acpSource: "session_resolved", turnId: "turn-001", sessionId: "session-001", channelId: "chan-1", }, // session/prompt:user — the user message bubble { id: "user-prompt", type: "message", role: "user", title: "Buzz event", text: "@Observer Agent help me debug this", timestamp: ts, acpSource: "session/prompt:user", turnId: "turn-001", sessionId: "session-001", channelId: "chan-1", }, ]; } test("buildTranscriptDisplayBlocks_firstTurnSequence_standaloneSystemPrompt", () => { // session/new is session-scoped, not turn-scoped. After the consolidation, // the grouper emits it as a standalone "System prompt" single block that // appears BEFORE the first user-prompt turn — never inside the prompt bundle. const blocks = buildTranscriptDisplayBlocks(firstTurnSequence()); // (a) Exactly one standalone "System prompt" single block. const systemPromptSingles = blocks.filter( (b) => b.kind === "single" && b.item.acpSource === "session/new", ); assert.equal( systemPromptSingles.length, 1, "system prompt must appear as exactly one standalone single block", ); // (b) No session-boundary blocks — this is a single-session transcript. const boundaryBlocks = blocks.filter((b) => b.kind === "session-boundary"); assert.equal( boundaryBlocks.length, 0, "must be zero session-boundary blocks for a first-turn single-session sequence", ); // (c) System prompt is NOT inside any turn group (not in prompt bundle). const turnBlocks = blocks.filter((b) => b.kind === "turn"); assert.ok(turnBlocks.length > 0, "at least one turn block must exist"); const allTurnItems = flattenDisplayBlocks(turnBlocks); const systemPromptInTurn = allTurnItems.some( (item) => item.acpSource === "session/new", ); assert.ok( !systemPromptInTurn, "system prompt item must NOT be present inside a turn block", ); // (d) Standalone system-prompt block appears BEFORE the first turn block. const systemPromptIdx = blocks.indexOf(systemPromptSingles[0]); const firstTurnIdx = blocks.findIndex((b) => b.kind === "turn"); assert.ok( firstTurnIdx !== -1, "there must be at least one turn block after the system prompt", ); assert.ok( systemPromptIdx < firstTurnIdx, `system-prompt single (${systemPromptIdx}) must precede first turn block (${firstTurnIdx})`, ); }); test("buildTranscriptDisplayBlocks_genuineSecondSession_boundaryPreserved", () => { // Regression guard: the deferral fix must not over-collapse two genuinely // distinct sessions. A second session_resolved with a different sessionId // still gets its own run and a session-boundary block. const ts2 = "2026-07-08T11:00:00.000Z"; const items = [ // First session (may have pre-resolution preamble) ...firstTurnSequence(), // Second session — starts fresh with its own turn_started (no pre-null preamble here) { id: "turn-started-2", type: "lifecycle", renderClass: "lifecycle", title: "Turn started", text: "", timestamp: ts2, acpSource: "turn_started", turnId: "turn-002", sessionId: "session-002", channelId: "chan-1", }, { id: "user-prompt-2", type: "message", role: "user", title: "Buzz event", text: "second session message", timestamp: ts2, acpSource: "session/prompt:user", turnId: "turn-002", sessionId: "session-002", channelId: "chan-1", }, ]; const blocks = buildTranscriptDisplayBlocks(items); const boundaryBlocks = blocks.filter((b) => b.kind === "session-boundary"); assert.equal( boundaryBlocks.length, 1, "exactly one session-boundary block between two distinct sessions", ); assert.equal( boundaryBlocks[0].sessionId, "session-002", "boundary is labeled with the newer session id", ); }); // ── Duplicate-key guard: non-contiguous runs of the same sessionId ──────────── test("buildTranscriptDisplayBlocks_nonContiguousRunsSameSession_distinctBoundaryKeys", () => { // Scenario: sess-B frames, then sess-A frames, then sess-C frames, then // sess-A frames re-resolve (e.g. same agent session re-observed in a new // live subscription). splitIntoSessionRuns treats each session-id change as // a new run, so sess-A appears twice — once at runIndex=1 and once at // runIndex=3 — yielding two session-boundary blocks both with // sessionId="sess-A". // // The React key for a session-boundary is // `session-boundary:${sessionId}:${runIndex}` // Without the runIndex tiebreaker the two sess-A boundaries share the same // key, causing React to silently duplicate or omit children. This test // asserts the runIndex tiebreaker keeps the keys distinct. const items = [ sessionItem("b-item", "sess-B", "2026-07-08T00:00:01.000Z"), sessionItem("a-item", "sess-A", "2026-07-08T00:00:02.000Z"), sessionItem("c-item", "sess-C", "2026-07-08T00:00:03.000Z"), sessionItem("a-item-2", "sess-A", "2026-07-08T00:00:04.000Z"), ]; const blocks = buildTranscriptDisplayBlocks(items, "sess-A"); const boundaryBlocks = blocks.filter((b) => b.kind === "session-boundary"); // Four distinct session runs → three boundaries (before sess-A, before // sess-C, and before the re-occurring sess-A). assert.equal( boundaryBlocks.length, 3, "three boundaries for four runs (sess-B, sess-A, sess-C, sess-A)", ); // Two boundaries share sessionId="sess-A" (the collision case pre-fix). const sessABoundaries = boundaryBlocks.filter( (b) => b.sessionId === "sess-A", ); assert.equal( sessABoundaries.length, 2, "two boundary blocks both carry sessionId=sess-A (the collision the fix guards)", ); // The runIndex values must be distinct across ALL boundary blocks so that // `session-boundary:${sessionId}:${runIndex}` keys are unique. const runIndexSet = new Set(boundaryBlocks.map((b) => b.runIndex)); assert.equal( runIndexSet.size, boundaryBlocks.length, "every session-boundary block has a distinct runIndex", ); // The React keys derived from each boundary must also be distinct. const keys = boundaryBlocks.map( (b) => `session-boundary:${b.sessionId}:${b.runIndex}`, ); const keySet = new Set(keys); assert.equal( keySet.size, keys.length, "all React keys derived from session-boundary blocks are unique", ); }); // ── session/new run-anchor: restart scenario ────────────────────────────────── /** * Build a system-prompt metadata item as the normalizer produces it on restart: * stale-stamped with the previous session's id (latestSessionId at emit time). */ function systemPromptItem(id, staleSessionId, ts = "2026-07-08T12:00:00.000Z") { return { id, type: "metadata", renderClass: "raw-rail", title: "System prompt", sections: [{ title: "Base", body: "You are a helpful assistant." }], timestamp: ts, acpSource: "session/new", turnId: null, sessionId: staleSessionId, channelId: "chan-1", }; } test("buildTranscriptDisplayBlocks_restartScenario_systemPromptAfterBoundary", () => { // Restart wire sequence: toolA(sess-1) → session/new(sess-1 stale) → toolB(sess-2) // The session/new item is stale-stamped with sess-1 (the OLD session id). // After the fix it must sort into the sess-2 run — AFTER the session-boundary // block, not before it. const ts1 = "2026-07-08T12:00:00.000Z"; const ts2 = "2026-07-08T12:01:00.000Z"; const items = [ // Prior session activity { ...sessionItem("toolA", "sess-1", ts1), turnId: "turn-1" }, // session/new stale-stamped with OLD session id (the bug scenario) systemPromptItem("system-prompt", "sess-1", ts2), // New session activity { ...sessionItem("toolB", "sess-2", ts2), turnId: "turn-2" }, ]; const blocks = buildTranscriptDisplayBlocks(items, "sess-2"); // (a) Exactly one session-boundary block must exist. const boundaryBlocks = blocks.filter((b) => b.kind === "session-boundary"); assert.equal( boundaryBlocks.length, 1, "exactly one session-boundary block for two sessions", ); // (b) The session/new item must appear AFTER the boundary, not before it. const boundaryIndex = blocks.indexOf(boundaryBlocks[0]); const systemPromptBlockIndex = blocks.findIndex( (b) => b.kind === "single" && b.item.acpSource === "session/new", ); assert.ok( systemPromptBlockIndex !== -1, "system-prompt item must be present in the output", ); assert.ok( boundaryIndex < systemPromptBlockIndex, `boundary (index ${boundaryIndex}) must come before system-prompt (index ${systemPromptBlockIndex})`, ); // (c) toolA must appear before the boundary; toolB after. const flatAll = flattenDisplayBlocks(blocks); const ids = flatAll.map((i) => i.id); assert.ok(ids.includes("toolA"), "toolA present in flattened output"); assert.ok(ids.includes("toolB"), "toolB present in flattened output"); const toolAIdx = blocks.findIndex((b) => flattenDisplayBlocks([b]).some((i) => i.id === "toolA"), ); const toolBIdx = blocks.findIndex((b) => flattenDisplayBlocks([b]).some((i) => i.id === "toolB"), ); assert.ok( toolAIdx < boundaryIndex, "toolA block must be before the boundary", ); assert.ok(toolBIdx > boundaryIndex, "toolB block must be after the boundary"); }); test("buildTranscriptDisplayBlocks_firstEverSession_systemPromptInSingleRun", () => { // First-ever session: session/new arrives with sessionId null before any // session resolves — the preSessionBuffer path handles it, no boundary emitted. // This test guards against regressing the first-session behavior. const ts = "2026-07-08T10:00:00.000Z"; const items = [ // session/new with null sessionId (first ever, no stale-stamp) { id: "system-prompt", type: "metadata", renderClass: "raw-rail", title: "System prompt", sections: [{ title: "Base", body: "You are a helpful assistant." }], timestamp: ts, acpSource: "session/new", turnId: null, sessionId: null, channelId: "chan-1", }, // session_resolved gives the first non-null sessionId { id: "session-resolved", type: "lifecycle", renderClass: "lifecycle", title: "Session ready", text: "", timestamp: ts, acpSource: "session_resolved", turnId: "turn-001", sessionId: "session-001", channelId: "chan-1", }, // User prompt follows { id: "user-prompt", type: "message", role: "user", title: "Buzz event", text: "@Agent hello", timestamp: ts, acpSource: "session/prompt:user", turnId: "turn-001", sessionId: "session-001", channelId: "chan-1", }, ]; const blocks = buildTranscriptDisplayBlocks(items, "session-001"); // No boundary — this is a single-session transcript. const boundaryBlocks = blocks.filter((b) => b.kind === "session-boundary"); assert.equal( boundaryBlocks.length, 0, "no session-boundary block for a first-ever single session", ); // System prompt must appear somewhere in the output (as a standalone single). const flat = flattenDisplayBlocks(blocks); assert.ok( flat.some((i) => i.acpSource === "session/new"), "system-prompt item must be present in the single-session output", ); }); test("buildTranscriptDisplayBlocks_sessionNewNoFollowingSession_notDropped", () => { // session/new arrives after a resolved session but the stream ends before a // new session resolves (e.g. agent shutdown mid-restart). The item must not // be silently dropped — it should fall back into the current run. const ts1 = "2026-07-08T12:00:00.000Z"; const ts2 = "2026-07-08T12:01:00.000Z"; const items = [ { ...sessionItem("toolA", "sess-1", ts1), turnId: "turn-1" }, // session/new stale-stamped; stream ends here — no new session resolves. systemPromptItem("system-prompt", "sess-1", ts2), ]; const blocks = buildTranscriptDisplayBlocks(items); // No boundary — still a single session (no new session resolved). const boundaryBlocks = blocks.filter((b) => b.kind === "session-boundary"); assert.equal( boundaryBlocks.length, 0, "no boundary when session/new has no following new session", ); // system-prompt must not be dropped. const flat = flattenDisplayBlocks(blocks); assert.ok( flat.some((i) => i.id === "system-prompt"), "system-prompt must not be dropped when no new session follows", ); // toolA must also be present. assert.ok( flat.some((i) => i.id === "toolA"), "toolA must be present when session/new has no following new session", ); }); // --- no-user-prompt ordering: system-prompt leads turns in the same run ───── test("splitIntoSessionRuns: system-prompt renders before turn blocks when no user-prompt follows", () => { // Covers buildBlocksForRun's no-user-prompt branch: when session/new is // followed by a tool turn but no session/prompt:user, the system-prompt must // appear as a standalone block BEFORE the turn (boundary → prompt → activity), // not appended after all turns (the old behaviour). const ts1 = "2026-07-08T12:00:00.000Z"; const ts2 = "2026-07-08T12:01:00.000Z"; const ts3 = "2026-07-08T12:02:00.000Z"; const items = [ // A preceding tool item establishes run sess-1 so a boundary is emitted. { ...sessionItem("toolA", "sess-1", ts1), turnId: "turn-1" }, // system/new repositioned to tail (stale sess-1 stamp) — represents the // restart marker after the normalizer's reposition-on-refire fix. systemPromptItem("system-prompt", "sess-1", ts2), // New run's tool item (no user prompt in turn-2). { ...sessionItem("toolB", "sess-2", ts3), turnId: "turn-2" }, ]; const blocks = buildTranscriptDisplayBlocks(items); // One boundary between the two sessions. const boundaryBlocks = blocks.filter((b) => b.kind === "session-boundary"); assert.equal(boundaryBlocks.length, 1, "exactly one boundary"); const boundaryIdx = blocks.indexOf(boundaryBlocks[0]); const systemPromptIdx = blocks.findIndex( (b) => b.kind === "single" && b.item?.id === "system-prompt", ); assert.ok(systemPromptIdx !== -1, "system-prompt block must be present"); const toolBIdx = blocks.findIndex((b) => flattenDisplayBlocks([b]).some((i) => i.id === "toolB"), ); assert.ok(toolBIdx !== -1, "toolB block must be present"); // Required order: boundary → system-prompt → toolB activity. assert.ok( boundaryIdx < systemPromptIdx, `boundary (${boundaryIdx}) must precede system-prompt (${systemPromptIdx})`, ); assert.ok( systemPromptIdx < toolBIdx, `system-prompt (${systemPromptIdx}) must precede toolB activity (${toolBIdx})`, ); // toolA (sess-1) must appear before the boundary. const toolAIdx = blocks.findIndex((b) => flattenDisplayBlocks([b]).some((i) => i.id === "toolA"), ); assert.ok(toolAIdx !== -1, "toolA block must be present"); assert.ok( toolAIdx < boundaryIdx, `toolA (${toolAIdx}) must be before boundary (${boundaryIdx})`, ); }); // ── Session-boundary firstItemId key stability (regression) ─────────────────── // // Previously getDisplayBlockKey keyed boundaries as // `session-boundary:${sessionId}:${runIndex}`. `runIndex` is the run's // position in the ordered array — it SHIFTS when older sessions are prepended // (archive page load), causing React to remount unchanged boundaries and churn // the virtual list. // // The fix replaces `runIndex` with `firstItemId` — the id of the first // TranscriptItem in the run — which is invariant across prepend. test("buildTranscriptDisplayBlocks_sessionBoundary_emitsFirstItemId", () => { // Two sessions: sess-1 (older) then sess-2 (newer). const items = [ sessionItem("a", "sess-1", "2026-07-08T00:00:01.000Z"), sessionItem("b", "sess-2", "2026-07-08T00:00:02.000Z"), sessionItem("c", "sess-2", "2026-07-08T00:00:03.000Z"), ]; const blocks = buildTranscriptDisplayBlocks(items, null); const boundary = blocks.find((b) => b.kind === "session-boundary"); assert.ok(boundary, "boundary must be present between two sessions"); assert.equal( boundary.sessionId, "sess-2", "boundary labels the newer session", ); // firstItemId must equal the id of the first item in sess-2's run. assert.ok(boundary.firstItemId, "boundary must carry firstItemId"); // "b" is the first item in sess-2's run (from sessionItem("b", "sess-2")). assert.equal( boundary.firstItemId, "b", "firstItemId must equal the id of the first item in the following session run", ); }); test("buildTranscriptDisplayBlocks_sessionBoundary_keyStableAcrossPrepend", () => { // Before: sess-1 then sess-2. const before = [ sessionItem("a", "sess-1", "2026-07-08T00:00:02.000Z"), sessionItem("b", "sess-2", "2026-07-08T00:00:03.000Z"), ]; const blocksBefore = buildTranscriptDisplayBlocks(before, null); const boundaryBefore = blocksBefore.find( (b) => b.kind === "session-boundary" && b.sessionId === "sess-2", ); assert.ok(boundaryBefore, "must have sess-2 boundary before prepend"); const keyBefore = `session-boundary:${boundaryBefore.sessionId}:${boundaryBefore.firstItemId}`; // After: prepend an older sess-0 before sess-1. Now [sess-0, sess-1, sess-2]. const after = [ sessionItem("z", "sess-0", "2026-07-08T00:00:01.000Z"), // oldest — prepended sessionItem("a", "sess-1", "2026-07-08T00:00:02.000Z"), sessionItem("b", "sess-2", "2026-07-08T00:00:03.000Z"), ]; const blocksAfter = buildTranscriptDisplayBlocks(after, null); const boundaryAfter = blocksAfter.find( (b) => b.kind === "session-boundary" && b.sessionId === "sess-2", ); assert.ok(boundaryAfter, "must still have sess-2 boundary after prepend"); const keyAfter = `session-boundary:${boundaryAfter.sessionId}:${boundaryAfter.firstItemId}`; // firstItemId-based key must be identical before and after prepend. assert.equal( keyBefore, keyAfter, "session-boundary React key must not change when an older session is prepended", ); // Sanity check: runIndex DID shift (from 1 to 2) confirming the old key would have changed. assert.equal(boundaryBefore.runIndex, 1, "runIndex before prepend must be 1"); assert.equal( boundaryAfter.runIndex, 2, "runIndex after prepend must be 2, confirming it is unstable", ); }); // ── Multiple pre-resolution session/new markers — both survive ──────────────── test("buildTranscriptDisplayBlocks_twoPreResolutionSessionNewMarkers_bothSurviveBeforeTurn", () => { // Restart loop: two session/new items arrive after sess-1 resolves but before // sess-2 resolves (e.g. a rapid double-restart). A null-session frame arrives // between them (models a lifecycle event with no session yet). All three must // appear in the sess-2 run — neither marker nor the interleaved null-session // frame may be silently dropped. This is the documented // "marker(s) plus trailing null-session items" contract. // // Wire order: // toolA(sess-1) → sp-a(sess-1 stale) → null-frame(null) → sp-b(sess-1 stale) → toolB(sess-2) const ts1 = "2026-07-08T12:00:00.000Z"; const ts2 = "2026-07-08T12:01:00.000Z"; const ts3 = "2026-07-08T12:01:15.000Z"; const ts4 = "2026-07-08T12:01:30.000Z"; const ts5 = "2026-07-08T12:02:00.000Z"; // A null-session lifecycle frame (models turn_started or similar arriving // between two session/new firings during a rapid double-restart). const nullFrame = { id: "null-frame", type: "lifecycle", renderClass: "lifecycle", title: "Session starting", text: "", timestamp: ts3, acpSource: "turn_started", turnId: null, sessionId: null, channelId: "chan-1", }; const items = [ { ...sessionItem("toolA", "sess-1", ts1), turnId: "turn-1" }, systemPromptItem("sp-a", "sess-1", ts2), nullFrame, systemPromptItem("sp-b", "sess-1", ts4), { ...sessionItem("toolB", "sess-2", ts5), turnId: "turn-2" }, ]; const blocks = buildTranscriptDisplayBlocks(items, "sess-2"); // Exactly one boundary between the two sessions. const boundaryBlocks = blocks.filter((b) => b.kind === "session-boundary"); assert.equal( boundaryBlocks.length, 1, "exactly one session-boundary for two sessions", ); const boundaryIdx = blocks.indexOf(boundaryBlocks[0]); // Both system-prompt blocks must be present. const systemPromptBlocks = blocks.filter( (b) => b.kind === "single" && b.item?.acpSource === "session/new", ); assert.equal( systemPromptBlocks.length, 2, "both session/new markers must produce standalone single blocks", ); const spAIdx = blocks.findIndex( (b) => b.kind === "single" && b.item?.id === "sp-a", ); const spBIdx = blocks.findIndex( (b) => b.kind === "single" && b.item?.id === "sp-b", ); const toolBIdx = blocks.findIndex((b) => flattenDisplayBlocks([b]).some((i) => i.id === "toolB"), ); // Both system-prompt blocks appear AFTER the boundary. assert.ok( boundaryIdx < spAIdx, `boundary (${boundaryIdx}) must precede sp-a (${spAIdx})`, ); assert.ok( boundaryIdx < spBIdx, `boundary (${boundaryIdx}) must precede sp-b (${spBIdx})`, ); // Both system-prompt blocks appear BEFORE toolB's turn block. assert.ok( spAIdx < toolBIdx, `sp-a (${spAIdx}) must precede toolB (${toolBIdx})`, ); assert.ok( spBIdx < toolBIdx, `sp-b (${spBIdx}) must precede toolB (${toolBIdx})`, ); // sp-a must appear before sp-b (wire order preserved). assert.ok( spAIdx < spBIdx, `sp-a (${spAIdx}) must precede sp-b (${spBIdx}) — wire order must be preserved`, ); // The interleaved null-session frame must survive and appear in the sess-2 run // (after the boundary). This proves the "marker plus trailing null-session items" // contract — the null frame lands in the pending buffer between sp-a and sp-b // and must not be lost when the second marker arrives. const flat = flattenDisplayBlocks(blocks); const nullFrameInFlat = flat.some((i) => i.id === "null-frame"); assert.ok( nullFrameInFlat, "interleaved null-session frame must survive in the flattened output", ); const nullFrameBlockIdx = blocks.findIndex((b) => flattenDisplayBlocks([b]).some((i) => i.id === "null-frame"), ); assert.ok( boundaryIdx < nullFrameBlockIdx, `null-session frame (${nullFrameBlockIdx}) must appear after boundary (${boundaryIdx}) — it belongs to the new session run`, ); // Wire order: sp-a → null-frame → sp-b (all flush in insertion order). const flatSpAIdx = flat.findIndex((i) => i.id === "sp-a"); const flatNullIdx = flat.findIndex((i) => i.id === "null-frame"); const flatSpBIdx = flat.findIndex((i) => i.id === "sp-b"); assert.ok( flatSpAIdx < flatNullIdx, `sp-a (${flatSpAIdx}) must precede null-frame (${flatNullIdx}) in flat output`, ); assert.ok( flatNullIdx < flatSpBIdx, `null-frame (${flatNullIdx}) must precede sp-b (${flatSpBIdx}) in flat output`, ); // toolA (sess-1) must appear before the boundary. const toolAIdx = blocks.findIndex((b) => flattenDisplayBlocks([b]).some((i) => i.id === "toolA"), ); assert.ok( toolAIdx < boundaryIdx, `toolA (${toolAIdx}) must be before boundary (${boundaryIdx})`, ); }); // ── Regression suite for pendingForTurn / openBatch sealing ───────────────── // Production wire order: turn_started(t1) → session/new → session_resolved(t1). // The session/new arrives AFTER the bucket for turn-1 already exists. // The system-prompt must still be hoisted before turn-1. test("buildTranscriptDisplayBlocks_productionWireOrder_systemPromptBeforeTurn1", () => { const ts = "2026-07-08T12:00:00.000Z"; const items = [ // turn_started creates the bucket BEFORE session/new fires. { id: "turn-started-t1", type: "lifecycle", renderClass: "lifecycle", title: "Turn started", text: "", timestamp: ts, acpSource: "turn_started", turnId: "turn-1", sessionId: "sess-1", channelId: "chan-1", }, // session/new fires AFTER the bucket exists (stale-stamped session). systemPromptItem("sp", "sess-0", ts), // session_resolved reuses the existing turn-1 bucket. { id: "session-resolved-t1", type: "lifecycle", renderClass: "lifecycle", title: "Session resolved", text: "", timestamp: ts, acpSource: "session_resolved", turnId: "turn-1", sessionId: "sess-1", channelId: "chan-1", }, // A real activity item so the turn block has segments and appears in output. { id: "assistant-t1", type: "message", role: "assistant", title: "Assistant", text: "Hello", timestamp: ts, acpSource: "agent_message_chunk", turnId: "turn-1", sessionId: "sess-1", channelId: "chan-1", }, ]; const blocks = buildTranscriptDisplayBlocks(items); // sp must appear as a standalone single block. const spBlockIdx = blocks.findIndex( (b) => b.kind === "single" && b.item?.id === "sp", ); // turn-1 must appear as a turn block. const turnBlockIdx = blocks.findIndex( (b) => b.kind === "turn" && b.turnId === "turn-1", ); assert.ok(spBlockIdx !== -1, "sp block must be present"); assert.ok(turnBlockIdx !== -1, "turn-1 block must be present"); // System-prompt block must appear before the turn-1 block. assert.ok( spBlockIdx < turnBlockIdx, `sp block (${spBlockIdx}) must precede turn-1 block (${turnBlockIdx}) — system-prompt must flush before its anchor turn even when bucket pre-existed`, ); }); // Later null-session items (after turn-1) must NOT be hoisted before turn-1. // Sequence: sp(null) → turn-1 item → later-null-frame(null) → turn-2 item test("buildTranscriptDisplayBlocks_laterNullFrameAfterTurn1_notHoistedBeforeTurn1", () => { const ts = "2026-07-08T12:00:00.000Z"; const items = [ systemPromptItem("sp", "sess-1", ts), { ...sessionItem("toolA", "sess-1", ts), turnId: "turn-1" }, { id: "later-null-frame", type: "lifecycle", renderClass: "lifecycle", title: "Session resolved", text: "", timestamp: ts, acpSource: "session_resolved", turnId: null, sessionId: "sess-1", channelId: "chan-1", }, { ...sessionItem("toolB", "sess-1", ts), turnId: "turn-2" }, ]; const blocks = buildTranscriptDisplayBlocks(items); const flat = flattenDisplayBlocks(blocks); const spIdx = flat.findIndex((i) => i.id === "sp"); const toolAIdx = flat.findIndex((i) => i.id === "toolA"); const laterNullIdx = flat.findIndex((i) => i.id === "later-null-frame"); const toolBIdx = flat.findIndex((i) => i.id === "toolB"); assert.ok(spIdx !== -1, "sp must be present"); assert.ok(toolAIdx !== -1, "toolA must be present"); assert.ok(laterNullIdx !== -1, "later-null-frame must be present"); assert.ok(toolBIdx !== -1, "toolB must be present"); assert.ok(spIdx < toolAIdx, `sp (${spIdx}) must precede toolA (${toolAIdx})`); assert.ok( toolAIdx < laterNullIdx, `toolA (${toolAIdx}) must precede later-null-frame (${laterNullIdx}) — post-turn null items must NOT be hoisted`, ); assert.ok( laterNullIdx < toolBIdx, `later-null-frame (${laterNullIdx}) must precede toolB (${toolBIdx})`, ); }); // A leading non-system-prompt null-session item before any session/new must be // emitted inline and must not prevent subsequent system-prompt hoisting. The // buffer-open decision must not depend on displayOrder.length === 0. test("buildTranscriptDisplayBlocks_leadingSingleBeforeSessionNew_emittedInline", () => { const ts = "2026-07-08T12:00:00.000Z"; const items = [ { id: "early-lifecycle", type: "lifecycle", renderClass: "lifecycle", title: "Agent connected", text: "", timestamp: ts, acpSource: "agent_connected", turnId: null, sessionId: "sess-1", channelId: "chan-1", }, systemPromptItem("sp", "sess-1", ts), { ...sessionItem("toolA", "sess-1", ts), turnId: "turn-1" }, ]; const blocks = buildTranscriptDisplayBlocks(items); const flat = flattenDisplayBlocks(blocks); const earlyIdx = flat.findIndex((i) => i.id === "early-lifecycle"); const spIdx = flat.findIndex((i) => i.id === "sp"); const toolAIdx = flat.findIndex((i) => i.id === "toolA"); assert.ok(earlyIdx !== -1, "early-lifecycle must be present"); assert.ok(spIdx !== -1, "sp must be present"); assert.ok(toolAIdx !== -1, "toolA must be present"); assert.ok( earlyIdx < toolAIdx, `early-lifecycle (${earlyIdx}) must precede toolA (${toolAIdx})`, ); assert.ok( spIdx < toolAIdx, `sp (${spIdx}) must precede toolA (${toolAIdx}) — system-prompt must be hoisted even after a leading item`, ); }); // ── getDisplayBlockKey ────────────────────────────────────────────────────────── test("getDisplayBlockKey_single_returnsItemId", () => { const block = { kind: "single", item: { id: "item-42" } }; assert.equal(getDisplayBlockKey(block), "item-42"); }); test("getDisplayBlockKey_turn_returnsPrefixedTurnId", () => { const block = { kind: "turn", turnId: "t-99", segments: [] }; assert.equal(getDisplayBlockKey(block), "turn:t-99"); }); test("getDisplayBlockKey_sessionBoundary_usesFirstItemIdNotRunIndex", () => { const block = { kind: "session-boundary", sessionId: "sess-2", sessionStartTimestamp: "2026-07-08T00:00:00.000Z", labelState: "most-recent", runIndex: 3, firstItemId: "first-item-abc", }; assert.equal( getDisplayBlockKey(block), "session-boundary:sess-2:first-item-abc", "key must use firstItemId, not runIndex", ); }); test("getDisplayBlockKey_independentOfLatestLiveSessionId", () => { // Build blocks for the same items with different latestLiveSessionId values. // Keys must be identical — only labelState differs. const items = [ sessionItem("a", "sess-1", "2026-07-08T00:00:01.000Z"), sessionItem("b", "sess-2", "2026-07-08T00:00:02.000Z"), ]; const blocksLive = buildTranscriptDisplayBlocks(items, "sess-2"); const blocksNoLive = buildTranscriptDisplayBlocks(items, null); const blocksDiffLive = buildTranscriptDisplayBlocks(items, "sess-other"); const keysLive = blocksLive.map(getDisplayBlockKey); const keysNoLive = blocksNoLive.map(getDisplayBlockKey); const keysDiffLive = blocksDiffLive.map(getDisplayBlockKey); assert.deepEqual( keysLive, keysNoLive, "keys must not vary with latestLiveSessionId=null", ); assert.deepEqual( keysLive, keysDiffLive, "keys must not vary with latestLiveSessionId=other", ); // Sanity check: labelState DID change (proves the test setup matters). const boundaryLive = blocksLive.find((b) => b.kind === "session-boundary"); const boundaryNoLive = blocksNoLive.find( (b) => b.kind === "session-boundary", ); assert.notEqual( boundaryLive.labelState, boundaryNoLive.labelState, "labelState must differ (test setup sanity check)", ); }); test("getDisplayBlockKey_parity_matchesBuildTranscriptDisplayBlocksOutput", () => { // End-to-end: derive keys from buildTranscriptDisplayBlocks and assert they // match the expected key for each block kind (the load-bearing invariant that // outer-panel-derived ids match inner DOM data-message-id). const items = [ sessionItem("orphan", "sess-1", "2026-07-08T00:00:00.000Z"), // Multi-session to get a boundary block. sessionItem("tool-a", "sess-2", "2026-07-08T00:00:01.000Z"), ]; // Give orphan no turnId so it becomes a "single" block. items[0].turnId = undefined; const blocks = buildTranscriptDisplayBlocks(items, null); const keys = blocks.map(getDisplayBlockKey); // Expect: [single:orphan, boundary:sess-2:tool-a, turn:turn-tool-a] assert.ok(keys.includes("orphan"), "single block key = item.id"); assert.ok( keys.some((k) => k.startsWith("session-boundary:sess-2:")), "boundary block key uses session-boundary: prefix", ); assert.ok( keys.some((k) => k.startsWith("turn:")), "turn block key uses turn: prefix", ); // No duplicates. assert.equal(new Set(keys).size, keys.length, "all keys must be unique"); }); // ── Key-parity invariant: transient [turn, single] → [single, turn] reorder ── test("getDisplayBlockKey_firstTurnReorder_keysStableAcrossReorder", () => { // Partial sequence: turn_started → session/new (two items, before session_resolved). // At this point the key set is some combination of single + turn keys. const ts = "2026-07-08T10:00:00.000Z"; const partialItems = [ { id: "turn-started", type: "lifecycle", renderClass: "lifecycle", title: "Turn started", text: "", timestamp: ts, acpSource: "turn_started", turnId: "turn-001", sessionId: null, channelId: "chan-1", }, { id: "system-prompt:chan-1", type: "metadata", renderClass: "raw-rail", title: "System prompt", sections: [{ title: "Base", body: "You are a helpful AI assistant." }], timestamp: ts, acpSource: "session/new", turnId: null, sessionId: null, channelId: "chan-1", }, ]; const partialBlocks = buildTranscriptDisplayBlocks(partialItems); const partialKeys = new Set(partialBlocks.map(getDisplayBlockKey)); // Full sequence: add session_resolved (same turnId) — this seals the open // batch and may reorder [turn, single] → [single, turn]. const fullItems = [ ...partialItems, { id: "session-resolved", type: "lifecycle", renderClass: "lifecycle", title: "Session ready", text: "", timestamp: ts, acpSource: "session_resolved", turnId: "turn-001", sessionId: "session-001", channelId: "chan-1", }, ]; const fullBlocks = buildTranscriptDisplayBlocks(fullItems); const fullKeys = new Set(fullBlocks.map(getDisplayBlockKey)); // The KEY IDENTITIES must be identical — only the ORDER may differ. assert.deepEqual( partialKeys, fullKeys, "block key identities must be identical before and after session_resolved seals the open batch (order may differ)", ); });