This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
maestro/web/src/components/structured/model.test.ts
T
Miguel PalhasandClaude Opus 5 9ba1078386 feat(maestro): port the structured pane to React
Closes #246. The transcript, composer and approval controls move from
structured.ts/structuredRender.ts into components under
web/src/components/structured/, against docs/STRUCTURED-VIEW.md: eight row
kinds, both projections, the single-active approval nonce, and diff rendering
with its own line counts.

The pure layers stay shared — structuredDiff.ts computes the diff,
structuredRender.ts still sanitizes markdown — and model.ts holds the
client-side decisions (activity grouping, notice classification, offered
decisions, `/` completion) so they are testable without a DOM.

Like TerminalPane, the pane draws the body only; pane chrome reads the
connection through onStatusChange.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 00:41:18 +01:00

253 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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("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"]);
});
});