Pure read/write/lookup for the vault sidecar index that will hold page identity (pageId) + collision token (slugId) keyed by file path, so the .md files can be clean markdown. parseVaultIndex is tolerant (missing/garbage/bad entries degrade to empty/skipped — never crashes a cycle); serializeVaultIndex is deterministic (sorted keys -> stable diffs, no churn). Lookups (pageIdAt, pathForPageId reverse, trackedPageIds) + mutations (set/remove/move). NOT wired into pull/push yet — no behavior change. 5 unit tests; engine suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
79 lines
3.0 KiB
TypeScript
79 lines
3.0 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import {
|
|
emptyVaultIndex,
|
|
parseVaultIndex,
|
|
serializeVaultIndex,
|
|
pageIdAt,
|
|
slugIdAt,
|
|
pathForPageId,
|
|
trackedPageIds,
|
|
setEntry,
|
|
removeAt,
|
|
moveEntry,
|
|
} from "../src/engine/vault-index";
|
|
|
|
describe("vault-index parse/serialize", () => {
|
|
it("round-trips a populated index", () => {
|
|
const idx = emptyVaultIndex("sp1");
|
|
setEntry(idx, "Проект/index.md", { pageId: "p1", slugId: "Ab12" });
|
|
setEntry(idx, "Заметка.md", { pageId: "p2" });
|
|
const text = serializeVaultIndex(idx);
|
|
const back = parseVaultIndex(text);
|
|
expect(back.spaceId).toBe("sp1");
|
|
expect(pageIdAt(back, "Проект/index.md")).toBe("p1");
|
|
expect(slugIdAt(back, "Проект/index.md")).toBe("Ab12");
|
|
expect(pageIdAt(back, "Заметка.md")).toBe("p2");
|
|
expect(slugIdAt(back, "Заметка.md")).toBeUndefined();
|
|
});
|
|
|
|
it("serializes deterministically (sorted keys -> stable diffs)", () => {
|
|
const a = emptyVaultIndex("s");
|
|
setEntry(a, "b.md", { pageId: "2" });
|
|
setEntry(a, "a.md", { pageId: "1" });
|
|
const b = emptyVaultIndex("s");
|
|
setEntry(b, "a.md", { pageId: "1" });
|
|
setEntry(b, "b.md", { pageId: "2" });
|
|
// insertion order differs; serialized output must be identical.
|
|
expect(serializeVaultIndex(a)).toBe(serializeVaultIndex(b));
|
|
// keys are sorted in the output
|
|
expect(serializeVaultIndex(a).indexOf('"a.md"')).toBeLessThan(
|
|
serializeVaultIndex(a).indexOf('"b.md"'),
|
|
);
|
|
});
|
|
|
|
it("is tolerant: null / garbage / bad entries -> empty or skipped", () => {
|
|
expect(parseVaultIndex(null).pages.size).toBe(0);
|
|
expect(parseVaultIndex("").pages.size).toBe(0);
|
|
expect(parseVaultIndex("not json{").pages.size).toBe(0);
|
|
expect(parseVaultIndex("[1,2,3]").pages.size).toBe(0);
|
|
// a page entry missing pageId is skipped, valid ones kept
|
|
const idx = parseVaultIndex(
|
|
JSON.stringify({ version: 1, pages: { "ok.md": { pageId: "p" }, "bad.md": { slugId: "x" } } }),
|
|
);
|
|
expect(idx.pages.size).toBe(1);
|
|
expect(pageIdAt(idx, "ok.md")).toBe("p");
|
|
});
|
|
});
|
|
|
|
describe("vault-index lookups + mutations", () => {
|
|
it("reverse lookup + tracked set", () => {
|
|
const idx = emptyVaultIndex();
|
|
setEntry(idx, "x.md", { pageId: "px" });
|
|
setEntry(idx, "y/index.md", { pageId: "py" });
|
|
expect(pathForPageId(idx, "py")).toBe("y/index.md");
|
|
expect(pathForPageId(idx, "missing")).toBeUndefined();
|
|
expect([...trackedPageIds(idx)].sort()).toEqual(["px", "py"]);
|
|
});
|
|
|
|
it("moveEntry relocates identity; removeAt drops it", () => {
|
|
const idx = emptyVaultIndex();
|
|
setEntry(idx, "Old.md", { pageId: "p", slugId: "s" });
|
|
moveEntry(idx, "Old.md", "New/index.md");
|
|
expect(pageIdAt(idx, "Old.md")).toBeUndefined();
|
|
expect(pageIdAt(idx, "New/index.md")).toBe("p");
|
|
expect(slugIdAt(idx, "New/index.md")).toBe("s"); // identity preserved
|
|
removeAt(idx, "New/index.md");
|
|
expect(pageIdAt(idx, "New/index.md")).toBeUndefined();
|
|
});
|
|
});
|