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 { createHash } from "node:crypto"; import { GlobalSidecarStore } from "../src/globalSidecarStore"; import { emptyArtifact, SCHEMA_VERSION, type Artifact } from "../src/model"; let dir: string; beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), "global-sidecar-")); }); afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); }); const FILE_URI = "file:///abs/notes/chapter-1.md"; const hash = (uri: string) => createHash("sha256").update(uri).digest("hex"); function withThread(uri: string): Artifact { const a = emptyArtifact(uri); a.anchors["an_1"] = { fingerprint: { text: "hi", before: "", after: "", lineHint: 0 } }; a.threads.push({ id: "t_1", anchorId: "an_1", status: "open", messages: [] }); return a; } describe("GlobalSidecarStore — file: URIs persist to /sidecars/.json", () => { it("returns null for a key with no sidecar", () => { expect(new GlobalSidecarStore(dir).load(FILE_URI)).toBeNull(); }); it("save then load round-trips the artifact at the hashed path", () => { const store = new GlobalSidecarStore(dir); const a = withThread(FILE_URI); store.save(FILE_URI, a); const onDisk = path.join(dir, "sidecars", `${hash(FILE_URI)}.json`); expect(fs.existsSync(onDisk)).toBe(true); expect(store.load(FILE_URI)).toEqual(a); }); it("sidecarPath returns the hashed disk path for a file: URI", () => { const store = new GlobalSidecarStore(dir); expect(store.sidecarPath(FILE_URI)).toBe(path.join(dir, "sidecars", `${hash(FILE_URI)}.json`)); }); it("overwrites in place (newest wins, no history)", () => { const store = new GlobalSidecarStore(dir); store.save(FILE_URI, withThread(FILE_URI)); const empty = emptyArtifact(FILE_URI); store.save(FILE_URI, empty); expect(store.load(FILE_URI)).toEqual(empty); }); it("consumeSelfWrite is a no-op returning false (outside the .threads watcher)", () => { const store = new GlobalSidecarStore(dir); store.save(FILE_URI, withThread(FILE_URI)); expect(store.consumeSelfWrite(store.sidecarPath(FILE_URI)!)).toBe(false); }); it("update applies the mutation, prunes unreferenced anchors, and persists", () => { const store = new GlobalSidecarStore(dir); const result = store.update(FILE_URI, (a) => { a.anchors["orphan"] = { fingerprint: { text: "x", before: "", after: "", lineHint: 0 } }; a.anchors["kept"] = { fingerprint: { text: "y", before: "", after: "", lineHint: 0 } }; a.threads.push({ id: "t", anchorId: "kept", status: "open", messages: [] }); }); expect(Object.keys(result.anchors)).toEqual(["kept"]); // "orphan" pruned expect(store.load(FILE_URI)!.threads.length).toBe(1); }); it("update throws on a newer-major sidecar (INV-16 backstop)", () => { const store = new GlobalSidecarStore(dir); const future = { ...emptyArtifact(FILE_URI), schemaVersion: SCHEMA_VERSION + 1 }; store.save(FILE_URI, future as Artifact); expect(() => store.update(FILE_URI, () => {})).toThrow(/INV-16/); }); }); describe("GlobalSidecarStore — untitled: keys are in-memory only (no disk, lost on reload)", () => { const UNTITLED = "untitled:Untitled-1"; it("save then load round-trips within the same instance (session)", () => { const store = new GlobalSidecarStore(dir); const a = emptyArtifact(UNTITLED); a.threads.push({ id: "t", anchorId: "an", status: "open", messages: [] }); a.anchors["an"] = { fingerprint: { text: "z", before: "", after: "", lineHint: 0 } }; store.save(UNTITLED, a); expect(store.load(UNTITLED)).toEqual(a); }); it("writes NO disk file for an untitled key", () => { const store = new GlobalSidecarStore(dir); store.save(UNTITLED, emptyArtifact(UNTITLED)); expect(store.sidecarPath(UNTITLED)).toBeUndefined(); expect(fs.existsSync(path.join(dir, "sidecars"))).toBe(false); }); it("a fresh instance (simulated reload) has no untitled state", () => { const store = new GlobalSidecarStore(dir); store.save(UNTITLED, emptyArtifact(UNTITLED)); expect(new GlobalSidecarStore(dir).load(UNTITLED)).toBeNull(); }); });