Files
obsidian-foundry-sync/tests/resolver.test.ts
2026-06-20 19:38:51 +00:00

92 lines
3.9 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { rm, access } from "node:fs/promises";
import { join } from "node:path";
import {
MapNameResolver, nameUuidIndexFromEntries, saveNameUuidIndex, loadNameUuidIndex,
type NameUuidIndex,
} from "../src/resolver.js";
const TMP = "/tmp/test-resolver-out";
describe("nameUuidIndexFromEntries", () => {
it("builds both directions, first occurrence wins", () => {
const idx = nameUuidIndexFromEntries([
{ name: "Fenris", uuid: "JournalEntry.aaa" },
{ name: "Joron", uuid: "JournalEntry.bbb" },
{ name: "Fenris", uuid: "JournalEntry.ccc" }, // dup name -> name keeps first uuid
{ name: "", uuid: "JournalEntry.ddd" }, // empty name -> ignored
]);
expect(idx.nameToUuid["Fenris"]).toBe("JournalEntry.aaa"); // first wins for the name
expect(idx.nameToUuid["Joron"]).toBe("JournalEntry.bbb");
expect(idx.uuidToName["JournalEntry.aaa"]).toBe("Fenris");
expect(idx.uuidToName["JournalEntry.bbb"]).toBe("Joron");
expect(idx.uuidToName["JournalEntry.ccc"]).toBe("Fenris"); // distinct uuid still recorded
expect(idx.nameToUuid[""]).toBeUndefined();
});
});
describe("MapNameResolver", () => {
const idx: NameUuidIndex = {
nameToUuid: { Fenris: "JournalEntry.aaa" },
uuidToName: { "JournalEntry.aaa": "Fenris" },
};
it("resolves name->uuid and uuid->name", () => {
const r = new MapNameResolver(idx);
expect(r.uuidOf("Fenris")).toBe("JournalEntry.aaa");
expect(r.nameOf("JournalEntry.aaa")).toBe("Fenris");
});
it("returns undefined for unknown names/uuids", () => {
const r = new MapNameResolver(idx);
expect(r.uuidOf("Nobody")).toBeUndefined();
expect(r.nameOf("JournalEntry.zzz")).toBeUndefined();
});
it("does not return Object.prototype members for prototype-colliding names", () => {
// A JSON.parse-loaded index carries Object.prototype, so a name like
// "constructor" must NOT resolve to Object.prototype.constructor (a truthy
// function) — that would corrupt pushed Foundry @UUID links. Regression
// guard for the Map.get → record-access switch.
const r = new MapNameResolver(JSON.parse('{"nameToUuid":{"Fenris":"JournalEntry.aaa"},"uuidToName":{"JournalEntry.aaa":"Fenris"}}') as NameUuidIndex);
expect(r.uuidOf("constructor")).toBeUndefined();
expect(r.uuidOf("toString")).toBeUndefined();
expect(r.uuidOf("hasOwnProperty")).toBeUndefined();
expect(r.nameOf("constructor")).toBeUndefined();
expect(r.uuidOf("Fenris")).toBe("JournalEntry.aaa");
});
it("stores a name literally named __proto__ as an own key", () => {
const idx = nameUuidIndexFromEntries([{ name: "__proto__", uuid: "JournalEntry.zzz" }]);
expect(idx.nameToUuid["__proto__"]).toBe("JournalEntry.zzz");
expect(new MapNameResolver(idx).uuidOf("__proto__")).toBe("JournalEntry.zzz");
});
it("satisfies the NameResolver interface (duck-types like JournalDb)", () => {
const r: { uuidOf(n: string): string | undefined; nameOf(u: string): string | undefined } = new MapNameResolver(idx);
expect(r.uuidOf("Fenris")).toBe("JournalEntry.aaa");
});
});
describe("save/load name-uuid index", () => {
it("round-trips through disk", async () => {
const idx = nameUuidIndexFromEntries([
{ name: "Fenris", uuid: "JournalEntry.aaa" },
{ name: "Joron", uuid: "JournalEntry.bbb" },
]);
const path = join(TMP, "name-uuid.json");
await saveNameUuidIndex(idx, path);
const r = await loadNameUuidIndex(path);
expect(r.uuidOf("Fenris")).toBe("JournalEntry.aaa");
expect(r.nameOf("JournalEntry.bbb")).toBe("Joron");
});
it("saveNameUuidIndex creates the parent dir", async () => {
await rm(TMP, { recursive: true, force: true });
await expect(access(TMP)).rejects.toThrow();
const path = join(TMP, "nested", "name-uuid.json");
await saveNameUuidIndex(nameUuidIndexFromEntries([]), path);
const r = await loadNameUuidIndex(path);
expect(r.uuidOf("x")).toBeUndefined();
});
});