Files
buzz/desktop/src/features/agents/ui/agentSessionTranscript.test.mjs
2026-06-30 12:19:06 -07:00

642 lines
19 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import { buildTranscript } from "./agentSessionTranscript.ts";
import { formatToolTitle } from "./agentSessionToolCatalog.ts";
const baseEvent = {
seq: 1,
timestamp: "2026-06-18T00:00:00Z",
kind: "acp_write",
agentIndex: 0,
channelId: "11111111-1111-1111-1111-111111111111",
sessionId: "sess-1",
turnId: "turn-1",
};
const PROMPT_EVENT_ID = "c".repeat(64);
function acpToolUpdate(seq, update) {
return {
...baseEvent,
seq,
kind: "acp_read",
payload: {
method: "session/update",
params: {
sessionId: baseEvent.sessionId,
update,
},
},
};
}
function toolItems(events) {
return buildTranscript(events).filter((item) => item.type === "tool");
}
function activityTitle(item) {
return formatToolTitle(item.buzzToolName ?? item.toolName, item.title);
}
// --- stub-overflow vanish (pins the pre-existing degraded-frame behavior) ---
test("buildTranscript drops a session/prompt turn whose frame was stubbed by the size trimmer", () => {
// When fit_observer_event_to_budget cannot shrink a frame below the cap it
// replaces the whole payload with {elided, originalBytes} (no `method`), so
// the method-keyed acp_write dispatch matches no arm and there is no terminal
// else: the turn produces ZERO transcript items. This is worse than a
// "1 section" collapse (the item vanishes entirely) and is pre-existing,
// outside the format_prompt seam. Pin it so a later change can't silently
// regress the vanish-vs-degrade behavior without updating this test.
const stubbed = {
...baseEvent,
payload: {
elided: "acp_write payload too large",
originalBytes: 123456,
},
};
assert.deepEqual(buildTranscript([stubbed]), []);
});
// --- positive control: a well-formed multi-block prompt DOES render ---
test("buildTranscript renders Prompt context + user message for a multi-block session/prompt frame", () => {
// Guards the vanish assertion above against a false pass from a broken
// import or dispatch: a normal per-section prompt frame must still produce a
// user message and a "Prompt context" metadata item.
const event = {
...baseEvent,
payload: {
method: "session/prompt",
params: {
sessionId: "sess-1",
prompt: [
{ type: "text", text: "[Agent Memory — core]\nremember this" },
{ type: "text", text: "[Context]\nScope: thread" },
{
type: "text",
text: `[Buzz event: @mention]\nEvent ID: ${PROMPT_EVENT_ID.toUpperCase()}\nFrom: x (hex: ${"a".repeat(64)})\nContent: hello`,
},
],
},
},
};
const items = buildTranscript([event]);
const titles = items.map((i) => i.title);
assert.ok(
items.some((i) => i.type === "metadata" && i.title === "Prompt context"),
`expected a Prompt context metadata item, got titles: ${titles.join(", ")}`,
);
const promptContext = items.find((i) => i.title === "Prompt context");
assert.deepEqual(
promptContext.sections.map((s) => s.title),
["Agent Memory — core", "Context", "Buzz event: @mention"],
"every section header is counted",
);
const userMessage = items.find((i) => i.type === "message");
assert.equal(userMessage.messageId, PROMPT_EVENT_ID);
});
test("buildTranscript falls back to a single turn trigger id for older prompt frames", () => {
const promptEvent = {
...baseEvent,
seq: 2,
payload: {
method: "session/prompt",
params: {
sessionId: "sess-1",
prompt: [
{
type: "text",
text: `[Buzz event: @mention]\nFrom: x (hex: ${"a".repeat(64)})\nContent: hello`,
},
],
},
},
};
const [userMessage] = buildTranscript([
{
...baseEvent,
seq: 1,
kind: "turn_started",
payload: {
triggeringEventIds: [PROMPT_EVENT_ID],
},
},
promptEvent,
]).filter((candidate) => candidate.type === "message");
assert.equal(userMessage.messageId, PROMPT_EVENT_ID);
});
test("buildTranscript keeps read_file activity categorized by the actual tool when output names Buzz tools", () => {
const [item] = toolItems([
acpToolUpdate(10, {
sessionUpdate: "tool_call",
toolCallId: "call-read-file",
status: "executing",
title: "read_file",
kind: "read_file",
rawInput: {
path: "desktop/src/features/agents/ui/agentSessionToolCatalog.ts",
},
}),
acpToolUpdate(11, {
sessionUpdate: "tool_call_update",
toolCallId: "call-read-file",
status: "completed",
title: "read_file",
kind: "read_file",
rawInput: {
path: "desktop/src/features/agents/ui/agentSessionToolCatalog.ts",
},
content: {
type: "text",
text: 'const BUZZ_READ_TOOLS = new Set(["get_feed", "get_event"]);\nconst BUZZ_WRITE_TOOLS = new Set(["delete_message"]);',
},
}),
]);
assert.equal(item.toolName, "read_file");
assert.equal(item.buzzToolName, null);
assert.equal(item.title, "read_file");
assert.equal(activityTitle(item), "read_file");
assert.equal(item.status, "completed");
assert.match(item.result, /get_feed/);
assert.match(item.result, /delete_message/);
});
test("buildTranscript keeps shell activity categorized by the actual tool when grep output names Buzz tools", () => {
const [item] = toolItems([
acpToolUpdate(20, {
sessionUpdate: "tool_call",
toolCallId: "call-shell-rg",
status: "executing",
title: "shell",
kind: "shell",
rawInput: {
command: 'rg -n "get_event|delete_message" desktop/src',
},
}),
acpToolUpdate(21, {
sessionUpdate: "tool_call_update",
toolCallId: "call-shell-rg",
status: "completed",
title: "shell",
kind: "shell",
rawInput: {
command: 'rg -n "get_event|delete_message" desktop/src',
},
rawOutput:
'desktop/src/features/agents/ui/agentSessionToolCatalog.ts:83: "get_event",\n' +
'desktop/src/features/agents/ui/agentSessionToolCatalog.ts:92: "delete_message",',
}),
]);
assert.equal(item.toolName, "shell");
assert.equal(item.buzzToolName, null);
assert.equal(activityTitle(item), "shell");
assert.equal(item.status, "completed");
assert.match(item.result, /get_event/);
assert.match(item.result, /delete_message/);
});
test("buildTranscript categorizes explicit Buzz tool calls for the activity bar", () => {
const [item] = toolItems([
acpToolUpdate(30, {
sessionUpdate: "tool_call",
toolCallId: "call-get-feed",
status: "executing",
title: "Tool call",
toolName: "get_feed",
rawInput: { limit: 20 },
}),
acpToolUpdate(31, {
sessionUpdate: "tool_call_update",
toolCallId: "call-get-feed",
status: "completed",
title: "Tool call",
toolName: "get_feed",
content: { type: "text", text: "[]" },
}),
]);
assert.equal(item.toolName, "get_feed");
assert.equal(item.buzzToolName, "get_feed");
assert.equal(activityTitle(item), "Get Feed");
assert.deepEqual(item.args, { limit: 20 });
assert.equal(item.status, "completed");
});
function sessionUpdate(seq, update, overrides = {}) {
return {
...baseEvent,
...overrides,
seq,
kind: "acp_read",
payload: {
method: "session/update",
params: {
sessionId: overrides.sessionId ?? baseEvent.sessionId,
update,
},
},
};
}
function assistantChunk(seq, messageId, text, overrides = {}) {
return sessionUpdate(
seq,
{
sessionUpdate: "agent_message_chunk",
messageId,
content: { type: "text", text },
},
overrides,
);
}
test("buildTranscript preserves author pubkeys on user message chunks", () => {
const authorPubkey = "b".repeat(64);
const [item] = buildTranscript([
sessionUpdate(25, {
sessionUpdate: "user_message_chunk",
messageId: "user-chunk-1",
authorPubkey,
content: { type: "text", text: "please keep this visible" },
}),
]).filter((candidate) => candidate.type === "message");
assert.equal(item.role, "user");
assert.equal(item.authorPubkey, authorPubkey);
assert.equal(item.messageId, null);
});
test("buildTranscript preserves real event ids on user message chunks", () => {
const authorPubkey = "b".repeat(64);
const messageId = "d".repeat(64);
const [item] = buildTranscript([
sessionUpdate(26, {
sessionUpdate: "user_message_chunk",
messageId,
authorPubkey,
content: { type: "text", text: "this came from a channel message" },
}),
]).filter((candidate) => candidate.type === "message");
assert.equal(item.role, "user");
assert.equal(item.messageId, messageId);
});
test("buildTranscript de-duplicates repeated tool updates into one canonical row", () => {
const items = toolItems([
acpToolUpdate(40, {
sessionUpdate: "tool_call",
toolCallId: "call-dupe",
status: "executing",
title: "shell",
kind: "shell",
rawInput: { command: "echo hi" },
}),
acpToolUpdate(41, {
sessionUpdate: "tool_call_update",
toolCallId: "call-dupe",
status: "completed",
title: "shell",
kind: "shell",
rawOutput: "hi",
}),
acpToolUpdate(42, {
sessionUpdate: "tool_call_update",
toolCallId: "call-dupe",
status: "completed",
title: "shell",
kind: "shell",
rawOutput: "hi",
}),
]);
assert.equal(items.length, 1);
assert.equal(items[0].id, `tool:${baseEvent.channelId}:call-dupe`);
assert.equal(items[0].status, "completed");
assert.equal(items[0].result, "hi");
});
test("buildTranscript keeps a completed tool terminal when a late executing call arrives", () => {
const [item] = toolItems([
acpToolUpdate(50, {
sessionUpdate: "tool_call_update",
toolCallId: "call-regression",
status: "completed",
title: "shell",
kind: "shell",
rawOutput: "done",
}),
acpToolUpdate(51, {
sessionUpdate: "tool_call",
toolCallId: "call-regression",
status: "executing",
title: "shell",
kind: "shell",
rawInput: { command: "echo done" },
}),
]);
assert.equal(item.status, "completed");
assert.equal(item.completedAt, baseEvent.timestamp);
assert.deepEqual(item.args, { command: "echo done" });
assert.equal(item.result, "done");
});
test("buildTranscript rebuilds out-of-order tool frames as one canonical row with retained ids", () => {
const [item] = toolItems([
sessionUpdate(
60,
{
sessionUpdate: "tool_call_update",
toolCallId: "call-out-of-order",
status: "completed",
title: "read_file",
kind: "read_file",
rawOutput: "file contents",
},
{
channelId: "22222222-2222-2222-2222-222222222222",
sessionId: "sess-2",
turnId: "turn-2",
timestamp: "2026-06-18T00:00:05Z",
},
),
sessionUpdate(
61,
{
sessionUpdate: "tool_call",
toolCallId: "call-out-of-order",
status: "executing",
title: "read_file",
kind: "read_file",
rawInput: { path: "AGENTS.md" },
},
{
channelId: "22222222-2222-2222-2222-222222222222",
sessionId: "sess-2",
turnId: "turn-2",
timestamp: "2026-06-18T00:00:04Z",
},
),
]);
assert.equal(
item.id,
"tool:22222222-2222-2222-2222-222222222222:call-out-of-order",
);
assert.equal(item.status, "completed");
assert.deepEqual(item.args, { path: "AGENTS.md" });
assert.equal(item.channelId, "22222222-2222-2222-2222-222222222222");
assert.equal(item.turnId, "turn-2");
assert.equal(item.sessionId, "sess-2");
});
test("buildTranscript coalesces assistant chunks until the message is sealed", () => {
const messages = buildTranscript([
assistantChunk(70, "msg-1", "Hello "),
assistantChunk(71, "msg-1", "world"),
]).filter((item) => item.type === "message" && item.role === "assistant");
assert.equal(messages.length, 1);
assert.equal(messages[0].text, "Hello world");
assert.equal(messages[0].id, `assistant:${baseEvent.channelId}:msg-1`);
});
test("buildTranscript starts a continuation for same-message chunks after sealing", () => {
const messages = buildTranscript([
assistantChunk(80, "msg-2", "First"),
acpToolUpdate(81, {
sessionUpdate: "tool_call",
toolCallId: "call-seal",
status: "executing",
title: "shell",
kind: "shell",
}),
assistantChunk(82, "msg-2", "Second"),
]).filter((item) => item.type === "message" && item.role === "assistant");
assert.equal(messages.length, 2);
assert.equal(messages[0].text, "First");
assert.equal(messages[1].text, "Second");
assert.match(messages[1].id, /:c\d+$/);
});
test("buildTranscript preserves channel, turn, and session ids through message updates", () => {
const [message] = buildTranscript([
assistantChunk(90, "msg-identity", "One ", {
channelId: "33333333-3333-3333-3333-333333333333",
sessionId: "sess-identity",
turnId: "turn-identity",
}),
assistantChunk(91, "msg-identity", "Two", {
channelId: "33333333-3333-3333-3333-333333333333",
sessionId: null,
turnId: null,
}),
]).filter((item) => item.type === "message" && item.role === "assistant");
assert.equal(message.text, "One Two");
assert.equal(message.channelId, "33333333-3333-3333-3333-333333333333");
assert.equal(message.turnId, "turn-identity");
assert.equal(message.sessionId, "sess-identity");
});
test("buildTranscript promotes ACP plan updates to first-class plan items", () => {
const items = buildTranscript([
sessionUpdate(90, {
sessionUpdate: "plan",
content: { type: "text", text: "- [ ] Build registry" },
}),
]);
assert.equal(items.length, 1);
assert.equal(items[0].type, "plan");
assert.equal(items[0].renderClass, "plan");
assert.match(items[0].text, /Build registry/);
});
test("buildTranscript stores first-class render class descriptors for tool items", () => {
const [item] = toolItems([
acpToolUpdate(91, {
sessionUpdate: "tool_call",
toolCallId: "call-edit",
status: "completed",
title: "str_replace",
kind: "str_replace",
rawInput: { path: "src/app.ts" },
}),
]);
assert.equal(item.renderClass, "file-edit");
assert.equal(item.descriptor.label, "Edited file");
assert.equal(item.descriptor.preview, "src/app.ts");
});
test("buildTranscript surfaces session/request_permission as a permission lifecycle item", () => {
const transcript = buildTranscript([
{
seq: 1,
timestamp: "2026-06-30T09:00:00.000Z",
kind: "acp_read",
agentIndex: 0,
channelId: "channel-1",
sessionId: "session-1",
turnId: "turn-1",
payload: {
jsonrpc: "2.0",
method: "session/request_permission",
params: {
toolCallId: "tool-1",
title: "Confirm force-with-lease push to block/buzz.",
options: [
{ optionId: "allow_once", kind: "allow_once", name: "Allow" },
{ optionId: "reject_once", kind: "reject_once", name: "Reject" },
],
},
},
},
]);
assert.equal(transcript.length, 1);
assert.equal(transcript[0].type, "lifecycle");
assert.equal(transcript[0].renderClass, "permission");
assert.equal(transcript[0].title, "Permission requested");
assert.match(transcript[0].text, /Confirm force-with-lease push/);
});
test("buildTranscript stamps completedAt when a terminal tool update is inserted first", () => {
const transcript = buildTranscript([
{
seq: 1,
timestamp: "2026-06-30T09:00:00.000Z",
kind: "acp_read",
agentIndex: 0,
channelId: "channel-1",
sessionId: "session-1",
turnId: "turn-1",
payload: {
jsonrpc: "2.0",
method: "session/update",
params: {
sessionId: "session-1",
update: {
sessionUpdate: "tool_call_update",
toolCallId: "tool-1",
toolName: "dev__shell",
status: "completed",
rawInput: { command: "echo hi" },
content: [{ type: "text", text: "hi" }],
},
},
},
},
]);
assert.equal(transcript[0].type, "tool");
assert.equal(transcript[0].completedAt, "2026-06-30T09:00:00.000Z");
});
test("buildTranscript preserves permission, free-form status, and raw rail render classes", () => {
const transcript = buildTranscript([
{
...baseEvent,
seq: 1,
kind: "acp_read",
payload: {
method: "session/request_permission",
params: {
title: "Confirm force-with-lease push",
toolCallId: "tool-push",
options: [
{ optionId: "allow_once", kind: "allow_once", name: "Allow" },
{ optionId: "reject_once", kind: "reject_once", name: "Reject" },
],
},
},
},
{
...baseEvent,
seq: 2,
kind: "acp_read",
payload: {
type: "observer_connected",
title: "Observer connected",
text: "ACP stream attached",
},
},
{
...baseEvent,
seq: 3,
kind: "raw_json_rpc",
payload: {
method: "workspace/diagnostic",
params: { ok: true },
},
},
]);
const permissionItem = transcript.find(
(item) =>
item.id.startsWith("permission:") && item.renderClass === "permission",
);
assert.ok(
permissionItem,
"permission request should flow through the reducer",
);
assert.doesNotMatch(
permissionItem.text,
/^Permission requested\b/,
"permission detail should not duplicate the row title",
);
assert.ok(
transcript.some(
(item) =>
item.renderClass === "status" && item.title === "Observer connected",
),
"free-form status fixture should flow through the reducer",
);
assert.ok(
transcript.some(
(item) => item.type === "metadata" && item.renderClass === "raw-rail",
),
"raw_json_rpc fixture should flow through the reducer",
);
});
test("buildTranscript separates repeated lifecycle text", () => {
const events = [
{
seq: 1,
timestamp: "2026-06-30T09:00:00.000Z",
kind: "turn_error",
agentIndex: 0,
channelId: "channel-1",
sessionId: "session-1",
turnId: "turn-1",
payload: { outcome: "recovered", error: "first" },
},
{
seq: 2,
timestamp: "2026-06-30T09:00:01.000Z",
kind: "turn_error",
agentIndex: 0,
channelId: "channel-1",
sessionId: "session-1",
turnId: "turn-1",
payload: { outcome: "recovered", error: "second" },
},
];
const [item] = buildTranscript(events);
assert.equal(item.type, "lifecycle");
assert.equal(item.text, "recovered: first\nrecovered: second");
});