import { expect, test } from "bun:test"; import { indexingOn, type Indexing } from "../src/indexing"; import type { Nuri } from "../src/port"; import { INDEX_FIELD } from "../src/vocabulary"; import { FakeNextGraph, publishObject } from "./fake-nextgraph"; /** * The two acts, and nothing else — this package creates an index and hands one a * reference. What becomes of that reference is the business of the layer that * processes the index's inbox, and there is nothing here that could do it or ask * for it. * * 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; ports: { alice: Port; bob: Port }; } { const network = new FakeNextGraph(); const ports = { alice: network.portFor("alice"), bob: network.portFor("bob") }; // A handle is nothing but a port bound to one identity: building one reaches // nothing, which is why there is nothing to await here. return { network, alice: indexingOn(ports.alice), bob: indexingOn(ports.bob), ports }; } // --- creating an index ---------------------------------------------------- test("any user creates an index in their public store, and it declares its field", async () => { const { alice, network } = world(); const index = await alice.create(PUBLISHED_AT); // An ordinary document: what makes it an index is the field it declares, on the // index's own subject, which is where a reader of the document finds it. expect(network.contentsOf(index)).toEqual([ { subject: index, predicate: INDEX_FIELD, values: [PUBLISHED_AT] }, ]); }); test("a new index is ready to receive: its inbox is open the moment create returns", async () => { const { alice, bob, network } = world(); const index = hardcodedInAppSource(await alice.create(PUBLISHED_AT)); // Nothing to open, register or remember — a stranger deposits straight away. await bob.add(index, "did:ng:o:doc-9"); expect(network.depositsIn(index)).toHaveLength(1); }); test("a field that could never match an object is refused at creation", async () => { const { alice } = world(); // It cannot be corrected later — nothing here deletes — so it is refused now. await expect(alice.create("")).rejects.toThrow(/cannot be changed later/); await expect(alice.create(" ")).rejects.toThrow(/cannot be changed later/); }); test("two indexes are two documents, each declaring its own field", async () => { const { alice, network } = world(); const byDate = await alice.create(PUBLISHED_AT); const byName = await alice.create(NAME); expect(byDate).not.toBe(byName); expect(network.contentsOf(byDate)).toEqual([ { subject: byDate, predicate: INDEX_FIELD, values: [PUBLISHED_AT] }, ]); expect(network.contentsOf(byName)).toEqual([ { subject: byName, predicate: INDEX_FIELD, values: [NAME] }, ]); }); // --- handing an index a reference ----------------------------------------- test("a stranger hands an index a reference, and it waits in the index's inbox", async () => { const { alice, bob, ports, network } = world(); // Alice creates the index and its NURI goes into the application's source. const index = hardcodedInAppSource(await alice.create(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.add(index, article); // THE WHOLE PAYLOAD is the reference: no wrapper, no claim, no copy of the value, // and no index either — the inbox it landed in is what identifies one. This is the // one thing this package hands the layer that will make an entry of it. expect(network.depositsIn(index)).toEqual([ { from: "bob", payload: article, ts: expect.any(Number) }, ]); // And it stayed a deposit: nothing wrote it into the index document. expect(network.contentsOf(index)).toEqual([ { subject: index, predicate: INDEX_FIELD, values: [PUBLISHED_AT] }, ]); }); test("the same reference handed over twice is two deposits, and nothing is lost", async () => { const { alice, bob, ports, network } = world(); const index = hardcodedInAppSource(await alice.create(PUBLISHED_AT)); const article = await publishObject(ports.bob, PUBLISHED_AT, "2026-03-04"); await bob.add(index, article); await bob.add(index, article); // Nothing here de-duplicates: a deposit is an invitation to look, so repeating one // is legitimate and cheap, and whoever processes the inbox is the one that settles. expect(network.depositsIn(index)?.map((d) => d.payload)).toEqual([article, article]); }); 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.add(notAnIndex, "did:ng:o:doc-9")).rejects.toThrow(/has no inbox/); }); test("a payload that is not a reference is refused here, not deposited for someone else to find", async () => { const { alice, bob } = world(); const index = hardcodedInAppSource(await alice.create(PUBLISHED_AT)); // Both sides are checked, because both go on to name a document. An index's inbox // takes anything anyone posts to it; what THIS package puts there is a NURI. await expect(bob.add(index, "please index my article")).rejects.toThrow(/not a NURI/); await expect(bob.add("http://example.org/index", "did:ng:o:doc-9")).rejects.toThrow( /not a NURI/, ); }); // --- only the owner owns it ----------------------------------------------- test("nobody but the owner writes an index, whatever they know about it", async () => { const { alice, ports } = world(); const index = hardcodedInAppSource(await alice.create(PUBLISHED_AT)); await expect( ports.bob.addLiteralProperty(index, "did:ng:o:forged", INDEX_FIELD, "http://schema.org/aaa"), ).rejects.toThrow(/only a document's owner writes to it/); await expect(ports.bob.openInbox(index)).rejects.toThrow(/may not open an inbox/); });