Files
Ben Stull dca12f4f50 feat(f8): SidecarRouter — keyOf + per-document routing (spec §6.2)
Routes by #24's isUnderRoot: in-workspace file: → CoauthorStore (repo-relative key,
.threads/), out-of-folder file: + untitled: → GlobalSidecarStore (URI-string key).
keyOf is the single document identity. vscode-free; CoauthorStore conforms
structurally (unmodified, INV-2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 13:08:01 -07:00

90 lines
3.9 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 { 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);
});
});