import { expect, test } from "bun:test"; import { indexing, type Indexing } from "../src/indexing"; import type { Nuri } from "../src/port"; import { ENTRY_VALUE, INDEX_FIELD } from "../src/vocabulary"; import { FakeNextGraph, publishObject } from "./fake-nextgraph"; /** * Each actor gets their own handle, and they share no variable carrying business * data. The ONE value that crosses between them is the index's NURI — and that * crossing is the mechanism this design names: an application references the * index's NURI in its own source. `hardcodedInAppSource` marks every such * crossing, so anything else moving between actors would stand out. */ const PUBLISHED_AT = "http://schema.org/datePublished"; const NAME = "http://schema.org/name"; function hardcodedInAppSource(nuri: Nuri): Nuri { return nuri; } type Port = ReturnType; function world(): { network: FakeNextGraph; alice: Indexing; bob: Indexing; carol: Indexing; ports: { alice: Port; bob: Port; carol: Port }; } { const network = new FakeNextGraph(); const ports = { alice: network.portFor("alice"), bob: network.portFor("bob"), carol: network.portFor("carol"), }; return { network, alice: indexing(ports.alice), bob: indexing(ports.bob), carol: indexing(ports.carol), ports, }; } // --- creating an index ---------------------------------------------------- test("any user creates an index in their public store, and it declares its field", async () => { const { alice, ports } = world(); const index = await alice.createIndex(PUBLISHED_AT); // An ordinary document: what makes it an index is the field it declares, which // a reader going straight to `readUnion` sees on the index's own subject. const subjects = await ports.alice.readDocument(index); const self = subjects.find((s) => s.subject === index); expect(self?.props[INDEX_FIELD]).toEqual([PUBLISHED_AT]); expect(await alice.read(index)).toEqual([]); }); test("reading a document that declares no index field is refused, not answered empty", async () => { const { alice, ports } = world(); const ordinary = await ports.alice.createPublicDocument(); await expect(alice.read(ordinary)).rejects.toThrow(/declares no index field/); }); // --- the whole loop, across three people ---------------------------------- test("a stranger refers an object, the owner curates, and anyone reads the result", async () => { const { alice, bob, carol, ports } = world(); // Alice creates the index and its NURI goes into the application's source. const indexNuri = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT)); // Bob, who owns nothing of Alice's, creates his own public object and hands the // index a reference to it. He needs no permission and gets no write. const article = await publishObject(ports.bob, PUBLISHED_AT, "2026-03-04"); await bob.refer(indexNuri, article); // Nothing is in the index until its owner acts. expect(await carol.read(indexNuri)).toEqual([]); const report = await alice.curate(indexNuri); expect(report.outcomes).toEqual([{ result: "indexed", object: article, value: "2026-03-04" }]); // Carol knows only the NURI from the application's source, and gets the entry. const entries = await carol.read(indexNuri); expect(entries).toEqual([{ object: article, value: "2026-03-04" }]); // The entry is a usable reference: Carol opens the object straight from it, // holding nothing but what she read out of the index. const first = entries[0]; expect(first).toBeDefined(); const opened = await ports.carol.readDocument(first!.object); expect(opened[0]?.props[PUBLISHED_AT]).toEqual(["2026-03-04"]); }); test("an entry is a subject keyed by the object's NURI, so reading needs nothing new", async () => { const { alice, bob, ports } = world(); const indexNuri = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT)); const article = await publishObject(ports.bob, PUBLISHED_AT, "2026-03-04"); await bob.refer(indexNuri, article); await alice.curate(indexNuri); // What `readUnion([indexNuri])` hands an application that never loaded this // package: the index's own subject, plus one subject per indexed object. const subjects = await ports.bob.readDocument(indexNuri); const entry = subjects.find((s) => s.subject === article); expect(entry?.props[ENTRY_VALUE]).toEqual(["2026-03-04"]); expect(subjects.map((s) => s.subject).sort()).toEqual([article, indexNuri].sort()); }); // --- only the owner curates ---------------------------------------------- test("nobody but the index's owner can curate it: the inbox is refused to others", async () => { const { alice, bob, ports } = world(); const indexNuri = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT)); const article = await publishObject(ports.bob, PUBLISHED_AT, "2026-03-04"); await bob.refer(indexNuri, article); await expect(bob.curate(indexNuri)).rejects.toThrow(/may only READ your own/); expect(await alice.read(indexNuri)).toEqual([]); }); test("nobody but the owner writes an index, whatever they know about it", async () => { const { alice, ports } = world(); const indexNuri = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT)); await expect( ports.bob.addLiteralProperty(indexNuri, "did:ng:o:forged", ENTRY_VALUE, "2999-01-01"), ).rejects.toThrow(/only a document's owner writes to it/); await expect(ports.bob.openInbox(indexNuri)).rejects.toThrow(/may not open an inbox/); }); test("an index whose owner never opened an inbox refuses a deposit rather than losing it", async () => { const { bob, ports } = world(); // A public document that was never made into an index: no inbox was opened. const notAnIndex = hardcodedInAppSource(await ports.alice.createPublicDocument()); await expect(bob.refer(notAnIndex, "did:ng:o:doc-9")).rejects.toThrow(/has no inbox/); }); // --- adding is idempotent ------------------------------------------------- test("the same reference deposited twice produces one entry", async () => { const { alice, bob, ports } = world(); const indexNuri = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT)); const article = await publishObject(ports.bob, PUBLISHED_AT, "2026-03-04"); await bob.refer(indexNuri, article); await bob.refer(indexNuri, article); const report = await alice.curate(indexNuri); expect(report.outcomes).toEqual([ { result: "indexed", object: article, value: "2026-03-04" }, { result: "unchanged", object: article }, ]); expect(await alice.read(indexNuri)).toEqual([{ object: article, value: "2026-03-04" }]); }); test("curating twice changes nothing the second time — deposits are not consumed", async () => { const { alice, bob, ports } = world(); const indexNuri = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT)); const article = await publishObject(ports.bob, PUBLISHED_AT, "2026-03-04"); await bob.refer(indexNuri, article); await alice.curate(indexNuri); const before = await alice.read(indexNuri); const second = await alice.curate(indexNuri); expect(second.outcomes).toEqual([{ result: "unchanged", object: article }]); expect(await alice.read(indexNuri)).toEqual(before); }); // --- a read that cannot answer must never cost the index anything --------- test("a reference the broker cannot resolve is reported, and adds nothing", async () => { const { network, alice, bob, ports } = world(); const indexNuri = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT)); const first = await publishObject(ports.bob, PUBLISHED_AT, "2026-01-01"); await bob.refer(indexNuri, first); await alice.curate(indexNuri); const second = await publishObject(ports.bob, PUBLISHED_AT, "2026-02-02"); await bob.refer(indexNuri, second); network.breakReadsOf(second, "broker unreachable"); const report = await alice.curate(indexNuri); const unresolved = report.outcomes.filter((o) => o.result === "unresolved"); expect(unresolved).toHaveLength(1); expect(unresolved[0]).toMatchObject({ object: second }); // THE POINT: the entry that was already there is untouched. expect(await alice.read(indexNuri)).toEqual([{ object: first, value: "2026-01-01" }]); }); test("an already-indexed object survives its own reads failing, and is not even re-read", async () => { const { network, alice, bob, carol, ports } = world(); const indexNuri = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT)); const article = await publishObject(ports.bob, PUBLISHED_AT, "2026-01-01"); await bob.refer(indexNuri, article); await alice.curate(indexNuri); // A passer-by nudges the index about an entry she found IN IT. Carol obtains // the reference the only way she could in a real application — by reading the // index whose NURI her app hardcodes — rather than being handed it by the test. const seen = await carol.read(indexNuri); const noticed = seen[0]; expect(noticed).toBeDefined(); // …and only then does the object become unreachable. network.breakReadsOf(article, "broker unreachable"); await carol.refer(indexNuri, noticed!.object); const report = await alice.curate(indexNuri); expect(report.outcomes.every((o) => o.result === "unchanged")).toBe(true); expect(await alice.read(indexNuri)).toEqual([{ object: article, value: "2026-01-01" }]); }); test("a failed resolve is self-correcting: the next curation adds what it could not", async () => { const { network, alice, bob, ports } = world(); const indexNuri = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT)); const article = await publishObject(ports.bob, PUBLISHED_AT, "2026-05-06"); await bob.refer(indexNuri, article); network.breakReadsOf(article, "broker unreachable"); expect((await alice.curate(indexNuri)).outcomes[0]?.result).toBe("unresolved"); expect(await alice.read(indexNuri)).toEqual([]); // The deposit is still there, so nothing has to be re-deposited. network.healReadsOf(article); expect((await alice.curate(indexNuri)).outcomes[0]).toEqual({ result: "indexed", object: article, value: "2026-05-06", }); expect(await alice.read(indexNuri)).toEqual([{ object: article, value: "2026-05-06" }]); }); test("a reference to something that was never created is reported, not silently dropped", async () => { const { network, alice, bob } = world(); const indexNuri = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT)); await bob.refer(indexNuri, network.neverCreatedNuri()); const report = await alice.curate(indexNuri); expect(report.outcomes).toHaveLength(1); expect(report.outcomes[0]?.result).toBe("unresolved"); expect(await alice.read(indexNuri)).toEqual([]); }); // --- an object that does not fit the index -------------------------------- test("an object carrying nothing for the index's field is not added", async () => { const { alice, bob, ports } = world(); const indexNuri = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT)); // Exists, is public, is readable — but says nothing about the field this index // is built on. OPEN QUESTION: this is the narrow behaviour, not a settled policy. const object = await publishObject(ports.bob, NAME, "an object with no date"); await bob.refer(indexNuri, object); const report = await alice.curate(indexNuri); expect(report.outcomes).toEqual([{ result: "skipped", object, reason: "no-field" }]); expect(await alice.read(indexNuri)).toEqual([]); }); test("an object carrying several values for the field is not added", async () => { const { alice, bob, ports } = world(); const indexNuri = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT)); const object = await publishObject(ports.bob, PUBLISHED_AT, "2026-01-01"); await ports.bob.addLiteralProperty(object, object, PUBLISHED_AT, "2026-09-09"); await bob.refer(indexNuri, object); const report = await alice.curate(indexNuri); expect(report.outcomes).toEqual([{ result: "skipped", object, reason: "several-values" }]); expect(await alice.read(indexNuri)).toEqual([]); }); test("a payload that is not a reference is reported as foreign and changes nothing", async () => { const { alice, bob, ports } = world(); const indexNuri = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT)); const article = await publishObject(ports.bob, PUBLISHED_AT, "2026-03-04"); await bob.refer(indexNuri, article); // Anyone may deposit anything into an inbox, so untrusted payloads do arrive. await ports.bob.depositTo(indexNuri, { drop: "everything" }); const report = await alice.curate(indexNuri); expect(report.outcomes).toEqual([ { result: "indexed", object: article, value: "2026-03-04" }, { result: "foreign", reason: "payload is not a reference" }, ]); expect(await alice.read(indexNuri)).toEqual([{ object: article, value: "2026-03-04" }]); }); test("an index referred to itself is skipped, so its declaration cannot become an entry", async () => { const { alice, bob } = world(); const indexNuri = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT)); await bob.refer(indexNuri, indexNuri); const report = await alice.curate(indexNuri); expect(report.outcomes).toEqual([ { result: "skipped", object: indexNuri, reason: "self-reference" }, ]); expect(await alice.read(indexNuri)).toEqual([]); }); // --- indexing by a date is an instance of indexing by a field ------------- test("an index whose field is a date reads back in chronological order", async () => { const { alice, bob, carol, ports } = world(); const indexNuri = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT)); const march = await publishObject(ports.bob, PUBLISHED_AT, "2026-03-04"); const january = await publishObject(ports.bob, PUBLISHED_AT, "2026-01-31"); const december = await publishObject(ports.bob, PUBLISHED_AT, "2025-12-25"); // Referred out of order, on purpose. await bob.refer(indexNuri, march); await bob.refer(indexNuri, december); await bob.refer(indexNuri, january); await alice.curate(indexNuri); expect((await carol.read(indexNuri)).map((e) => e.value)).toEqual([ "2025-12-25", "2026-01-31", "2026-03-04", ]); }); test("two indexes over the same objects, on different fields, do not interfere", async () => { const { alice, bob, ports } = world(); const byDate = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT)); const byName = hardcodedInAppSource(await alice.createIndex(NAME)); const object = await publishObject(ports.bob, PUBLISHED_AT, "2026-03-04"); await ports.bob.addLiteralProperty(object, object, NAME, "Anemone"); await bob.refer(byDate, object); await bob.refer(byName, object); await alice.curate(byDate); await alice.curate(byName); expect(await alice.read(byDate)).toEqual([{ object, value: "2026-03-04" }]); expect(await alice.read(byName)).toEqual([{ object, value: "Anemone" }]); });