feat!: curer n'est plus un appel, c'est ce que fait le traitement de l'inbox
This commit is contained in:
+56
-44
@@ -1,5 +1,6 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { indexing, type Indexing } from "../src/indexing";
|
||||
import { curate } from "../src/curator";
|
||||
import type { Nuri } from "../src/port";
|
||||
import { ENTRY_VALUE, INDEX_FIELD } from "../src/vocabulary";
|
||||
import { FakeNextGraph, publishObject } from "./fake-nextgraph";
|
||||
@@ -20,32 +21,43 @@ function hardcodedInAppSource(nuri: Nuri): Nuri {
|
||||
|
||||
type Port = ReturnType<FakeNextGraph["portFor"]>;
|
||||
|
||||
function world(): {
|
||||
async function world(): Promise<{
|
||||
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"),
|
||||
};
|
||||
// Three connections, none of which owns an index yet: there is nothing to catch up
|
||||
// on and nothing to watch. What each of them does next is what these tests are about.
|
||||
return {
|
||||
network,
|
||||
alice: indexing(ports.alice),
|
||||
bob: indexing(ports.bob),
|
||||
carol: indexing(ports.carol),
|
||||
alice: await indexing(ports.alice),
|
||||
bob: await indexing(ports.bob),
|
||||
carol: await indexing(ports.carol),
|
||||
ports,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* These tests exercise the curation RULES, so they run the curator itself rather
|
||||
* than wait for an inbox notification: what a run makes of a deposit is what is
|
||||
* under test, not when the run happens. `inbox-processing.test.ts` covers the when.
|
||||
*
|
||||
* The deposits below therefore sit in their inbox, told to nobody, which is exactly
|
||||
* the state an owner's next connection finds.
|
||||
*/
|
||||
|
||||
// --- creating an index ----------------------------------------------------
|
||||
|
||||
test("any user creates an index in their public store, and it declares its field", async () => {
|
||||
const { alice, ports } = world();
|
||||
const { alice, ports } = await world();
|
||||
|
||||
const index = await alice.createIndex(PUBLISHED_AT);
|
||||
|
||||
@@ -59,7 +71,7 @@ test("any user creates an index in their public store, and it declares its field
|
||||
});
|
||||
|
||||
test("reading a document that declares no index field is refused, not answered empty", async () => {
|
||||
const { alice, ports } = world();
|
||||
const { alice, ports } = await world();
|
||||
const ordinary = await ports.alice.createPublicDocument();
|
||||
await expect(alice.read(ordinary)).rejects.toThrow(/declares no index field/);
|
||||
});
|
||||
@@ -67,7 +79,7 @@ test("reading a document that declares no index field is refused, not answered e
|
||||
// --- 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();
|
||||
const { alice, bob, carol, ports } = await world();
|
||||
|
||||
// Alice creates the index and its NURI goes into the application's source.
|
||||
const indexNuri = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT));
|
||||
@@ -80,7 +92,7 @@ test("a stranger refers an object, the owner curates, and anyone reads the resul
|
||||
// Nothing is in the index until its owner acts.
|
||||
expect(await carol.read(indexNuri)).toEqual([]);
|
||||
|
||||
const report = await alice.curate(indexNuri);
|
||||
const report = await curate(ports.alice, 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.
|
||||
@@ -96,11 +108,11 @@ test("a stranger refers an object, the owner curates, and anyone reads the resul
|
||||
});
|
||||
|
||||
test("an entry is a subject keyed by the object's NURI, so reading needs nothing new", async () => {
|
||||
const { alice, bob, ports } = world();
|
||||
const { alice, bob, ports } = await 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);
|
||||
await curate(ports.alice, indexNuri);
|
||||
|
||||
// What `readUnion([indexNuri])` hands an application that never loaded this
|
||||
// package: the index's own subject, plus one subject per indexed object.
|
||||
@@ -113,17 +125,17 @@ test("an entry is a subject keyed by the object's NURI, so reading needs nothing
|
||||
// --- 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 { alice, bob, ports } = await 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/);
|
||||
await expect(curate(ports.bob, 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 { alice, ports } = await world();
|
||||
const indexNuri = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT));
|
||||
|
||||
await expect(
|
||||
@@ -133,7 +145,7 @@ test("nobody but the owner writes an index, whatever they know about it", async
|
||||
});
|
||||
|
||||
test("an index whose owner never opened an inbox refuses a deposit rather than losing it", async () => {
|
||||
const { bob, ports } = world();
|
||||
const { bob, ports } = await 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/);
|
||||
@@ -142,14 +154,14 @@ test("an index whose owner never opened an inbox refuses a deposit rather than l
|
||||
// --- adding is idempotent -------------------------------------------------
|
||||
|
||||
test("the same reference deposited twice produces one entry", async () => {
|
||||
const { alice, bob, ports } = world();
|
||||
const { alice, bob, ports } = await 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);
|
||||
const report = await curate(ports.alice, indexNuri);
|
||||
expect(report.outcomes).toEqual([
|
||||
{ result: "indexed", object: article, value: "2026-03-04" },
|
||||
{ result: "unchanged", object: article },
|
||||
@@ -158,15 +170,15 @@ test("the same reference deposited twice produces one entry", async () => {
|
||||
});
|
||||
|
||||
test("curating twice changes nothing the second time — deposits are not consumed", async () => {
|
||||
const { alice, bob, ports } = world();
|
||||
const { alice, bob, ports } = await 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);
|
||||
await curate(ports.alice, indexNuri);
|
||||
const before = await alice.read(indexNuri);
|
||||
|
||||
const second = await alice.curate(indexNuri);
|
||||
const second = await curate(ports.alice, indexNuri);
|
||||
expect(second.outcomes).toEqual([{ result: "unchanged", object: article }]);
|
||||
expect(await alice.read(indexNuri)).toEqual(before);
|
||||
});
|
||||
@@ -174,18 +186,18 @@ test("curating twice changes nothing the second time — deposits are not consum
|
||||
// --- 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 { network, alice, bob, ports } = await 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);
|
||||
await curate(ports.alice, 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 report = await curate(ports.alice, indexNuri);
|
||||
const unresolved = report.outcomes.filter((o) => o.result === "unresolved");
|
||||
expect(unresolved).toHaveLength(1);
|
||||
expect(unresolved[0]).toMatchObject({ object: second });
|
||||
@@ -195,11 +207,11 @@ test("a reference the broker cannot resolve is reported, and adds nothing", asyn
|
||||
});
|
||||
|
||||
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 { network, alice, bob, carol, ports } = await 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);
|
||||
await curate(ports.alice, 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
|
||||
@@ -212,24 +224,24 @@ test("an already-indexed object survives its own reads failing, and is not even
|
||||
network.breakReadsOf(article, "broker unreachable");
|
||||
await carol.refer(indexNuri, noticed!.object);
|
||||
|
||||
const report = await alice.curate(indexNuri);
|
||||
const report = await curate(ports.alice, 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 { network, alice, bob, ports } = await 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 curate(ports.alice, 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({
|
||||
expect((await curate(ports.alice, indexNuri)).outcomes[0]).toEqual({
|
||||
result: "indexed",
|
||||
object: article,
|
||||
value: "2026-05-06",
|
||||
@@ -238,11 +250,11 @@ test("a failed resolve is self-correcting: the next curation adds what it could
|
||||
});
|
||||
|
||||
test("a reference to something that was never created is reported, not silently dropped", async () => {
|
||||
const { network, alice, bob } = world();
|
||||
const { network, alice, bob, ports } = await world();
|
||||
const indexNuri = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT));
|
||||
await bob.refer(indexNuri, network.neverCreatedNuri());
|
||||
|
||||
const report = await alice.curate(indexNuri);
|
||||
const report = await curate(ports.alice, indexNuri);
|
||||
expect(report.outcomes).toHaveLength(1);
|
||||
expect(report.outcomes[0]?.result).toBe("unresolved");
|
||||
expect(await alice.read(indexNuri)).toEqual([]);
|
||||
@@ -251,39 +263,39 @@ test("a reference to something that was never created is reported, not silently
|
||||
// --- 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 { alice, bob, ports } = await 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);
|
||||
const report = await curate(ports.alice, 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 { alice, bob, ports } = await 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);
|
||||
const report = await curate(ports.alice, 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 { alice, bob, ports } = await 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);
|
||||
const report = await curate(ports.alice, indexNuri);
|
||||
expect(report.outcomes).toEqual([
|
||||
{ result: "indexed", object: article, value: "2026-03-04" },
|
||||
{ result: "foreign", reason: "payload is not a reference" },
|
||||
@@ -292,11 +304,11 @@ test("a payload that is not a reference is reported as foreign and changes nothi
|
||||
});
|
||||
|
||||
test("an index referred to itself is skipped, so its declaration cannot become an entry", async () => {
|
||||
const { alice, bob } = world();
|
||||
const { alice, bob, ports } = await world();
|
||||
const indexNuri = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT));
|
||||
await bob.refer(indexNuri, indexNuri);
|
||||
|
||||
const report = await alice.curate(indexNuri);
|
||||
const report = await curate(ports.alice, indexNuri);
|
||||
expect(report.outcomes).toEqual([
|
||||
{ result: "skipped", object: indexNuri, reason: "self-reference" },
|
||||
]);
|
||||
@@ -306,7 +318,7 @@ test("an index referred to itself is skipped, so its declaration cannot become a
|
||||
// --- 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 { alice, bob, carol, ports } = await world();
|
||||
const indexNuri = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT));
|
||||
|
||||
const march = await publishObject(ports.bob, PUBLISHED_AT, "2026-03-04");
|
||||
@@ -317,7 +329,7 @@ test("an index whose field is a date reads back in chronological order", async (
|
||||
await bob.refer(indexNuri, march);
|
||||
await bob.refer(indexNuri, december);
|
||||
await bob.refer(indexNuri, january);
|
||||
await alice.curate(indexNuri);
|
||||
await curate(ports.alice, indexNuri);
|
||||
|
||||
expect((await carol.read(indexNuri)).map((e) => e.value)).toEqual([
|
||||
"2025-12-25",
|
||||
@@ -327,7 +339,7 @@ test("an index whose field is a date reads back in chronological order", async (
|
||||
});
|
||||
|
||||
test("two indexes over the same objects, on different fields, do not interfere", async () => {
|
||||
const { alice, bob, ports } = world();
|
||||
const { alice, bob, ports } = await world();
|
||||
const byDate = hardcodedInAppSource(await alice.createIndex(PUBLISHED_AT));
|
||||
const byName = hardcodedInAppSource(await alice.createIndex(NAME));
|
||||
|
||||
@@ -336,8 +348,8 @@ test("two indexes over the same objects, on different fields, do not interfere",
|
||||
|
||||
await bob.refer(byDate, object);
|
||||
await bob.refer(byName, object);
|
||||
await alice.curate(byDate);
|
||||
await alice.curate(byName);
|
||||
await curate(ports.alice, byDate);
|
||||
await curate(ports.alice, byName);
|
||||
|
||||
expect(await alice.read(byDate)).toEqual([{ object, value: "2026-03-04" }]);
|
||||
expect(await alice.read(byName)).toEqual([{ object, value: "Anemone" }]);
|
||||
|
||||
Reference in New Issue
Block a user