/**
* 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 = ``;
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 = ``;
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(``)!;
// An element inside a title goes with its content; the entity stays an entity.
expect(svg).toContain("A & B ");
expect(sanitize(``)).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(``)).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");
});