/**
* Tests for the model-invokable `visual` ui_show surface.
*
* Two layers are covered. The tool-level guards (`definitions.ts`) reject a
* fragment the sandbox would fail silently on — external resources, colour
* literals, invented `var()` names — before anything is proxied. The daemon
* resolver then emits the surface: the `ui_surface_show` it sends must
* validate against the canonical event schema, because the client's stream
* parser silently drops events that do not.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "bun:test";
import { UISurfaceShowEventSchema } from "../api/events/ui-surface-show.js";
import type { AssistantEvent } from "../api/index.js";
import type { Conversation } from "../daemon/conversation.js";
import {
createSurfaceMutex,
surfaceProxyResolver,
} from "../daemon/conversation-surfaces.js";
import type { SurfaceType } from "../daemon/message-protocol.js";
import { INTERACTIVE_SURFACE_TYPES } from "../daemon/message-protocol.js";
import type { ToolContext, ToolExecutionResult } from "../tools/types.js";
import { uiShowTool } from "../tools/ui-surface/definitions.js";
import { validateVisualHtml } from "../tools/ui-surface/visual-validation.js";
import { asConversation } from "./helpers/mock-conversation.js";
const HTML = '
Hello
';
function makeContext(sent: AssistantEvent[] = []): Conversation {
return asConversation({
conversationId: "session-1",
emit: (msg) => sent.push(msg),
pendingSurfaceActions: new Map(),
lastSurfaceAction: new Map<
string,
{ actionId: string; data?: Record }
>(),
surfaceState: new Map(),
surfaceUndoStacks: new Map(),
accumulatedSurfaceState: new Map>(),
surfaceActionRequestIds: new Set(),
currentTurnSurfaces: [],
isProcessing: () => false,
enqueueMessage: () => ({ queued: false, requestId: "req-1" }),
getQueueDepth: () => 0,
processMessage: async () => "ok",
withSurface: createSurfaceMutex(),
});
}
function makeToolContext(onProxy?: () => void): ToolContext {
return {
conversationId: "conversation-123",
workingDir: "/tmp",
trustClass: "guardian",
proxyToolResolver: async (): Promise => {
onProxy?.();
return { content: "Surface displayed", isError: false };
},
};
}
/** Run ui_show and report whether the payload reached the proxy. */
async function runUiShow(
input: Record,
): Promise<{ result: ToolExecutionResult; proxied: boolean }> {
let proxied = false;
const result = await uiShowTool.execute(
input,
makeToolContext(() => {
proxied = true;
}),
);
return { result, proxied };
}
// ---------------------------------------------------------------------------
// Tool definition
// ---------------------------------------------------------------------------
describe("ui_show advertises visual", () => {
test("the surface_type enum includes visual", () => {
const surfaceTypeEnum = (
uiShowTool.input_schema as {
properties: { surface_type: { enum: string[] } };
}
).properties.surface_type.enum;
expect(surfaceTypeEnum).toContain("visual");
});
test("the description points at the visualize skill in one index line", () => {
expect(uiShowTool.description).toContain(
"visual (polished inline diagram/chart/explainer — PREFER this when explaining how something works or compares; load the `visualize` skill first)",
);
});
test("visual is display-only, not an interactive surface", () => {
expect(INTERACTIVE_SURFACE_TYPES).not.toContain("visual");
});
});
// ---------------------------------------------------------------------------
// Fragment guards
// ---------------------------------------------------------------------------
describe("ui_show visual fragment guards", () => {
test("rejects an empty fragment without proxying", async () => {
const { result, proxied } = await runUiShow({
surface_type: "visual",
data: { html: " " },
});
expect(result.isError).toBe(true);
expect(result.content).toContain("non-empty HTML fragment in `data.html`");
expect(proxied).toBe(false);
});
test("rejects an oversized fragment with a simplification hint", async () => {
const { result, proxied } = await runUiShow({
surface_type: "visual",
data: { html: "
',
},
});
expect(result.isError).toBe(false);
expect(proxied).toBe(true);
});
test("accepts a custom property the fragment sets from script", async () => {
const { result, proxied } = await runUiShow({
surface_type: "visual",
data: {
html:
'
Hi
' +
"",
},
});
expect(result.isError).toBe(false);
expect(proxied).toBe(true);
});
test("validates a fragment the model placed at the top level", async () => {
const { result, proxied } = await runUiShow({
surface_type: "visual",
html: '
Hi
',
data: {},
});
expect(result.isError).toBe(true);
expect(result.content).toContain("#fff");
expect(proxied).toBe(false);
});
test("rejects a bare label whose only same-palette fill is a distant pill", async () => {
// The fragment that rendered at 1.04:1 in dark mode: two labels painted
// directly on the transparent background, with the matching light stops
// used as pill fills several hundred characters away.
const pill = (palette: string): string =>
`tag';
const { result, proxied } = await runUiShow({
surface_type: "visual",
data: {
html:
'' +
'
' +
"Buckets hold one entry each; a collision walks forward to the next free slot. " +
"Load factor stays under 0.7 so the walk stays short and lookups stay near constant time. " +
"The table doubles when it crosses that line, which rehashes every key once.
" +
`
${pill("forest")}${pill("danger")}
`,
},
});
expect(result.isError).toBe(true);
expect(result.content).toContain("--color-forest-900");
expect(result.content).toContain("--color-danger-900");
expect(result.content).toContain("mirror across their own ramp");
expect(proxied).toBe(false);
});
test("accepts a label painted beside its box in the same group", async () => {
const { result, proxied } = await runUiShow({
surface_type: "visual",
data: {
html:
'',
},
});
expect(result.isError).toBe(false);
expect(proxied).toBe(true);
});
test("accepts a label whose box fill comes from the group's class rule", async () => {
const { result, proxied } = await runUiShow({
surface_type: "visual",
data: {
html:
'',
},
});
expect(result.isError).toBe(false);
expect(proxied).toBe(true);
});
test("rejects dark ramp text with no light fill of the same palette", async () => {
const { result, proxied } = await runUiShow({
surface_type: "visual",
data: {
html:
'',
},
});
expect(result.isError).toBe(true);
expect(result.content).toContain("--color-forest-900");
expect(result.content).toContain("--color-forest-800");
expect(result.content).toContain("mirror across their own ramp");
expect(result.content).toContain("--content-*");
expect(proxied).toBe(false);
});
// Which elements a selector reaches is not knowable from the markup, so a
// fill anywhere in the fragment pairs a stylesheet-painted label.
test("accepts stylesheet-painted text once a matched light fill is present", async () => {
const { result, proxied } = await runUiShow({
surface_type: "visual",
data: {
html:
'',
},
});
expect(result.isError).toBe(false);
expect(proxied).toBe(true);
});
test("accepts a matched triple written in CSS on an HTML card", async () => {
const { result, proxied } = await runUiShow({
surface_type: "visual",
data: {
html:
"" +
'
Warm path
',
},
});
expect(result.isError).toBe(false);
expect(proxied).toBe(true);
});
test("leaves theme-aware content tokens alone", async () => {
const { result, proxied } = await runUiShow({
surface_type: "visual",
data: {
html:
'',
},
});
expect(result.isError).toBe(false);
expect(proxied).toBe(true);
});
test("rejects light ramp text with no dark fill of the same palette", async () => {
const { result, proxied } = await runUiShow({
surface_type: "visual",
data: {
html: '
Quiet label
',
},
});
expect(result.isError).toBe(true);
expect(result.content).toContain("--color-stone-100");
expect(result.content).toContain("invisible in light mode");
expect(proxied).toBe(false);
});
test("does not mistake background-color for a text colour", async () => {
const { result, proxied } = await runUiShow({
surface_type: "visual",
data: {
html:
'
Failed
',
},
});
expect(result.isError).toBe(false);
expect(proxied).toBe(true);
});
test("the app-substitute guard does not fire for visual", async () => {
const html =
'";
const { result, proxied } = await runUiShow({
surface_type: "visual",
title: "Labor Market Stats dashboard",
data: { html },
});
expect(result.isError).toBe(false);
expect(result.content).not.toContain("app-builder");
expect(proxied).toBe(true);
});
});
// ---------------------------------------------------------------------------
// SVG sizing
// ---------------------------------------------------------------------------
describe("an svg that cannot scale to the frame", () => {
test("a root svg with no viewBox names the fix", () => {
const problems = validateVisualHtml(
'',
);
expect(problems.some((p) => p.includes("no viewBox attribute"))).toBe(true);
expect(problems.join(" ")).toContain('viewBox="0 0 W H"');
expect(problems.join(" ")).toContain('width="100%"');
});
test("a viewBox-less svg sized past the frame is flagged for its width", () => {
const problems = validateVisualHtml(
'',
);
const oversized = problems.find((p) => p.includes("sized past"));
expect(oversized).toBeDefined();
expect(oversized).toContain('width="900"');
});
test("a viewBox is enough: the pixel sizes are only read without one", () => {
expect(
validateVisualHtml(
'',
),
).toEqual([]);
});
test("a shape drawn past the viewBox is reported with its overrun", () => {
const problems = validateVisualHtml(
'',
);
expect(problems).toHaveLength(1);
expect(problems[0]).toContain("outside the viewBox");
expect(problems[0]).toContain("ends at 700, past 680");
});
});
describe("coordinate-system guards", () => {
test("a transform attribute is rejected with the absolute-coordinates fix", () => {
const problems = validateVisualHtml(
'',
);
expect(problems).toHaveLength(1);
expect(problems[0]).toContain("transform attribute");
expect(problems[0]).toContain("absolute viewBox coordinate");
});
test("transform-free absolute placement passes", () => {
expect(
validateVisualHtml(
'',
),
).toEqual([]);
});
test("a group-level surface fill that would repaint text is rejected", () => {
const problems = validateVisualHtml(
'',
);
expect(problems).toHaveLength(1);
expect(problems[0]).toContain(".note");
expect(problems[0]).toContain("fill inherits to text");
});
test("a surface fill scoped to the shape passes", () => {
expect(
validateVisualHtml(
'',
),
).toEqual([]);
});
});
// ---------------------------------------------------------------------------
// Skill reference examples
// ---------------------------------------------------------------------------
describe("the visualize skill examples pass the fragment guards", () => {
const skillFile = join(
import.meta.dir,
"../config/bundled-skills/visualize/SKILL.md",
);
const source = readFileSync(skillFile, "utf8");
const examples = [...source.matchAll(/```(\w*)\n([\s\S]*?)```/g)].map(
(match, index) => ({
name: `SKILL.md example ${index + 1} (${match[1] || "text"})`,
body: match[2],
}),
);
test("every fenced example is present", () => {
expect(examples.length).toBe(6);
});
for (const example of examples) {
test(`${example.name} is accepted verbatim`, () => {
expect(validateVisualHtml(example.body)).toEqual([]);
});
}
});
// ---------------------------------------------------------------------------
// Stringified `data`
// ---------------------------------------------------------------------------
describe("ui_show data double-encoded as a JSON string", () => {
test("a string that decodes to an object is still accepted", async () => {
const { result, proxied } = await runUiShow({
surface_type: "visual",
data: JSON.stringify({ html: HTML, height: 320 }),
});
expect(result.isError).toBe(false);
expect(proxied).toBe(true);
});
test("a string that fails to decode names the parse failure, not empty html", async () => {
// The shape the model actually produced: HTML attributes quoted with `"`
// inside a JSON string, escaped wrong.
const { result, proxied } = await runUiShow({
surface_type: "visual",
data: '{"html": "
Hi
"',
});
expect(result.isError).toBe(true);
const content = result.content as string;
expect(content).toContain(
"`data` arrived as a JSON-encoded string that could not be parsed",
);
expect(content).toContain(
"Pass `data` as a JSON object rather than a string",
);
expect(content).toContain("use single quotes for the HTML attributes");
expect(content).toContain(
"Resend the same ui_show with `data` as an object",
);
// The misleading cause the model chased for eight minutes in E2E.
expect(content).not.toContain("non-empty HTML fragment in `data.html`");
expect(proxied).toBe(false);
});
test("the parse error itself is quoted back so the model can locate it", async () => {
const { result } = await runUiShow({
surface_type: "visual",
data: '{"html": "
x
", }',
});
// Whatever the engine's SyntaxError wording, its text is carried through.
let parseMessage = "";
try {
JSON.parse('{"html": "
x
", }');
} catch (error) {
parseMessage = (error as Error).message;
}
expect(parseMessage.length).toBeGreaterThan(0);
expect(result.content).toContain(parseMessage);
});
test("dynamic_page gets the same envelope instead of the empty-html hint", async () => {
const { result, proxied } = await runUiShow({
surface_type: "dynamic_page",
data: '{"html": ""',
});
expect(result.isError).toBe(true);
const content = result.content as string;
expect(content).toContain(
"`data` arrived as a JSON-encoded string that could not be parsed",
);
expect(content).not.toContain("requires non-empty HTML in `data.html`");
expect(proxied).toBe(false);
});
});
// ---------------------------------------------------------------------------
// Daemon emission
// ---------------------------------------------------------------------------
describe("ui_show visual emission", () => {
test("emits a schema-valid inline visual surface and returns its id", async () => {
const sent: AssistantEvent[] = [];
const ctx = makeContext(sent);
const result = await surfaceProxyResolver(ctx, "ui_show", {
surface_type: "visual",
title: "How staging works",
data: { html: HTML, height: 320 },
});
expect(result.isError).toBe(false);
expect(result.yieldToUser).toBeUndefined();
const show = sent.find((msg) => msg.type === "ui_surface_show");
expect(show).toBeDefined();
const parsed = UISurfaceShowEventSchema.parse(show);
expect(parsed.conversationId).toBe("session-1");
expect(parsed.surfaceType).toBe("visual");
expect(parsed.display).toBe("inline");
expect(parsed.title).toBe("How staging works");
expect(parsed.data).toEqual({ html: HTML, height: 320 });
const payload = JSON.parse(result.content) as {
surfaceId: string;
note: string;
};
expect(payload.surfaceId).toBe(parsed.surfaceId);
expect(payload.note).toContain("Continue your response in prose");
expect(payload.note).toContain("ui_dismiss");
expect(ctx.pendingSurfaceActions.has(parsed.surfaceId)).toBe(false);
});
test("lifts a top-level html and height into the surface data", async () => {
const sent: AssistantEvent[] = [];
await surfaceProxyResolver(makeContext(sent), "ui_show", {
surface_type: "visual",
html: HTML,
height: 240,
data: {},
});
const parsed = UISurfaceShowEventSchema.parse(
sent.find((msg) => msg.type === "ui_surface_show"),
);
expect(parsed.data).toEqual({ html: HTML, height: 240 });
});
test("clamps an out-of-range height estimate", async () => {
const sent: AssistantEvent[] = [];
await surfaceProxyResolver(makeContext(sent), "ui_show", {
surface_type: "visual",
data: { html: HTML, height: 99999 },
});
const parsed = UISurfaceShowEventSchema.parse(
sent.find((msg) => msg.type === "ui_surface_show"),
);
expect(parsed.data.height).toBe(1400);
});
test("omits height when the caller supplies none", async () => {
const sent: AssistantEvent[] = [];
await surfaceProxyResolver(makeContext(sent), "ui_show", {
surface_type: "visual",
data: { html: HTML },
});
const parsed = UISurfaceShowEventSchema.parse(
sent.find((msg) => msg.type === "ui_surface_show"),
);
expect(parsed.data).toEqual({ html: HTML });
});
test("a pending visual does not hold the one-interactive-surface lock", async () => {
const sent: AssistantEvent[] = [];
const ctx = makeContext(sent);
ctx.pendingSurfaceActions.set("surface-visual", { surfaceType: "visual" });
const result = await surfaceProxyResolver(ctx, "ui_show", {
surface_type: "choice",
data: { options: [{ id: "a", title: "Option A" }] },
});
expect(result.isError).toBe(false);
expect(result.content).not.toContain("already awaiting user input");
});
});