/** * The wall every SVG passes, the reply read back, and the palette swap. * The wall is the one place a model's output meets the page, so it is * tested by what gets through: nothing that runs, loads or links. */ import { expect, test } from "bun:test"; import { claudeEnv, drawArgs, resultText } from "./claude.ts"; import { claudeDrawer } from "./draw.ts"; import { ask, GLOW, INK, inPalette, nameOf, PALETTES, parseReply, sanitize } from "./glyph.ts"; const ok = `Hex Chip`; test("a clean SVG comes through as it was", () => { expect(sanitize(ok)).toBe(ok); expect(nameOf(ok)).toBe("Hex Chip"); }); test("nothing that runs, loads or links gets through", () => { const evil = `
hi
<script>alert(3)</script>
`; const clean = sanitize(evil)!; expect(clean).toBeDefined(); for (const bad of ["script", "onload", "onclick", "onmouseover", "foreignObject", "image", "href", "javascript", "style", "url(", "evil", "expression", "`); expect(clean).toContain(``); expect(clean).not.toContain(" { const svg = sanitize(``)!; expect(svg).toContain(``); }); test("a title's text is text, and markup in values is refused", () => { const svg = sanitize(`A & B bold`)!; // An element inside a title goes with its content; the entity stays an entity. expect(svg).toContain("A & B "); expect(sanitize(`R&D`)).toContain("R&D"); expect(svg).toContain(``); }); test("no SVG, an empty one, a second root or a huge one is not kept", () => { expect(sanitize("just words")).toBeUndefined(); expect(sanitize(`Nothing`)).toBeUndefined(); expect(sanitize(``)).toBeUndefined(); // no viewBox expect(sanitize(``)).toBeUndefined(); const huge = `${``.repeat(2000)}`; expect(sanitize(huge)).toBeUndefined(); // Unclosed tags are closed. expect(sanitize(``)).toBe(``); }); test("the reply's fenced blocks are read, bad ones dropped, at most four kept", () => { const reply = ["```svg", ok, "```", "```svg", "nope", "```", ...Array(5).fill(["```svg", ok, "```"]).flat()].join("\n"); expect(parseReply(reply)).toEqual([ok, ok, ok, ok]); // No fences: the bare SVGs in the text. expect(parseReply(`Here: ${ok} and ${ok}`)).toEqual([ok, ok]); expect(parseReply("I can't draw that.")).toEqual([]); }); test("a palette swaps ink and glow, even when one palette's ink is another's placeholder", () => { const neon = inPalette(ok, "neon"); expect(neon).toContain(`stroke="${PALETTES.neon!.ink}"`); expect(neon).toContain(`fill="${PALETTES.neon!.glow}"`); expect(neon).not.toContain(INK); expect(inPalette(ok, "sage")).toBe(ok); expect(inPalette(ok.toUpperCase().replace(" { const text = ask("a fox", "badge"); for (const part of ["a fox", "badge", INK, GLOW, "4 distinct", "viewBox=\"0 0 64 64\""]) expect([part, text.includes(part)]).toEqual([part, true]); }); test("Claude is started with no tools, and with nothing of ours but its token and the way out", () => { const args = drawArgs(); expect(args.slice(args.indexOf("--tools"), args.indexOf("--tools") + 2)).toEqual(["--tools", ""]); expect(args).toContain("--no-session-persistence"); const env = claudeEnv("tok", "/data/claude", { PATH: "/bin", HTTPS_PROXY: "http://127.0.0.1:3128", FORGE_APP_IDENTITY_KEY: "secret", SOMETHING_ELSE: "x", }); expect(env).toMatchObject({ CLAUDE_CODE_OAUTH_TOKEN: "tok", CLAUDE_CONFIG_DIR: "/data/claude", HTTPS_PROXY: "http://127.0.0.1:3128" }); expect(env.FORGE_APP_IDENTITY_KEY).toBeUndefined(); expect(env.SOMETHING_ELSE).toBeUndefined(); }); test("Claude's answer is read, and a refusal or junk is a sentence, not a crash", async () => { const answer = JSON.stringify({ type: "result", is_error: false, result: "```svg\n" + ok + "\n```" }); expect(resultText(answer)).toEqual({ ok: true, text: "```svg\n" + ok + "\n```" }); expect(resultText("not json").ok).toBe(false); expect(resultText(JSON.stringify({ is_error: true, result: "Not logged in" }))).toEqual({ ok: false, why: "Claude stopped: Not logged in" }); const seen: string[][] = []; const draw = claudeDrawer(async (args) => { seen.push([...args]); return answer; }); expect(await draw("a fox", "mark")).toEqual([ok]); expect(seen[0]).toEqual(drawArgs()); const empty = claudeDrawer(async () => JSON.stringify({ result: "Sorry, no." })); await expect(empty("a fox", "mark")).rejects.toThrow("no drawing in it passed"); });