/** * 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: "

x

".repeat(8000) }, }); expect(result.isError).toBe(true); expect(result.content).toContain("Simplify"); expect(result.content).toContain("Fix every item above"); expect(proxied).toBe(false); }); test("rejects fragments that reach for external resources", async () => { for (const bad of [ '', '', '', ]) { const { result, proxied } = await runUiShow({ surface_type: "visual", data: { html: bad }, }); expect(result.isError).toBe(true); expect(result.content).toContain("no network access"); expect(proxied).toBe(false); } }); test("rejects hardcoded hex and functional colour literals together", async () => { const { result, proxied } = await runUiShow({ surface_type: "visual", data: { html: '
Hi
', }, }); expect(result.isError).toBe(true); expect(result.content).toContain("#eff6ff"); expect(result.content).toContain("#2563eb"); expect(result.content).toContain("rgba(255, 255, 255, 0.1)"); expect(proxied).toBe(false); }); test("rejects an invented var() name and enumerates the real vocabulary", async () => { const { result, proxied } = await runUiShow({ surface_type: "visual", data: { html: '
Hi
' }, }); expect(result.isError).toBe(true); expect(result.content).toContain("--color-text-primary"); expect(result.content).toContain("--content-default"); expect(result.content).toContain("--color--<50-950>"); expect(result.content).toContain( "--color--<100-950>", ); expect(proxied).toBe(false); }); test("does not mistake SVG references, anchors, or entities for colours", async () => { const { result, proxied } = await runUiShow({ surface_type: "visual", data: { html: 't' + '' + '' + '' + 'Jump ↗', }, }); expect(result.isError).toBe(false); expect(proxied).toBe(true); }); test("accepts custom properties the fragment declares itself", async () => { const { result, proxied } = await runUiShow({ surface_type: "visual", data: { html: "" + '
Hi
', }, }); 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: 'LookupWalk' + '' + '' + '' + 'key' + 'hash' + 'found!' + 'check next' + '
' + "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: 'td' + '' + 'Ingest', }, }); 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: 'td' + "" + '' + 'Worker', }, }); 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: 'td' + "" + 'Gateway' + 'Routes traffic', }, }); 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: 'td' + "" + '' + 'Gateway', }, }); 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: 'td' + "" + 'Gateway', }, }); 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( 'Browser', ); 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( 'Browser', ), ).toEqual([]); }); test("a group-level surface fill that would repaint text is rejected", () => { const problems = validateVisualHtml( 'Caching makes repeat visits fast', ); 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( 'Caching makes repeat visits fast', ), ).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"); }); });