import assert from "node:assert"; import { describe, it, test, expect } from "vitest"; import { SCHEMA_VERSION, emptyArtifact, isNewerMajor, serializeArtifact, type Artifact, } from "../src/model"; function sampleArtifact(): Artifact { return { schemaVersion: SCHEMA_VERSION, document: { path: "docs/spec.md" }, anchors: { a2: { fingerprint: { text: "beta", before: "x", after: "y", lineHint: 5 } }, a1: { fingerprint: { text: "alpha", before: "", after: "", lineHint: 1 } }, }, threads: [ { id: "t1", anchorId: "a1", status: "open", messages: [ { id: "m1", author: { kind: "human", id: "ben" }, body: "hi", createdAt: "2026-06-10T00:00:00.000Z" }, ], }, ], attributions: [], proposals: [], }; } describe("serializeArtifact", () => { it("round-trips through JSON.parse", () => { const a = sampleArtifact(); const parsed = JSON.parse(serializeArtifact(a)) as Artifact; expect(parsed).toEqual(a); }); it("emits stable, sorted anchor keys and a trailing newline regardless of insertion order", () => { const a = sampleArtifact(); const out = serializeArtifact(a); expect(out.endsWith("\n")).toBe(true); // a1 must serialize before a2 even though a2 was inserted first expect(out.indexOf('"a1"')).toBeLessThan(out.indexOf('"a2"')); // byte-identical on re-serialize expect(serializeArtifact(JSON.parse(out))).toBe(out); }); it("emptyArtifact has the shared F3/F4 extension points present and empty", () => { const e = emptyArtifact("docs/x.md"); expect(e.schemaVersion).toBe(SCHEMA_VERSION); expect(e.document.path).toBe("docs/x.md"); expect(e.anchors).toEqual({}); expect(e.threads).toEqual([]); expect(e.attributions).toEqual([]); expect(e.proposals).toEqual([]); }); }); describe("cross-rung identity (F5 SLICE-2)", () => { it("round-trips email on human provenance, omitted when absent", () => { const a = emptyArtifact("docs/x.md"); a.anchors["a_1"] = { fingerprint: { text: "alpha", before: "", after: "", lineHint: 0 } }; a.threads.push({ id: "t_1", anchorId: "a_1", status: "open", messages: [ { id: "m_1", author: { kind: "human", id: "ben", email: "ben@wiggleverse.org" }, body: "hi", createdAt: "2026-06-10T00:00:00.000Z" }, { id: "m_2", author: { kind: "human", id: "anon" }, body: "yo", createdAt: "2026-06-10T00:00:01.000Z" }, ], }); const out = serializeArtifact(a); const parsed = JSON.parse(out) as Artifact; expect(parsed.threads[0].messages[0].author).toEqual({ kind: "human", id: "ben", email: "ben@wiggleverse.org" }); expect("email" in parsed.threads[0].messages[1].author).toBe(false); expect(serializeArtifact(parsed)).toBe(out); }); it("round-trips agent.onBehalfOf and omits it when absent", () => { const a = emptyArtifact("docs/x.md"); a.attributions.push({ id: "at_1", anchorId: "a_1", author: { kind: "agent", id: "claude", agent: { sdk: "@cline/sdk", model: "sonnet", sessionId: "run-1", onBehalfOf: { id: "benstull", email: "ben@wiggleverse.org" } }, }, createdAt: "2026-06-10T00:00:00.000Z", updatedAt: "2026-06-10T00:00:00.000Z", }); const out = serializeArtifact(a); const parsed = JSON.parse(out) as Artifact; const author = parsed.attributions[0].author; expect(author.kind).toBe("agent"); if (author.kind === "agent") { expect(author.agent.onBehalfOf).toEqual({ id: "benstull", email: "ben@wiggleverse.org" }); } expect(out.includes("onBehalfOf")).toBe(true); const a2 = emptyArtifact("docs/x.md"); expect(serializeArtifact(a2).includes("onBehalfOf")).toBe(false); }); }); describe("isNewerMajor (F5 SLICE-2, INV-16)", () => { it("is false for v1 and true for a newer major", () => { expect(isNewerMajor(emptyArtifact("d.md"))).toBe(false); expect(isNewerMajor({ schemaVersion: 2 })).toBe(true); }); }); describe("unknown-field preservation (F5 SLICE-2, INV-15)", () => { it("a rewrite preserves unknown fields at every level, after known keys, sorted", () => { const foreign = { schemaVersion: 1, document: { path: "docs/x.md", zFuture: "keep-me" }, anchors: { a_1: { fingerprint: { text: "alpha", before: "", after: "", lineHint: 0, confidence: 0.9 }, label: "intro" }, }, threads: [ { id: "t_1", anchorId: "a_1", status: "open", messages: [ { id: "m_1", author: { kind: "human", id: "ben", forgeLogin: "bstull" }, body: "hi", createdAt: "2026-06-10T00:00:00.000Z", reactions: ["+1"], }, ], locked: false, }, ], attributions: [ { id: "at_1", anchorId: "a_1", author: { kind: "human", id: "ben" }, createdAt: "2026-06-10T00:00:00.000Z", updatedAt: "2026-06-10T00:00:00.000Z", reviewState: "approved", }, ], proposals: [ { id: "p_1", anchorId: "a_1", replacement: "ALPHA", author: { kind: "human", id: "ben" }, createdAt: "2026-06-10T00:00:00.000Z", priority: 2, }, ], futureSection: [{ id: "f_1" }], aaaEarly: true, }; const out = serializeArtifact(foreign as unknown as Artifact); const parsed = JSON.parse(out); expect(parsed.futureSection).toEqual([{ id: "f_1" }]); expect(parsed.aaaEarly).toBe(true); expect(parsed.document.zFuture).toBe("keep-me"); expect(parsed.anchors.a_1.label).toBe("intro"); expect(parsed.anchors.a_1.fingerprint.confidence).toBe(0.9); expect(parsed.threads[0].locked).toBe(false); expect(parsed.threads[0].messages[0].reactions).toEqual(["+1"]); expect(parsed.threads[0].messages[0].author.forgeLogin).toBe("bstull"); expect(parsed.attributions[0].reviewState).toBe("approved"); expect(parsed.proposals[0].priority).toBe(2); // unknown ROOT keys serialize AFTER known keys, sorted: aaaEarly then futureSection, both after proposals expect(out.indexOf('"proposals"')).toBeLessThan(out.indexOf('"aaaEarly"')); expect(out.indexOf('"aaaEarly"')).toBeLessThan(out.indexOf('"futureSection"')); // byte-stable on re-serialize (INV-2 holds with unknowns present) expect(serializeArtifact(parsed as Artifact)).toBe(out); }); }); test("serializeArtifact round-trips Proposal.original", () => { const a = emptyArtifact("doc.md"); a.anchors["a_1"] = { fingerprint: { text: "new", before: "", after: "", lineHint: 0 } }; a.proposals.push({ id: "pr_1", anchorId: "a_1", replacement: "new", author: { kind: "agent", id: "claude", agent: { sdk: "@cline/sdk", model: "sonnet", sessionId: "s" } }, createdAt: "2026-06-26T00:00:00.000Z", original: "old", granularity: "block", }); const json = serializeArtifact(a); assert.match(json, /"original": "old"/); assert.match(json, /"granularity": "block"/); }); describe("attributions[] (F3 SLICE-1)", () => { it("round-trips an attribution record through serialize → parse", () => { const a = emptyArtifact("docs/x.md"); a.anchors["a_1"] = { fingerprint: { text: "alpha", before: "", after: " beta", lineHint: 0 } }; a.attributions.push({ id: "at_1", anchorId: "a_1", author: { kind: "agent", id: "claude", agent: { sdk: "@cline/sdk", model: "sonnet", sessionId: "run-1" } }, createdAt: "2026-06-10T00:00:00.000Z", updatedAt: "2026-06-10T00:00:00.000Z", turnId: "turn-1", }); const parsed = JSON.parse(serializeArtifact(a)) as Artifact; expect(parsed.attributions).toEqual(a.attributions); }); it("omits turnId when undefined and serializes stably", () => { const a = emptyArtifact("docs/x.md"); a.attributions.push({ id: "at_2", anchorId: "a_2", author: { kind: "human", id: "ben" }, createdAt: "2026-06-10T00:00:00.000Z", updatedAt: "2026-06-10T00:00:00.000Z", }); const s1 = serializeArtifact(a); expect(s1.includes("turnId")).toBe(false); const s2 = serializeArtifact(JSON.parse(s1) as Artifact); expect(s2).toBe(s1); }); });