import { describe, expect, it } from "vitest"; import type { ReducedState, TranscriptRow } from "../../types.js"; import { activityGroupsByTurn, applyCommand, commandsFromTranscript, commandTokenAt, contentBlockText, filterCommands, isStateOnlyNotice, offeredDecisions, orderConfigOptions, pendingApprovalRowId, splitToolTitle, statusMeta, toolDiffMeta, toolKindLabel, transcriptItems, turnMeta, } from "./model.js"; function row(id: string, kind: TranscriptRow["kind"], data: unknown = {}): TranscriptRow { return { id, kind, data }; } function tool(id: string): TranscriptRow { return row(id, "tool_call", { title: `Read(${id})`, status: "completed" }); } function text(id: string, kind: "user" | "agent_text" | "agent_thought", body: string): TranscriptRow { return row(id, kind, { content: { type: "text", text: body } }); } const idle: ReducedState = { turn: { responding: false, thinking: false, active_tool_calls: [] } }; describe("row taxonomy", () => { it("splits an adapter's Name(argument) title, and falls back to the kind", () => { expect(splitToolTitle("Read(src/main.ts)", null, "read")).toEqual({ label: "Read", arg: "src/main.ts" }); expect(splitToolTitle("something opaque", null, "search")).toEqual({ label: "Search", arg: "something opaque", }); expect(splitToolTitle(undefined, null, undefined)).toEqual({ label: "Tool", arg: "" }); }); it("labels an unknown tool kind rather than leaving it blank", () => { expect(toolKindLabel("execute")).toBe("Execute"); expect(toolKindLabel("no_such_kind")).toBe("Tool"); expect(toolKindLabel(undefined)).toBe("Tool"); }); it("names non-text content blocks instead of dropping them", () => { expect(contentBlockText({ type: "text", text: "hi" })).toBe("hi"); expect(contentBlockText({ type: "image" })).toBe("[image]"); expect(contentBlockText({ type: "resource_link", name: "spec.md" })).toBe("[resource: spec.md]"); expect(contentBlockText(undefined)).toBe(""); }); it("counts a tool call's diff lines", () => { expect(toolDiffMeta([{ type: "diff", path: "a.ts", oldText: "one\ntwo\n", newText: "one\nTWO\nthree\n" }])) .toBe("+2 −1"); expect(toolDiffMeta([])).toBe("+0 −0"); }); it("shows a status only while it is worth showing", () => { expect(statusMeta("in_progress", false)).toEqual({ text: "running", kind: "running" }); expect(statusMeta("failed", true)).toEqual({ text: "failed", kind: "failed" }); expect(statusMeta("pending", true)).toBeNull(); // the diff counts already say it expect(statusMeta("completed", false)).toBeNull(); }); }); describe("notices", () => { it("reads a maestroTurnMeta footer, and only a well-formed one", () => { const meta = { elapsedMs: 4200, outcome: "completed", model: "sonnet" }; expect(turnMeta(row("n1", "notice", { maestroTurnMeta: meta }))).toEqual(meta); expect(turnMeta(row("n2", "notice", { maestroTurnMeta: { outcome: "completed" } }))).toBeNull(); expect(turnMeta(row("n3", "notice", { sessionUpdate: "session_info_update" }))).toBeNull(); }); it("marks the state-only notices the client consumes rather than shows", () => { expect(isStateOnlyNotice(row("n1", "notice", { sessionUpdate: "available_commands_update" }))).toBe(true); expect(isStateOnlyNotice(row("n2", "notice", { sessionUpdate: "session_info_update" }))).toBe(true); expect(isStateOnlyNotice(row("n3", "notice", { sessionUpdate: "something_else" }))).toBe(false); }); }); describe("approvals", () => { it("offers only decisions the request actually backs", () => { expect( offeredDecisions([ { optionId: "a", name: "Allow once", kind: "allow_once" }, { optionId: "r", name: "Reject", kind: "reject_once" }, ]), ).toEqual(["allow_once", "deny"]); expect(offeredDecisions([{ optionId: "a", name: "Always", kind: "allow_always" }])).toEqual([ "allow_for_session", ]); expect(offeredDecisions([])).toEqual([]); }); it("points a live pending approval at the newest approval row", () => { const rows = [row("approval:1", "approval"), tool("tool:1"), row("approval:2", "approval")]; const pending: ReducedState = { ...idle, pending_approval: { nonce: "n", request: {} as never }, }; expect(pendingApprovalRowId(rows, pending)).toBe("approval:2"); expect(pendingApprovalRowId(rows, idle)).toBeNull(); }); }); describe("activity grouping", () => { it("groups a turn's tools once there are two of them", () => { const rows = [text("user:1", "user", "go"), tool("tool:1"), tool("tool:2")]; const groups = activityGroupsByTurn(rows, null, false); expect([...groups.keys()]).toEqual(["turn:user:1"]); expect(groups.get("turn:user:1")).toMatchObject({ toolCount: 2 }); }); it("leaves a single tool call standalone", () => { const rows = [text("user:1", "user", "go"), tool("tool:1")]; expect(activityGroupsByTurn(rows, null, false).size).toBe(0); }); it("absorbs intermediate narration but keeps the final answer outside", () => { const rows = [ text("user:1", "user", "go"), tool("tool:1"), text("agent:1", "agent_text", "working"), tool("tool:2"), text("agent:2", "agent_text", "done"), ]; const group = activityGroupsByTurn(rows, null, false).get("turn:user:1")!; expect(group.rows.map((r) => r.id)).toEqual(["tool:1", "agent:1", "tool:2"]); expect(group.toolCount).toBe(2); }); it("keeps the newest thought outside while the turn is still live", () => { const rows = [ text("user:1", "user", "go"), tool("tool:1"), tool("tool:2"), text("thought:1", "agent_thought", "hmm"), text("thought:2", "agent_thought", "aha"), ]; const live = activityGroupsByTurn(rows, null, true).get("turn:user:1")!; expect(live.rows.map((r) => r.id)).toEqual(["tool:1", "tool:2", "thought:1"]); const ended = activityGroupsByTurn(rows, null, false).get("turn:user:1")!; expect(ended.rows.map((r) => r.id)).toEqual(["tool:1", "tool:2", "thought:1", "thought:2"]); }); it("keeps the newest agent_text outside while the turn is still live", () => { const rows = [ text("user:1", "user", "go"), tool("tool:1"), text("agent:1", "agent_text", "working on it"), tool("tool:2"), text("agent:2", "agent_text", "almost done"), ]; const live = activityGroupsByTurn(rows, null, true).get("turn:user:1")!; // "almost done" stays outside as visible narration while the turn is live expect(live.rows.map((r) => r.id)).toEqual(["tool:1", "agent:1", "tool:2"]); const ended = activityGroupsByTurn(rows, null, false).get("turn:user:1")!; // Once ended, the final agent_text stays outside as the answer expect(ended.rows.map((r) => r.id)).toEqual(["tool:1", "agent:1", "tool:2"]); }); it("tracks the last tool's label and argument for the collapsed header", () => { const rows = [ text("user:1", "user", "go"), row("tool:1", "tool_call", { title: "Read(src/main.ts)", status: "completed" }), row("tool:2", "tool_call", { title: "Execute(git status)", kind: "execute", status: "completed" }), ]; const group = activityGroupsByTurn(rows, null, false).get("turn:user:1")!; expect(group.lastToolLabel).toBe("Execute"); expect(group.lastToolArg).toBe("git status"); }); it("absorbs a resolved approval, never the pending one", () => { const rows = [ text("user:1", "user", "go"), tool("tool:1"), row("approval:1", "approval"), tool("tool:2"), row("approval:2", "approval"), tool("tool:3"), ]; const group = activityGroupsByTurn(rows, "approval:2", false).get("turn:user:1")!; expect(group.rows.map((r) => r.id)).toEqual(["tool:1", "approval:1", "tool:2", "tool:3"]); }); }); describe("transcript render list", () => { it("renders a group once, at its first member's position", () => { const rows = [text("user:1", "user", "go"), tool("tool:1"), tool("tool:2"), text("agent:1", "agent_text", "done")]; const items = transcriptItems(rows, null, false); expect(items.map((item) => (item.type === "row" ? item.row.id : item.id))).toEqual([ "user:1", "turn:user:1", "agent:1", ]); }); it("omits plan rows, state-only notices and standalone resolved approvals", () => { const rows = [ text("user:1", "user", "go"), row("plan", "plan", { entries: [] }), row("notice:1", "notice", { sessionUpdate: "available_commands_update", availableCommands: [] }), row("approval:1", "approval"), text("agent:1", "agent_text", "done"), ]; const items = transcriptItems(rows, null, false); expect(items.map((item) => (item.type === "row" ? item.row.id : item.id))).toEqual(["user:1", "agent:1"]); }); it("keeps a turn footer only while it is the final item", () => { const footer = row("turn_meta:1", "notice", { maestroTurnMeta: { elapsedMs: 10, outcome: "completed" } }); expect(transcriptItems([text("user:1", "user", "go"), footer], null, false)).toHaveLength(2); const later = [text("user:1", "user", "go"), footer, text("user:2", "user", "again")]; expect(transcriptItems(later, null, false).map((i) => (i.type === "row" ? i.row.id : i.id))).toEqual([ "user:1", "user:2", ]); }); }); describe("composer commands", () => { const notice = row("notice:9", "notice", { sessionUpdate: "available_commands_update", availableCommands: [ { name: "compact", description: "Compact the\n conversation" }, { name: "cost", description: "Show cost", input: { hint: "[period]" } }, { name: 42 }, ], }); it("takes the newest available_commands_update, skipping malformed entries", () => { const stale = row("notice:1", "notice", { sessionUpdate: "available_commands_update", availableCommands: [{ name: "old", description: "gone" }], }); expect(commandsFromTranscript([stale, notice]).map((c) => c.name)).toEqual(["compact", "cost"]); expect(commandsFromTranscript([])).toEqual([]); }); it("only matches a /token under the caret", () => { expect(commandTokenAt("/comp", 5, 5)).toEqual({ start: 0, end: 5, query: "comp" }); expect(commandTokenAt("hello /co there", 9, 9)).toEqual({ start: 6, end: 9, query: "co" }); expect(commandTokenAt("no slash", 3, 3)).toBeNull(); expect(commandTokenAt("/comp", 2, 5)).toBeNull(); // a selection is not a caret }); it("ranks prefix matches ahead of substring matches", () => { const commands = commandsFromTranscript([notice]); expect(filterCommands(commands, "co").map((c) => c.name)).toEqual(["compact", "cost"]); expect(filterCommands(commands, "act").map((c) => c.name)).toEqual(["compact"]); }); it("replaces only the token under the caret", () => { const token = commandTokenAt("say /co now", 7, 7)!; expect(applyCommand("say /co now", token, "compact")).toEqual({ value: "say /compact now", cursor: 13 }); const trailing = commandTokenAt("/co", 3, 3)!; expect(applyCommand("/co", trailing, "compact")).toEqual({ value: "/compact ", cursor: 9 }); }); }); describe("session config", () => { it("orders the menu by category, unknown categories last", () => { const options = [ { id: "d", category: null }, { id: "b", category: "thought_level" }, { id: "a", category: "model" }, { id: "c", category: "mode" }, ]; expect(orderConfigOptions(options).map((o) => o.id)).toEqual(["a", "b", "c", "d"]); }); });