import { describe, expect, it } from "vitest"; import { applyArtifactUpdate, artifactFileName, collectArtifacts, normalizeArtifactKind, splitArtifactSegments, stripArtifacts, } from "./artifacts"; const HTML_ARTIFACT = ` snake `; describe("splitArtifactSegments", () => { it("returns plain text untouched", () => { expect(splitArtifactSegments("hello world")).toEqual([ { type: "text", content: "hello world" }, ]); }); it("extracts a complete artifact with surrounding text", () => { const segments = splitArtifactSegments(`Here you go:\n\n${HTML_ARTIFACT}\n\nEnjoy!`); expect(segments).toHaveLength(3); expect(segments[0]).toEqual({ type: "text", content: "Here you go:\n\n" }); const artifact = segments[1]; if (artifact.type !== "artifact" || artifact.op.kind !== "create") { throw new Error("expected create artifact"); } expect(artifact.op.identifier).toBe("snake-game"); expect(artifact.op.type).toBe("html"); expect(artifact.op.title).toBe("Snake Game"); expect(artifact.op.closed).toBe(true); expect(artifact.op.content).toBe("\nsnake"); expect(segments[2]).toEqual({ type: "text", content: "\n\nEnjoy!" }); }); it("parses attributes regardless of order and normalizes MIME types", () => { const segments = splitArtifactSegments( `code` ); const artifact = segments[0]; if (artifact.type !== "artifact" || artifact.op.kind !== "create") { throw new Error("expected create artifact"); } expect(artifact.op.identifier).toBe("widget"); expect(artifact.op.type).toBe("react"); }); it("keeps an unterminated artifact open while streaming", () => { const segments = splitArtifactSegments( `Intro \n# Heading\nbody text` ); expect(segments[0]).toEqual({ type: "text", content: "Intro " }); const artifact = segments[1]; if (artifact.type !== "artifact" || artifact.op.kind !== "create") { throw new Error("expected create artifact"); } expect(artifact.op.closed).toBe(false); expect(artifact.op.content).toBe("# Heading\nbody text"); }); it("hides a partially streamed opening tag", () => { const segments = splitArtifactSegments(`Sure thing!\n { expect(splitArtifactSegments("Sure thing!\n { const segments = splitArtifactSegments( `content { const segments = splitArtifactSegments( ` const SPEED = 200; const SPEED = 120; scoreboard score board ` ); const artifact = segments[0]; if (artifact.type !== "artifact" || artifact.op.kind !== "update") { throw new Error("expected update artifact"); } expect(artifact.op.identifier).toBe("snake-game"); expect(artifact.op.title).toBe("Faster snake"); expect(artifact.op.pairs).toEqual([ { old: "const SPEED = 200;", new: "const SPEED = 120;" }, { old: "scoreboard", new: "score board" }, ]); expect(artifact.op.closed).toBe(true); }); it("only yields complete pairs while an update streams", () => { const segments = splitArtifactSegments( ` a b partial` ); const artifact = segments[0]; if (artifact.type !== "artifact" || artifact.op.kind !== "update") { throw new Error("expected update artifact"); } expect(artifact.op.pairs).toEqual([{ old: "a", new: "b" }]); expect(artifact.op.closed).toBe(false); }); it("handles multiple artifacts in one message", () => { const segments = splitArtifactSegments(`${HTML_ARTIFACT}\nand\n${HTML_ARTIFACT}`); const kinds = segments.map((s) => s.type); expect(kinds).toEqual(["artifact", "text", "artifact"]); }); }); describe("applyArtifactUpdate", () => { it("replaces the first occurrence of each pair", () => { const result = applyArtifactUpdate("aaa bbb aaa", [{ old: "aaa", new: "xxx" }]); expect(result).toEqual({ content: "xxx bbb aaa", applied: 1, failed: 0 }); }); it("skips pairs that do not match without voiding the rest", () => { const result = applyArtifactUpdate("hello world", [ { old: "nope", new: "x" }, { old: "world", new: "there" }, ]); expect(result).toEqual({ content: "hello there", applied: 1, failed: 1 }); }); it("applies pairs sequentially so later pairs can match earlier output", () => { const result = applyArtifactUpdate("v1", [ { old: "v1", new: "v2" }, { old: "v2", new: "v3" }, ]); expect(result.content).toBe("v3"); }); }); describe("collectArtifacts", () => { const msg = (id: string, content: string, from: "user" | "assistant" = "assistant") => ({ id, from, content, }); it("folds create + update into versions", () => { const registry = collectArtifacts([ msg("m1", `v1 content`), msg("m2", "make it faster", "user"), msg( "m3", `Done! v1v2` ), ]); const artifact = registry.artifacts.get("app"); expect(artifact).toBeDefined(); expect(artifact?.versions).toHaveLength(2); expect(artifact?.versions[0]).toMatchObject({ op: "create", version: 1, content: "v1 content", complete: true, }); expect(artifact?.versions[1]).toMatchObject({ op: "update", version: 2, content: "v2 content", title: "App v2", type: "html", failedPairs: 0, }); expect(registry.byMessageOp.get("m1:0")).toEqual({ identifier: "app", version: 1 }); expect(registry.byMessageOp.get("m3:0")).toEqual({ identifier: "app", version: 2 }); expect(registry.streaming).toBeUndefined(); }); it("treats re-emitting a known identifier as a rewrite", () => { const registry = collectArtifacts([ msg("m1", `one`), msg("m2", `two`), ]); expect(registry.artifacts.get("app")?.versions[1]).toMatchObject({ op: "rewrite", version: 2, content: "two", }); }); it("marks the streaming version while its message is live", () => { const registry = collectArtifacts( [msg("m1", `stream`)], "m1" ); expect(registry.streaming).toEqual({ identifier: "app", version: 1 }); expect(registry.artifacts.get("app")?.versions[0].complete).toBe(false); }); it("finalizes an unclosed artifact whose message is no longer generating", () => { // Aborted/interrupted generations leave the tag unterminated forever; the // version must not read as streaming once nothing can append to it. const registry = collectArtifacts([ msg("m1", `stream`), ]); expect(registry.streaming).toBeUndefined(); expect(registry.artifacts.get("app")?.versions[0]).toMatchObject({ complete: true, interrupted: true, content: "stream", }); }); it("finalizes an unclosed artifact in an earlier message even while another is live", () => { const registry = collectArtifacts( [ msg("m1", `partial`), msg("m2", "continue", "user"), msg("m3", `fresh`), ], "m3" ); expect(registry.artifacts.get("app")?.versions[0]).toMatchObject({ complete: true, interrupted: true, }); expect(registry.artifacts.get("app")?.versions[1].complete).toBe(false); expect(registry.streaming).toEqual({ identifier: "app", version: 2 }); }); it("flags updates that reference unknown artifacts", () => { const registry = collectArtifacts([ msg( "m1", `ab` ), ]); expect(registry.artifacts.size).toBe(0); expect(registry.byMessageOp.get("m1:0")).toEqual({ identifier: "ghost", version: -1 }); }); it("applies streaming update pairs progressively", () => { const registry = collectArtifacts( [ msg("m1", `alpha beta`), msg( "m2", `alphagammabe` ), ], "m2" ); const version = registry.artifacts.get("app")?.versions[1]; expect(version?.content).toBe("gamma beta"); expect(version?.complete).toBe(false); expect(registry.streaming).toEqual({ identifier: "app", version: 2 }); }); it("finalizes an interrupted update op", () => { const registry = collectArtifacts([ msg("m1", `alpha beta`), msg( "m2", `alphagammabe` ), ]); const version = registry.artifacts.get("app")?.versions[1]; expect(version?.content).toBe("gamma beta"); expect(version).toMatchObject({ complete: true, interrupted: true }); expect(registry.streaming).toBeUndefined(); }); it("ignores artifacts in user messages", () => { const registry = collectArtifacts([ msg("m1", `x`, "user"), ]); expect(registry.artifacts.size).toBe(0); }); }); describe("stripArtifacts", () => { it("removes artifact blocks but keeps prose", () => { expect(stripArtifacts(`Here you go:\n\n${HTML_ARTIFACT}\n\nEnjoy!`)).toBe( "Here you go:\n\nEnjoy!" ); }); }); describe("misc helpers", () => { it("normalizes kinds with fallback to code", () => { expect(normalizeArtifactKind("text/html")).toBe("html"); expect(normalizeArtifactKind("REACT")).toBe("react"); expect(normalizeArtifactKind("something-else")).toBe("code"); expect(normalizeArtifactKind(undefined)).toBe("code"); }); it("picks sensible file extensions", () => { const base = { identifier: "my-app", title: "t", content: "", complete: true, op: "create" as const, version: 1, messageId: "m", }; expect(artifactFileName({ ...base, type: "html" })).toBe("my-app.html"); expect(artifactFileName({ ...base, type: "code", language: "python" })).toBe("my-app.py"); expect(artifactFileName({ ...base, type: "code", language: "weird" })).toBe("my-app.txt"); expect(artifactFileName({ ...base, type: "mermaid" })).toBe("my-app.mmd"); }); }); // Regression: Kimi-K2.6 (and others) sometimes double the opening bracket of // every tag they were taught: `<`, `<`. The parser must // consume the whole bracket run (no stray `<` in prose) and still pair up // old/new strings. Seen live in HuggingChat conversation 6a286d5e. describe("doubled-bracket tolerance", () => { it("parses < { const segments = splitArtifactSegments( `<\n\n\n` ); expect(segments[0]).toEqual({ type: "text", content: "" }); const artifact = segments[1]; if (artifact.type !== "artifact" || artifact.op.kind !== "create") { throw new Error("expected create artifact"); } expect(artifact.op.identifier).toBe("blue-button"); expect(artifact.op.content).toBe("\n"); }); it("parses </< pairs and doubled closing tags", () => { const segments = splitArtifactSegments( `< < background: #2563eb; < background: #16a34a;< <` ); const artifact = segments[0]; if (artifact.type !== "artifact" || artifact.op.kind !== "update") { throw new Error("expected update artifact"); } expect(artifact.op.closed).toBe(true); expect(artifact.op.pairs).toEqual([ { old: " background: #2563eb;", new: " background: #16a34a;" }, ]); }); it("applies the green-button edit end to end (three <<-mangled update blocks)", () => { const v1 = ` `; const updates = `< < background: #2563eb; < background: #16a34a; < < box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.3); < box-shadow: 0 4px 6px -1px rgba(22, 163, 74, 0.3); < < background: #1d4ed8; < background: #15803d; `; const registry = collectArtifacts([ { id: "m1", from: "assistant", content: v1 }, { id: "m2", from: "user", content: "make it green" }, { id: "m3", from: "assistant", content: updates }, ]); const versions = registry.artifacts.get("blue-button")?.versions ?? []; expect(versions).toHaveLength(4); expect(versions[3].content).toContain("background: #16a34a;"); expect(versions[3].content).toContain("rgba(22, 163, 74, 0.3)"); expect(versions[3].content).toContain("background: #15803d;"); expect(versions[3].content).not.toContain("#2563eb"); expect(versions.every((v) => !v.failedPairs)).toBe(true); }); it("hides a partially streamed doubled opening tag", () => { expect(splitArtifactSegments("Sure!\n< { const segments = splitArtifactSegments( `<\n<\n` ); const artifact = segments[0]; if (artifact.type !== "artifact" || artifact.op.kind !== "create") { throw new Error("expected create artifact"); } expect(artifact.op.content).toBe(``); }); it("matches old_str quoting raw <<-mangled output against normalized content", () => { const result = applyArtifactUpdate(`\n \n`, [ { old: `<`, new: `` }, ]); expect(result.applied).toBe(1); expect(result.failed).toBe(0); expect(result.content).toContain(`viewBox="0 0 20 20"`); }); }); describe("zero-pair updates are flagged", () => { it("marks a closed update with no parseable pairs as a failed edit", () => { const registry = collectArtifacts([ { id: "m1", from: "assistant", content: `hello`, }, { id: "m2", from: "assistant", content: `garbage without pairs`, }, ]); const v2 = registry.artifacts.get("app")?.versions[1]; expect(v2?.content).toBe("hello"); expect(v2?.failedPairs).toBe(1); }); it("does not flag a still-streaming update with no pairs yet", () => { const registry = collectArtifacts( [ { id: "m1", from: "assistant", content: `hello`, }, { id: "m2", from: "assistant", content: `\nhel`, }, ], "m2" ); const v2 = registry.artifacts.get("app")?.versions[1]; expect(v2?.complete).toBe(false); expect(v2?.failedPairs).toBe(0); }); }); describe("whitespace-tolerant matching", () => { it("applies a pair whose old_str has wrong indentation", () => { const base = `
\n
Ready to focus
\n
`; const result = applyArtifactUpdate(base, [ { old: `
Ready to focus
`, new: `
LET'S GO
`, }, ]); expect(result.applied).toBe(1); expect(result.failed).toBe(0); expect(result.content).toContain("LET'S GO"); expect(result.content).not.toContain("Ready to focus"); }); it("tolerates newline drift inside the needle", () => { const base = `a { color: red;\n font-weight: bold; }`; const result = applyArtifactUpdate(base, [ { old: `color: red; font-weight: bold;`, new: `color: blue;` }, ]); expect(result.applied).toBe(1); expect(result.content).toBe(`a { color: blue; }`); }); it("still fails when the text genuinely is not there", () => { const result = applyArtifactUpdate("hello world", [{ old: "goodbye", new: "x" }]); expect(result).toEqual({ content: "hello world", applied: 0, failed: 1 }); }); it("prefers the exact match over a fuzzy one", () => { const base = "x y\nx y"; const result = applyArtifactUpdate(base, [{ old: "x y", new: "Z" }]); expect(result.content).toBe("x y\nZ"); }); }); describe("think blocks are ignored by the registry", () => { it("does not create versions from artifact tags rehearsed inside ", () => { const registry = collectArtifacts([ { id: "m1", from: "assistant", content: `I'll draft it first: draft ok now for real.real content`, }, ]); const artifact = registry.artifacts.get("app"); expect(artifact?.versions).toHaveLength(1); expect(artifact?.versions[0].content).toBe("real content"); expect(registry.byMessageOp.get("m1:0")).toEqual({ identifier: "app", version: 1 }); }); it("ignores old_str/new_str rehearsed in an unclosed streaming think block", () => { const registry = collectArtifacts([ { id: "m1", from: "assistant", content: `v1`, }, { id: "m2", from: "assistant", content: `maybe v1v2 hmm still thinking`, }, ]); expect(registry.artifacts.get("app")?.versions).toHaveLength(1); expect(registry.streaming).toBeUndefined(); }); });