mirror of
https://github.com/block/buzz.git
synced 2026-10-30 05:51:46 +00:00
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Signed-off-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
(cherry picked from commit dad000df47)
Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz>
155 lines
6.6 KiB
JavaScript
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();
|
|
}
|
|
});
|