20b709f794
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
110 lines
4.2 KiB
TypeScript
110 lines
4.2 KiB
TypeScript
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));
|
|
});
|
|
|
|
it("update(): two writers merge sections instead of clobbering (F3 §6.2)", () => {
|
|
const store = new CoauthorStore(root);
|
|
store.update("d.md", (a) => {
|
|
a.anchors["a_t"] = { fingerprint: { text: "t", before: "", after: "", lineHint: 0 } };
|
|
a.threads.push({ id: "t1", anchorId: "a_t", status: "open", messages: [] });
|
|
});
|
|
store.update("d.md", (a) => {
|
|
a.anchors["a_at"] = { fingerprint: { text: "x", before: "", after: "", lineHint: 0 } };
|
|
a.attributions.push({
|
|
id: "at1", anchorId: "a_at", author: { kind: "human", id: "ben" },
|
|
createdAt: "2026-06-10T00:00:00.000Z", updatedAt: "2026-06-10T00:00:00.000Z",
|
|
});
|
|
});
|
|
const loaded = store.load("d.md")!;
|
|
expect(loaded.threads).toHaveLength(1);
|
|
expect(loaded.attributions).toHaveLength(1);
|
|
expect(Object.keys(loaded.anchors).sort()).toEqual(["a_at", "a_t"]);
|
|
});
|
|
|
|
it("update() prunes anchors no longer referenced by threads or attributions", () => {
|
|
const store = new CoauthorStore(root);
|
|
store.update("d.md", (a) => {
|
|
a.anchors["a_old"] = { fingerprint: { text: "o", before: "", after: "", lineHint: 0 } };
|
|
a.attributions.push({
|
|
id: "at1", anchorId: "a_old", author: { kind: "human", id: "ben" },
|
|
createdAt: "2026-06-10T00:00:00.000Z", updatedAt: "2026-06-10T00:00:00.000Z",
|
|
});
|
|
});
|
|
store.update("d.md", (a) => {
|
|
a.attributions = [];
|
|
});
|
|
expect(Object.keys(store.load("d.md")!.anchors)).toEqual([]);
|
|
});
|
|
|
|
it("consumeSelfWrite counts multiple writes to the same sidecar (double-save suppression)", () => {
|
|
const store = new CoauthorStore(root);
|
|
store.update("d.md", (a) => {
|
|
a.anchors["a_t"] = { fingerprint: { text: "t", before: "", after: "", lineHint: 0 } };
|
|
a.threads.push({ id: "t1", anchorId: "a_t", status: "open", messages: [] });
|
|
});
|
|
store.update("d.md", (a) => {
|
|
a.attributions.push({
|
|
id: "at1", anchorId: "a_t", author: { kind: "human", id: "ben" },
|
|
createdAt: "2026-06-10T00:00:00.000Z", updatedAt: "2026-06-10T00:00:00.000Z",
|
|
});
|
|
});
|
|
const p = store.sidecarPath("d.md");
|
|
expect(store.consumeSelfWrite(p)).toBe(true);
|
|
expect(store.consumeSelfWrite(p)).toBe(true);
|
|
expect(store.consumeSelfWrite(p)).toBe(false);
|
|
});
|
|
});
|