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 { CoauthorStore } from "../src/store"; import { GlobalSidecarStore } from "../src/globalSidecarStore"; import { SidecarRouter, docIdentity } from "../src/sidecarRouter"; import { emptyArtifact } from "../src/model"; let root: string; let globalDir: string; beforeEach(() => { root = fs.mkdtempSync(path.join(os.tmpdir(), "router-root-")); globalDir = fs.mkdtempSync(path.join(os.tmpdir(), "router-global-")); }); afterEach(() => { fs.rmSync(root, { recursive: true, force: true }); fs.rmSync(globalDir, { recursive: true, force: true }); }); function makeRouter(withRoot = true): SidecarRouter { return new SidecarRouter( withRoot ? new CoauthorStore(root) : null, new GlobalSidecarStore(globalDir), withRoot ? root : undefined, ); } // minimal duck-typed stand-in for a vscode.TextDocument's identity inputs const docOf = (uriString: string, fsPath: string, scheme: string) => ({ uri: { toString: () => uriString, fsPath, scheme }, }); const hash = (uri: string) => createHash("sha256").update(uri).digest("hex"); describe("SidecarRouter.keyOf — one identity per document", () => { it("an in-root file: doc → its repo-relative path", () => { const fsPath = path.join(root, "docs", "x.md"); const key = makeRouter().keyOf(docIdentity(docOf(`file://${fsPath}`, fsPath, "file"))); expect(key).toBe(path.join("docs", "x.md")); }); it("an out-of-root file: doc → its URI string", () => { const fsPath = "/elsewhere/y.md"; const uri = `file://${fsPath}`; expect(makeRouter().keyOf(docIdentity(docOf(uri, fsPath, "file")))).toBe(uri); }); it("an untitled: doc → its URI string", () => { expect(makeRouter().keyOf(docIdentity(docOf("untitled:Untitled-1", "Untitled-1", "untitled")))).toBe( "untitled:Untitled-1", ); }); it("root undefined (no folder) → everything is the URI string", () => { const fsPath = "/anything/z.md"; const uri = `file://${fsPath}`; expect(makeRouter(false).keyOf(docIdentity(docOf(uri, fsPath, "file")))).toBe(uri); }); }); describe("SidecarRouter routing — load/save/update dispatch by key home", () => { it("an in-root key writes to the repo .threads/ sidecar, not global storage", () => { const router = makeRouter(); const key = path.join("docs", "x.md"); router.save(key, emptyArtifact(key)); expect(fs.existsSync(path.join(root, ".threads", "docs", "x.md.json"))).toBe(true); expect(fs.existsSync(path.join(globalDir, "sidecars"))).toBe(false); expect(router.sidecarPath(key)).toBe(path.join(root, ".threads", "docs", "x.md.json")); }); it("an out-of-root file: key writes to global storage, not the repo", () => { const router = makeRouter(); const key = "file:///elsewhere/y.md"; router.save(key, emptyArtifact(key)); expect(fs.existsSync(path.join(globalDir, "sidecars", `${hash(key)}.json`))).toBe(true); expect(fs.existsSync(path.join(root, ".threads"))).toBe(false); expect(router.sidecarPath(key)).toBe(path.join(globalDir, "sidecars", `${hash(key)}.json`)); }); it("an untitled: key is in-memory (global store), no disk anywhere", () => { const router = makeRouter(); const key = "untitled:Untitled-1"; router.update(key, (a) => a.threads.push({ id: "t", anchorId: "an", status: "open", messages: [] })); expect(router.load(key)!.threads.length).toBe(1); expect(router.sidecarPath(key)).toBeUndefined(); expect(fs.existsSync(path.join(globalDir, "sidecars"))).toBe(false); }); it("round-trips through load after update on an in-root key", () => { const router = makeRouter(); const key = path.join("docs", "x.md"); router.update(key, (a) => a.threads.push({ id: "t", anchorId: "an", status: "open", messages: [] })); expect(router.load(key)!.threads.length).toBe(1); }); });