Files
buzz/desktop/src/features/agents/ui/AgentSessionTranscriptList.conversationChrome.test.mjs

155 lines
6.6 KiB
JavaScript

/**
* Presentation contract for the `conversation` transcript variant's *chrome*:
* the single-agent answer treatment and the focus code-block recipe.
*
* Split out of `AgentSessionTranscriptList.conversation.test.mjs` to stay under
* the repo's hard 1000-line/file ceiling (AGENTS.md). The shared jsdom setup,
* ambient-formatting pins, and render helpers live in the harness so the two
* suites cannot drift apart.
*/
import assert from "node:assert/strict";
import { test } from "node:test";
import {
cleanup,
fencedCodeItems,
fencedCodePromptItems,
renderTranscript,
renderTranscriptWithCodeChrome,
} from "./AgentSessionTranscriptList.conversationHarness.mjs";
test("conversation keeps assistant prose unboxed without repeating the pane identity", async () => {
// The activity pane is scoped to one agent and its sticky header already
// carries that identity. Repeating avatar + name above every answer adds no
// speaker information and visually collides with message-send tool bubbles.
const { container } = await renderTranscript("conversation");
assert.ok(
container.querySelector('[data-testid="transcript-assistant-identity"]') ===
null,
"the single-agent pane should not repeat its header identity",
);
const message = container.querySelector(
'[data-testid="transcript-assistant-message"]',
);
assert.ok(message, "the assistant answer should render");
assert.doesNotMatch(message.innerHTML, /rounded-2xl/);
});
test("conversation frames fenced code with berd's header row", async () => {
// berd puts the language in a real header row above the frame, with the copy
// action opposite it (`code-block.tsx` CodeBlockHeader:388-402), and the code
// itself in a 10px-radius, page-background, borderless-shadow frame
// (:528-529). Buzz's `rounded-lg` (`--radius: 0.625rem`) is exactly berd's
// `rounded-[0.625rem]`.
const { container } = await renderTranscriptWithCodeChrome("conversation", {
items: fencedCodeItems(),
});
const header = container.querySelector(
'[data-testid="markdown-code-block-header"]',
);
assert.ok(header, "focus mode should render a code-block header row");
// Language sits in the header, not inside the frame.
assert.match(header.textContent, /^ts/);
assert.match(header.className, /justify-between/);
assert.match(header.className, /items-end/);
assert.match(header.className, /min-h-7/);
assert.ok(
header.querySelector('[aria-label="Copy code block"]'),
"the copy action is a flow sibling of the language label",
);
const frame = container.querySelector("pre");
assert.ok(frame, "the code frame should render");
assert.match(frame.className, /rounded-lg/);
assert.match(frame.className, /bg-background/);
assert.match(frame.className, /border-border\/80/);
assert.doesNotMatch(
frame.className,
/shadow/,
"berd's code frame carries no shadow",
);
// Guards against the default recipe leaking in: it uses a 16px radius, a
// muted fill, `pr-12` to clear an absolutely-positioned copy button, and an
// inline `borderRadius` style.
assert.doesNotMatch(frame.className, /rounded-2xl/);
assert.doesNotMatch(frame.className, /bg-muted/);
assert.doesNotMatch(frame.className, /pr-12/);
assert.equal(frame.style.borderRadius, "");
// Line numbers come from `.code-block-lines [data-line]` in markdown.css, so
// the frame only has to keep emitting per-line elements under that class.
const code = frame.querySelector("code.code-block-lines");
assert.ok(code, "the code element keeps the line-number class");
assert.equal(code.querySelectorAll("[data-line]").length, 2);
});
test("conversation applies the code recipe to a fenced human prompt too", async () => {
// Regression guard for a real bug quality caught. The provider was first
// mounted inside `MessageActivity`, which only handles assistant items — the
// user bubble returns before it, so a fence inside a prompt kept the legacy
// 16px muted frame nested inside the new 12px bubble. The recipe is a
// property of the *surface*, not of a role, so the provider now sits at the
// transcript boundary and both roles inherit it.
const { container } = await renderTranscriptWithCodeChrome("conversation", {
items: fencedCodePromptItems(),
});
const bubble = container.querySelector(
'[data-testid="transcript-user-message"]',
);
assert.ok(bubble, "the prompt should render");
assert.ok(
bubble.querySelector('[data-testid="markdown-code-block-header"]'),
"a fence inside the prompt gets berd's header row",
);
const frame = bubble.querySelector("pre");
assert.match(frame.className, /rounded-lg/);
assert.doesNotMatch(
frame.className,
/rounded-2xl/,
"the legacy 16px frame must not nest inside the 12px bubble",
);
assert.doesNotMatch(frame.className, /pr-12/);
});
test("the default transcript variant keeps the legacy code chrome", async () => {
// The markdown renderer is shared with channel messages, so `focusProse` is
// opt-in per surface. Rendering the same fenced block through the `default`
// transcript variant must still produce the original chrome: no header row,
// 16px radius, muted fill, and the absolutely-positioned copy button.
//
// This proves the *variant gate*, not the channel-message row itself — those
// rows are covered by the markdown tests in `shared/ui/markdown`.
const { container } = await renderTranscriptWithCodeChrome("default", {
items: fencedCodeItems(),
});
// `assert.ok(x === null)` rather than `assert.equal(x, null)`: on failure the
// latter serializes the whole matched jsdom element (and its ancestors) to
// build a diff, which exhausts memory instead of printing the message.
assert.ok(
container.querySelector('[data-testid="markdown-code-block-header"]') ===
null,
"the default recipe has no header row",
);
const frame = container.querySelector("pre");
assert.match(frame.className, /rounded-2xl/);
assert.match(frame.className, /bg-muted\/60/);
assert.match(frame.className, /pr-12/);
assert.match(frame.className, /shadow-xs/);
const copy = container.querySelector('[aria-label="Copy code block"]');
assert.ok(copy, "the default copy button still renders");
assert.match(copy.className, /absolute/);
});
test("no transcript variant adds a per-answer identity row", async () => {
for (const variant of ["conversation", "default", "compactPreview"]) {
const { container } = await renderTranscript(variant);
assert.ok(
container.querySelector(
'[data-testid="transcript-assistant-identity"]',
) === null,
`${variant} must not render a per-answer identity row`,
);
cleanup();
}
});