Rewrites the 27 DOM-behavior test files (dialogs, sidebar, spawn form, command bar, structured pane, catalog, multiview, theme, etc.) to use screen/within/fireEvent/waitFor instead of manual querySelector and dispatchEvent. Pure-logic test files (matcher, tree, activity, paths, reconnect, keys, webgl, paneTitle, commands, daemonLink, events, layout, persistence, previewSchedule, projectList, spawnSimilar, structuredDiff, toolIcon, fastForward, spawnForm) are untouched, and preview.test.ts needed no changes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
513 lines
21 KiB
TypeScript
513 lines
21 KiB
TypeScript
// @vitest-environment happy-dom
|
||
import { describe, expect, it, vi } from "vitest";
|
||
import { fireEvent, waitFor, within } from "@testing-library/react";
|
||
|
||
import {
|
||
buildToolGroup,
|
||
buildTranscriptRow,
|
||
renderInlineMarkdown,
|
||
renderMarkdown,
|
||
} from "./structuredRender.js";
|
||
import type { TranscriptRow } from "./types.js";
|
||
|
||
function row<K extends TranscriptRow["kind"]>(
|
||
kind: K,
|
||
data: unknown,
|
||
id = "r1",
|
||
): TranscriptRow & { kind: K } {
|
||
return { id, kind, data };
|
||
}
|
||
|
||
describe("buildTranscriptRow: user/agent/thought", () => {
|
||
it("renders a user row with the text content and no markdown", () => {
|
||
const el = buildTranscriptRow(
|
||
row("user", { content: { type: "text", text: "hi <b>there</b>" } }),
|
||
);
|
||
expect(el.className).toContain("st-row-user");
|
||
expect(within(el).getByText("hi <b>there</b>")).toBeTruthy();
|
||
// Neither element exists at all — no accessible text/role to query by, so
|
||
// this stays a direct DOM check for "never actual markup from user input".
|
||
expect(el.querySelector(".st-who")).toBeNull();
|
||
expect(el.querySelector("b")).toBeNull();
|
||
});
|
||
|
||
it("renders agent prose as paragraphs with inline markdown", () => {
|
||
const el = buildTranscriptRow(
|
||
row("agent_text", { content: { type: "text", text: "one **two** `three`\n\nfour" } }),
|
||
);
|
||
expect(el.className).toContain("st-row-agent");
|
||
const ps = within(el).getAllByRole("paragraph");
|
||
expect(ps).toHaveLength(2);
|
||
expect(ps[0].innerHTML).toContain("<strong>two</strong>");
|
||
expect(ps[0].innerHTML).toContain("<code>three</code>");
|
||
});
|
||
|
||
it("trims ACP framing whitespace and renders thought-row inline markdown", () => {
|
||
const el = buildTranscriptRow(
|
||
row("agent_thought", {
|
||
content: { type: "text", text: "\n\n**Planning comply tool commentary update**\n" },
|
||
}),
|
||
);
|
||
expect(el.className).toContain("st-row-thought");
|
||
expect(el.textContent).toBe("Planning comply tool commentary update");
|
||
expect(
|
||
within(el).getByText("Planning comply tool commentary update", { selector: "strong" }),
|
||
).toBeTruthy();
|
||
});
|
||
|
||
it("falls back to a labelled placeholder for non-text content, never crashing", () => {
|
||
const el = buildTranscriptRow(row("user", { content: { type: "image" } }));
|
||
expect(within(el).getByText("[image]")).toBeTruthy();
|
||
});
|
||
|
||
it("never clips text: a very long user message is not truncated", () => {
|
||
const long = "x".repeat(5000);
|
||
const el = buildTranscriptRow(row("user", { content: { type: "text", text: long } }));
|
||
expect(within(el).getByText(long)).toBeTruthy();
|
||
});
|
||
|
||
it("copies message blocks on click and supports keyboard activation", async () => {
|
||
const writeText = vi.fn().mockResolvedValue(undefined);
|
||
Object.defineProperty(navigator, "clipboard", { configurable: true, value: { writeText } });
|
||
const el = buildTranscriptRow(
|
||
row("agent_text", { content: { type: "text", text: "copy **this** exactly" } }),
|
||
);
|
||
expect(el.getAttribute("role")).toBe("button");
|
||
fireEvent.click(el);
|
||
await waitFor(() => expect(writeText).toHaveBeenCalledWith("copy **this** exactly"));
|
||
fireEvent.keyDown(el, { key: "Enter" });
|
||
await waitFor(() => expect(writeText).toHaveBeenCalledTimes(2));
|
||
});
|
||
});
|
||
|
||
describe("renderInlineMarkdown", () => {
|
||
it("escapes raw HTML before applying markdown", () => {
|
||
expect(renderInlineMarkdown("<script>alert(1)</script>")).not.toContain("<script>");
|
||
});
|
||
|
||
it("does not treat a single asterisk pair meant as bold as nested italics", () => {
|
||
expect(renderInlineMarkdown("**bold**")).toBe("<strong>bold</strong>");
|
||
});
|
||
});
|
||
|
||
describe("renderMarkdown", () => {
|
||
it("renders GFM tables as semantic table markup", () => {
|
||
const html = renderMarkdown("| Model | State |\n| --- | --- |\n| Opus | ready |");
|
||
expect(html).toContain("<table>");
|
||
expect(html).toContain("<th>Model</th>");
|
||
expect(html).toContain("<td>Opus</td>");
|
||
});
|
||
|
||
it("removes executable HTML and unsafe URLs", () => {
|
||
const html = renderMarkdown(
|
||
'<img src=x onerror="alert(1)"><script>alert(2)</script>[bad](javascript:alert(3))',
|
||
);
|
||
expect(html).not.toContain("<img");
|
||
expect(html).not.toContain("<script");
|
||
expect(html).not.toContain('href="javascript:');
|
||
});
|
||
});
|
||
|
||
describe("buildTranscriptRow: tool calls", () => {
|
||
it("shows glyph, name, argument and no meta for a completed non-diff call", () => {
|
||
const el = buildTranscriptRow(
|
||
row("tool_call", {
|
||
toolCallId: "t1",
|
||
title: "Read(src/lib.rs)",
|
||
kind: "read",
|
||
status: "completed",
|
||
}),
|
||
);
|
||
const head = within(el).getByRole("button");
|
||
expect(within(head).getByText("Read")).toBeTruthy();
|
||
expect(within(head).getByText("src/lib.rs")).toBeTruthy();
|
||
// No badge text/role exists at all in this case — nothing accessible to
|
||
// query for its absence, so this stays a direct DOM check.
|
||
expect(head.querySelector(".st-tool-meta")).toBeNull();
|
||
});
|
||
|
||
it("falls back to the kind label and full title when the title has no Name(arg) shape", () => {
|
||
const el = buildTranscriptRow(row("tool_call", { title: "does something odd", kind: "execute" }));
|
||
const head = within(el).getByRole("button");
|
||
expect(within(head).getByText("Execute")).toBeTruthy();
|
||
expect(within(head).getByText("does something odd")).toBeTruthy();
|
||
});
|
||
|
||
it("shows a running badge for an in-progress call", () => {
|
||
const el = buildTranscriptRow(row("tool_call", { title: "Bash(sleep 1)", status: "in_progress" }));
|
||
const meta = within(el).getByText("running");
|
||
expect(meta.className).toContain("st-tool-meta-running");
|
||
});
|
||
|
||
it("shows a failed badge for a failed call", () => {
|
||
const el = buildTranscriptRow(row("tool_call", { title: "Bash(false)", status: "failed" }));
|
||
expect(within(el).getByText("failed")).toBeTruthy();
|
||
});
|
||
|
||
it("starts collapsed and expands its text output on click", () => {
|
||
const el = buildTranscriptRow(
|
||
row("tool_call", {
|
||
title: "Grep(x)",
|
||
status: "completed",
|
||
content: [{ type: "content", content: { type: "text", text: "match found" } }],
|
||
}),
|
||
);
|
||
const head = within(el).getByRole("button");
|
||
// The disclosure wrapper has no accessible role or text of its own before
|
||
// it is expanded, so it's located and inspected via raw DOM throughout.
|
||
const out = el.querySelector(".st-tool-out") as HTMLElement;
|
||
expect(out.hidden).toBe(true);
|
||
expect(head.getAttribute("aria-expanded")).toBe("false");
|
||
fireEvent.click(head);
|
||
expect(out.hidden).toBe(false);
|
||
expect(head.getAttribute("aria-expanded")).toBe("true");
|
||
expect(within(out).getByText("match found")).toBeTruthy();
|
||
fireEvent.click(head);
|
||
expect(out.hidden).toBe(false);
|
||
expect(out.getAttribute("aria-hidden")).toBe("true");
|
||
expect(out.inert).toBe(true);
|
||
expect(out.classList.contains("is-expanded")).toBe(false);
|
||
});
|
||
|
||
it("does not expand when there is nothing to show", () => {
|
||
const el = buildTranscriptRow(row("tool_call", { title: "Read(x)", status: "completed" }));
|
||
const head = within(el).getByRole("button");
|
||
fireEvent.click(head);
|
||
expect(head.getAttribute("aria-expanded")).toBe("false");
|
||
});
|
||
|
||
it("renders command fields and keeps the exact JSON available", () => {
|
||
const el = buildTranscriptRow(
|
||
row("tool_call", {
|
||
title: "Bash(test)",
|
||
rawInput: { command: "printf ok", cwd: "/workspace", timeout_ms: 1000 },
|
||
rawOutput: { formatted_output: "ok", exit_code: 0 },
|
||
}),
|
||
);
|
||
fireEvent.click(within(el).getByRole("button"));
|
||
expect(within(el).getByText("printf ok")).toBeTruthy();
|
||
// "Exit code" and its value are separate elements with no shared text
|
||
// node, so getByText can't match "Exit code0" — locate via the label.
|
||
const fact = within(el).getByText("Exit code").closest(".st-tool-fact");
|
||
expect(fact?.textContent).toBe("Exit code0");
|
||
const raw = within(el).getByText("Raw JSON").closest("details") as HTMLDetailsElement;
|
||
expect(raw.open).toBe(false);
|
||
expect(raw.textContent).toContain('"timeout_ms": 1000');
|
||
expect(raw.textContent).toContain('"exit_code": 0');
|
||
});
|
||
|
||
it("renders MCP arguments and results without hiding the source payload", () => {
|
||
const el = buildTranscriptRow(
|
||
row("tool_call", {
|
||
title: "mcp.github.search",
|
||
rawInput: { server: "github", tool: "search", arguments: { query: "ACP" } },
|
||
rawOutput: { result: { count: 2 }, error: null },
|
||
}),
|
||
);
|
||
fireEvent.click(within(el).getByRole("button"));
|
||
const labels = within(el)
|
||
.getAllByText(/^(Arguments|Result)$/)
|
||
.map((node) => node.textContent);
|
||
expect(labels).toEqual(["Arguments", "Result"]);
|
||
const raw = within(el).getByText("Raw JSON").closest("details");
|
||
expect(raw?.textContent).toContain('"server": "github"');
|
||
});
|
||
|
||
it("renders a recognized web search query", () => {
|
||
const el = buildTranscriptRow(
|
||
row("tool_call", {
|
||
title: "Search the web",
|
||
rawInput: { type: "webSearch", query: "Agent Client Protocol", action: { type: "search" } },
|
||
}),
|
||
);
|
||
fireEvent.click(within(el).getByRole("button"));
|
||
expect(within(el).getByText("Query")).toBeTruthy();
|
||
expect(within(el).getByText("Agent Client Protocol")).toBeTruthy();
|
||
});
|
||
|
||
it("falls back to raw JSON for an unknown provider shape", () => {
|
||
const el = buildTranscriptRow(
|
||
row("tool_call", { title: "Custom", rawInput: { peculiar: [1, 2] }, rawOutput: "done" }),
|
||
);
|
||
fireEvent.click(within(el).getByRole("button"));
|
||
// No provider-specific detail block has any known label to query by when
|
||
// none matched — asserting the absence of the whole class stays raw DOM.
|
||
expect(
|
||
el.querySelectorAll(
|
||
".st-tool-out > .st-disclosure-clip > .st-disclosure-inner > .st-tool-detail",
|
||
),
|
||
).toHaveLength(0);
|
||
const raw = within(el).getByText("Raw JSON").closest("details");
|
||
expect(raw?.textContent).toContain('"peculiar"');
|
||
expect(raw?.textContent).toContain('"done"');
|
||
});
|
||
|
||
it("renders standardized content before provider-specific and raw data", () => {
|
||
const el = buildTranscriptRow(
|
||
row("tool_call", {
|
||
title: "Bash(test)",
|
||
content: [
|
||
{ type: "content", content: { type: "text", text: "typed result" } },
|
||
{ type: "content", content: { type: "resource_link", name: "report", uri: "file:///tmp/report" } },
|
||
{ type: "terminal", terminalId: "term-1" },
|
||
],
|
||
rawInput: { command: "test" },
|
||
rawOutput: { formatted_output: "typed result", exit_code: 0 },
|
||
}),
|
||
);
|
||
fireEvent.click(within(el).getByRole("button"));
|
||
// Ordering across block kinds has no accessible query of its own — this
|
||
// stays a direct DOM structure check.
|
||
const inner = el.querySelector(".st-tool-out > .st-disclosure-clip > .st-disclosure-inner")!;
|
||
expect([...inner.children].map((node) => node.className)).toEqual([
|
||
"st-tool-text st-copyable",
|
||
"st-tool-resource",
|
||
"st-tool-terminal",
|
||
"st-tool-detail",
|
||
"st-tool-fact",
|
||
"st-tool-raw",
|
||
]);
|
||
expect(within(el).getByText("file:///tmp/report")).toBeTruthy();
|
||
expect(within(el).getByText("Terminal · term-1")).toBeTruthy();
|
||
});
|
||
|
||
it("renders safe typed image and audio blocks as media controls", () => {
|
||
const el = buildTranscriptRow(
|
||
row("tool_call", {
|
||
title: "Media",
|
||
content: [
|
||
{ type: "content", content: { type: "image", mimeType: "image/png", data: "aW1hZ2U=" } },
|
||
{ type: "content", content: { type: "audio", mimeType: "audio/mpeg", data: "YXVkaW8=" } },
|
||
],
|
||
}),
|
||
);
|
||
fireEvent.click(within(el).getByRole("button"));
|
||
expect((within(el).getByRole("img") as HTMLImageElement).src).toBe(
|
||
"data:image/png;base64,aW1hZ2U=",
|
||
);
|
||
// <audio> has no ARIA role mapping, so it isn't reachable via getByRole.
|
||
expect((el.querySelector(".st-tool-audio") as HTMLAudioElement).src).toBe(
|
||
"data:audio/mpeg;base64,YXVkaW8=",
|
||
);
|
||
});
|
||
});
|
||
|
||
describe("buildToolGroup", () => {
|
||
it("starts collapsed, then reveals independently expandable tool calls", () => {
|
||
const rows = [
|
||
row("tool_call", { title: "Read(a.ts)", content: [{ type: "content", content: { type: "text", text: "a" } }] }, "t1"),
|
||
row("tool_call", { title: "Bash(test)", content: [{ type: "content", content: { type: "text", text: "ok" } }] }, "t2"),
|
||
];
|
||
const children = rows.map((tool) => buildTranscriptRow(tool));
|
||
const onExpanded = vi.fn();
|
||
const group = buildToolGroup("turn:u1", rows, children, false, onExpanded);
|
||
const head = within(group).getByRole("button", { name: "2 tool calls" });
|
||
// The body has no accessible role/text of its own while collapsed, so its
|
||
// disclosure state is inspected via raw DOM.
|
||
const body = group.querySelector<HTMLElement>(".st-tool-group-body")!;
|
||
expect(body.getAttribute("aria-hidden")).toBe("true");
|
||
expect(body.inert).toBe(true);
|
||
fireEvent.click(head);
|
||
expect(body.getAttribute("aria-hidden")).toBe("false");
|
||
expect(body.classList.contains("is-expanded")).toBe(true);
|
||
expect(onExpanded).toHaveBeenCalledWith(true);
|
||
const firstHead = within(group).getByRole("button", { name: "Read a.ts" });
|
||
const secondHead = within(group).getByRole("button", { name: "Bash test" });
|
||
fireEvent.click(secondHead);
|
||
expect(firstHead.getAttribute("aria-expanded")).toBe("false");
|
||
expect(secondHead.getAttribute("aria-expanded")).toBe("true");
|
||
});
|
||
});
|
||
|
||
describe("buildTranscriptRow: tool diffs", () => {
|
||
it("shows +added -removed derived from the diff, and renders coloured hunks on expand", () => {
|
||
const el = buildTranscriptRow(
|
||
row("tool_diff", {
|
||
title: "Edit(x.rs)",
|
||
status: "completed",
|
||
content: [{ type: "diff", path: "/repo/x.rs", oldText: "a\nb\n", newText: "a\nc\n" }],
|
||
}),
|
||
);
|
||
expect(within(el).getByText("+1 −1")).toBeTruthy();
|
||
fireEvent.click(within(el).getByRole("button"));
|
||
expect(within(el).getByText("+ c")).toBeTruthy();
|
||
expect(within(el).getByText("- b")).toBeTruthy();
|
||
});
|
||
|
||
it("gives the diff its own horizontally-scrolling container", () => {
|
||
const el = buildTranscriptRow(
|
||
row("tool_diff", {
|
||
title: "Edit(x.rs)",
|
||
content: [{ type: "diff", path: "/repo/x.rs", newText: "new file\n" }],
|
||
}),
|
||
);
|
||
fireEvent.click(within(el).getByRole("button"));
|
||
// jsdom/happy-dom doesn't compute layout CSS, so assert the class the
|
||
// stylesheet keys `overflow-x: auto` off, not a computed style. The
|
||
// scroller itself has no accessible role/text, hence the raw query.
|
||
expect(el.querySelector(".st-diff")!.classList).toContain("st-diff");
|
||
});
|
||
|
||
it("renders a visible marker for a removed final newline", () => {
|
||
const el = buildTranscriptRow(
|
||
row("tool_diff", {
|
||
title: "Edit(x.rs)",
|
||
content: [{ type: "diff", path: "/repo/x.rs", oldText: "same\n", newText: "same" }],
|
||
}),
|
||
);
|
||
fireEvent.click(within(el).getByRole("button"));
|
||
expect(within(el).getByText("\\ No newline at end of file")).toBeTruthy();
|
||
});
|
||
});
|
||
|
||
describe("buildTranscriptRow: approval", () => {
|
||
const approvalRow = (options: Array<{ optionId: string; name: string; kind: string }>) =>
|
||
row("approval", {
|
||
sessionId: "s1",
|
||
toolCall: { toolCallId: "t1", title: "Write outside worktree", locations: [{ path: "/etc/x" }] },
|
||
options,
|
||
});
|
||
|
||
const allThreeOptions = [
|
||
{ optionId: "a1", name: "Allow once", kind: "allow_once" },
|
||
{ optionId: "a2", name: "Allow always", kind: "allow_always" },
|
||
{ optionId: "r1", name: "Reject once", kind: "reject_once" },
|
||
];
|
||
|
||
it("shows the three decision buttons only when the request offers all three option kinds", () => {
|
||
const el = buildTranscriptRow(approvalRow(allThreeOptions), {
|
||
pending: true,
|
||
onDecide: () => Promise.resolve(),
|
||
});
|
||
expect(within(el).getAllByRole("button")).toHaveLength(3);
|
||
expect(within(el).getByText("/etc/x")).toBeTruthy();
|
||
});
|
||
|
||
it("renders a compact one-liner once resolved", () => {
|
||
const el = buildTranscriptRow(approvalRow(allThreeOptions), {
|
||
pending: false,
|
||
onDecide: () => Promise.resolve(),
|
||
});
|
||
expect(within(el).queryByRole("button")).toBeNull();
|
||
expect(el.classList.contains("st-approval-compact")).toBe(true);
|
||
expect(el.textContent).toContain("approved");
|
||
});
|
||
|
||
it("invokes onDecide with the clicked decision and disables the buttons", () => {
|
||
const onDecide = vi.fn().mockResolvedValue(undefined);
|
||
const el = buildTranscriptRow(approvalRow(allThreeOptions), { pending: true, onDecide });
|
||
fireEvent.click(within(el).getByRole("button", { name: "Deny" }));
|
||
expect(onDecide).toHaveBeenCalledWith("deny");
|
||
expect(within(el).getByRole<HTMLButtonElement>("button", { name: "Allow once" }).disabled).toBe(
|
||
true,
|
||
);
|
||
});
|
||
|
||
it("treats a row with no approval state as not pending, defensively", () => {
|
||
const el = buildTranscriptRow(approvalRow(allThreeOptions));
|
||
expect(within(el).queryByRole("button")).toBeNull();
|
||
});
|
||
|
||
it("omits Allow for session when the request offers no allow_always option", () => {
|
||
const el = buildTranscriptRow(
|
||
approvalRow([
|
||
{ optionId: "a1", name: "Allow once", kind: "allow_once" },
|
||
{ optionId: "r1", name: "Reject once", kind: "reject_once" },
|
||
]),
|
||
{ pending: true, onDecide: () => Promise.resolve() },
|
||
);
|
||
const labels = within(el)
|
||
.getAllByRole("button")
|
||
.map((b) => b.textContent);
|
||
expect(labels).toEqual(["Allow once", "Deny"]);
|
||
});
|
||
|
||
it("offers Deny for either reject_once or reject_always, never for neither", () => {
|
||
const rejectAlwaysOnly = buildTranscriptRow(
|
||
approvalRow([{ optionId: "r2", name: "Reject always", kind: "reject_always" }]),
|
||
{ pending: true, onDecide: () => Promise.resolve() },
|
||
);
|
||
expect(
|
||
within(rejectAlwaysOnly)
|
||
.getAllByRole("button")
|
||
.map((b) => b.textContent),
|
||
).toEqual(["Deny"]);
|
||
});
|
||
|
||
it("shows a 'no decision offered' note instead of an empty action row when nothing matches", () => {
|
||
const el = buildTranscriptRow(approvalRow([{ optionId: "x", name: "Something else", kind: "allow_once" }]), {
|
||
pending: true,
|
||
onDecide: () => Promise.resolve(),
|
||
});
|
||
// allow_once is offered, so this specific case still shows a button —
|
||
// use a kind the client doesn't map to any decision at all.
|
||
expect(within(el).getAllByRole("button")).toHaveLength(1);
|
||
|
||
const none = buildTranscriptRow(approvalRow([]), { pending: true, onDecide: () => Promise.resolve() });
|
||
expect(within(none).queryByRole("button")).toBeNull();
|
||
expect(within(none).getByText("no decision offered")).toBeTruthy();
|
||
});
|
||
|
||
it("re-enables the buttons when onDecide rejects", async () => {
|
||
const onDecide = vi.fn().mockRejectedValue(new Error("network down"));
|
||
const el = buildTranscriptRow(approvalRow(allThreeOptions), { pending: true, onDecide });
|
||
const allowOnce = within(el).getByRole<HTMLButtonElement>("button", { name: "Allow once" });
|
||
const allowSession = within(el).getByRole<HTMLButtonElement>("button", {
|
||
name: "Allow for session",
|
||
});
|
||
fireEvent.click(allowOnce);
|
||
expect(allowOnce.disabled).toBe(true);
|
||
await waitFor(() => expect(allowOnce.disabled).toBe(false));
|
||
expect(allowSession.disabled).toBe(false);
|
||
});
|
||
|
||
it("leaves the buttons disabled when onDecide resolves", async () => {
|
||
const onDecide = vi.fn().mockResolvedValue(undefined);
|
||
const el = buildTranscriptRow(approvalRow(allThreeOptions), { pending: true, onDecide });
|
||
const allowOnce = within(el).getByRole<HTMLButtonElement>("button", { name: "Allow once" });
|
||
fireEvent.click(allowOnce);
|
||
await waitFor(() => expect(allowOnce.disabled).toBe(true));
|
||
});
|
||
});
|
||
|
||
describe("buildTranscriptRow: notice / unknown kinds", () => {
|
||
it("renders durable per-turn duration and context usage", () => {
|
||
const el = buildTranscriptRow(
|
||
row("notice", {
|
||
maestroTurnMeta: {
|
||
elapsedMs: 72_000,
|
||
contextTokens: 18_400,
|
||
contextDelta: 2_350,
|
||
outcome: "completed",
|
||
model: "opus",
|
||
},
|
||
}, "turn:1"),
|
||
)!;
|
||
|
||
expect(el.classList.contains("st-turn-meta")).toBe(true);
|
||
expect(el.textContent).toContain("1m 12s · +2.4k context tokens · opus");
|
||
expect(el.getAttribute("aria-label")).toBe("Copy turn details");
|
||
});
|
||
|
||
it("renders notice and unknown row kinds safely", () => {
|
||
expect(buildTranscriptRow(row("notice", { sessionUpdate: "future" }))?.textContent).toBe(
|
||
"[notice]",
|
||
);
|
||
const unknown = { id: "r1", kind: "future_kind", data: {} } as unknown as TranscriptRow;
|
||
expect(buildTranscriptRow(unknown)?.textContent).toBe("[future_kind]");
|
||
});
|
||
|
||
it("omits command-catalog and session-info notices used only as UI state", () => {
|
||
expect(
|
||
buildTranscriptRow(
|
||
row("notice", { sessionUpdate: "available_commands_update", availableCommands: [] }),
|
||
),
|
||
).toBeNull();
|
||
expect(buildTranscriptRow(row("notice", { sessionUpdate: "session_info_update" }))).toBeNull();
|
||
});
|
||
|
||
it("returns no transcript element for a plan row", () => {
|
||
expect(buildTranscriptRow(row("plan", { entries: [] }))).toBeNull();
|
||
});
|
||
});
|