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("\n
snake");
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(``, [
{ old: `<