F2 SLICE-1: artifact schema + CoauthorStore with round-trip tests (#4)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
SCHEMA_VERSION,
|
||||
emptyArtifact,
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { CoauthorStore } from "../src/store";
|
||||
import { emptyArtifact, newId, serializeArtifact, type Artifact } from "../src/model";
|
||||
|
||||
let root: string;
|
||||
|
||||
beforeEach(() => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), "coauthor-store-"));
|
||||
});
|
||||
afterEach(() => {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function withThread(): Artifact {
|
||||
const a = emptyArtifact("docs/spec.md");
|
||||
a.anchors["a1"] = { fingerprint: { text: "hello", before: "", after: "", lineHint: 0 } };
|
||||
a.threads.push({
|
||||
id: "t1",
|
||||
anchorId: "a1",
|
||||
status: "open",
|
||||
messages: [{ id: newId("m"), author: { kind: "human", id: "ben" }, body: "note", createdAt: "2026-06-10T00:00:00.000Z" }],
|
||||
});
|
||||
return a;
|
||||
}
|
||||
|
||||
describe("CoauthorStore", () => {
|
||||
it("returns null for a document with no sidecar", () => {
|
||||
const store = new CoauthorStore(root);
|
||||
expect(store.load("docs/spec.md")).toBeNull();
|
||||
});
|
||||
|
||||
it("computes the sidecar path as .threads/<docPath>.json", () => {
|
||||
const store = new CoauthorStore(root);
|
||||
expect(store.sidecarPath("docs/spec.md")).toBe(path.join(root, ".threads", "docs", "spec.md.json"));
|
||||
});
|
||||
|
||||
it("save then load round-trips the artifact", () => {
|
||||
const store = new CoauthorStore(root);
|
||||
const a = withThread();
|
||||
store.save("docs/spec.md", a);
|
||||
expect(store.load("docs/spec.md")).toEqual(a);
|
||||
});
|
||||
|
||||
it("writes byte-stable, pretty JSON (re-save is identical)", () => {
|
||||
const store = new CoauthorStore(root);
|
||||
const a = withThread();
|
||||
store.save("docs/spec.md", a);
|
||||
const first = fs.readFileSync(store.sidecarPath("docs/spec.md"), "utf8");
|
||||
store.save("docs/spec.md", store.load("docs/spec.md")!);
|
||||
const second = fs.readFileSync(store.sidecarPath("docs/spec.md"), "utf8");
|
||||
expect(second).toBe(first);
|
||||
expect(first).toBe(serializeArtifact(a));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user