feat!: curer n'est plus un appel, c'est ce que fait le traitement de l'inbox

This commit is contained in:
Sylvain Duchesne
2026-08-20 11:04:31 +02:00
parent 2ce2113157
commit e2ed970cbd
21 changed files with 956 additions and 273 deletions
+35 -30
View File
@@ -1,5 +1,6 @@
import { expect, mock, test } from "bun:test";
import { indexing } from "../src/indexing";
import { curate } from "../src/curator";
import { entriesOf, entryValue } from "../src/index-document";
import { resolutionFromFailure, resolutionFromRead } from "../src/resolution";
import type { Nuri, UnionSubject } from "../src/port";
@@ -64,11 +65,11 @@ test("an entry whose value is the empty string is still an entry", () => {
test("adding a second value to an entry cannot make it disappear", async () => {
const network = new FakeNextGraph();
const ownerPort = network.portFor("alice");
const owner = indexing(ownerPort);
const owner = await indexing(ownerPort);
const index = await owner.createIndex(FIELD);
const article = await publishObject(network.portFor("bob"), FIELD, "2026-01-01");
await indexing(network.portFor("bob")).refer(index, article);
await owner.curate(index);
await (await indexing(network.portFor("bob"))).refer(index, article);
await curate(ownerPort, index);
// A pure ADD — the only write this package has. Before the fix this emptied
// `read()` while both triples sat in the document.
@@ -80,12 +81,12 @@ test("adding a second value to an entry cannot make it disappear", async () => {
test("a raced double-add settles, and does not make every later run re-add", async () => {
const network = new FakeNextGraph();
const ownerPort = network.portFor("alice");
const owner = indexing(ownerPort);
const owner = await indexing(ownerPort);
const index = await owner.createIndex(FIELD);
const bobPort = network.portFor("bob");
const article = await publishObject(bobPort, FIELD, "2026-01-01");
await indexing(bobPort).refer(index, article);
await owner.curate(index);
await (await indexing(bobPort)).refer(index, article);
await curate(ownerPort, index);
// What two curation runs racing each other leave behind: the object's owner
// edited it between their reads, so each added its own value.
@@ -94,7 +95,7 @@ test("a raced double-add settles, and does not make every later run re-add", asy
// The entry is still there, and the curator recognises it as already indexed —
// before the fix it was invisible, so every run added yet another value.
const report = await owner.curate(index);
const report = await curate(ownerPort, index);
expect(report.outcomes).toEqual([{ result: "unchanged", object: article }]);
expect(await owner.read(index)).toEqual([{ object: article, value: "2026-01-01" }]);
});
@@ -104,13 +105,13 @@ test("a raced double-add settles, and does not make every later run re-add", asy
test("a second declared field stops curation LOUDLY and costs no entry", async () => {
const network = new FakeNextGraph();
const ownerPort = network.portFor("alice");
const owner = indexing(ownerPort);
const owner = await indexing(ownerPort);
const bobPort = network.portFor("bob");
const index = await owner.createIndex(FIELD);
for (const date of ["2026-01-01", "2026-02-02", "2026-03-03"]) {
await indexing(bobPort).refer(index, await publishObject(bobPort, FIELD, date));
await (await indexing(bobPort)).refer(index, await publishObject(bobPort, FIELD, date));
}
await owner.curate(index);
await curate(ownerPort, index);
expect(await owner.read(index)).toHaveLength(3);
// One add-only write through the published surface — and the SMALLER string, the
@@ -121,28 +122,28 @@ test("a second declared field stops curation LOUDLY and costs no entry", async (
// unreadable because the declaration above it turned ambiguous.
expect(await owner.read(index)).toHaveLength(3);
// Curating refuses, and says why instead of quietly picking one.
await expect(owner.curate(index)).rejects.toThrow(/declares 2 index fields/);
await expect(curate(ownerPort, index)).rejects.toThrow(/declares 2 index fields/);
});
test("a mixed-field index is never produced: curation refuses before adding anything", async () => {
const network = new FakeNextGraph();
const ownerPort = network.portFor("alice");
const owner = indexing(ownerPort);
const owner = await indexing(ownerPort);
const bobPort = network.portFor("bob");
const NAME = "http://schema.org/name";
const index = await owner.createIndex(NAME);
const first = await publishObject(bobPort, NAME, "Anemone");
await indexing(bobPort).refer(index, first);
await owner.curate(index);
await (await indexing(bobPort)).refer(index, first);
await curate(ownerPort, index);
// "…/datePublished" < "…/name", so under "smallest wins" the new field took over
// while `first` kept its old value forever — one list ordered by two properties.
await ownerPort.addLiteralProperty(index, index, INDEX_FIELD, FIELD);
const second = await publishObject(bobPort, FIELD, "2026-02-02");
await indexing(bobPort).refer(index, second);
await (await indexing(bobPort)).refer(index, second);
await expect(owner.curate(index)).rejects.toThrow(/refusing to curate rather than pick one/);
await expect(curate(ownerPort, index)).rejects.toThrow(/refusing to curate rather than pick one/);
expect(await owner.read(index)).toEqual([{ object: first, value: "Anemone" }]);
});
@@ -150,11 +151,11 @@ test("an index declaring no field at all is still refused", async () => {
const network = new FakeNextGraph();
const ownerPort = network.portFor("alice");
const ordinary = await ownerPort.createPublicDocument();
await expect(indexing(ownerPort).read(ordinary)).rejects.toThrow(/declares no index field/);
await expect((await indexing(ownerPort)).read(ordinary)).rejects.toThrow(/declares no index field/);
});
test("a field that could never match an object is refused at creation", async () => {
const owner = indexing(new FakeNextGraph().portFor("alice"));
const owner = await indexing(new FakeNextGraph().portFor("alice"));
// It cannot be corrected later — nothing here deletes — so it is refused now.
await expect(owner.createIndex("")).rejects.toThrow(/cannot be changed later/);
await expect(owner.createIndex(" ")).rejects.toThrow(/cannot be changed later/);
@@ -164,7 +165,8 @@ test("a field that could never match an object is refused at creation", async ()
test("a field colliding with Object.prototype neither crashes nor is silently mis-read", async () => {
const network = new FakeNextGraph();
const owner = indexing(network.portFor("alice"));
const ownerPort = network.portFor("alice");
const owner = await indexing(ownerPort);
const bobPort = network.portFor("bob");
for (const field of ["constructor", "toString", "valueOf", "hasOwnProperty"]) {
@@ -178,10 +180,10 @@ test("a field colliding with Object.prototype neither crashes nor is silently mi
// An object that merely LACKS it must still resolve cleanly: reading the field
// off a plain object literal would otherwise hand back an inherited function.
const lacks = await publishObject(bobPort, "http://schema.org/name", "unrelated");
await indexing(bobPort).refer(index, lacks);
await indexing(bobPort).refer(index, carries);
await (await indexing(bobPort)).refer(index, lacks);
await (await indexing(bobPort)).refer(index, carries);
const report = await owner.curate(index);
const report = await curate(ownerPort, index);
expect(report.outcomes[0]).toEqual({ result: "skipped", object: lacks, reason: "no-field" });
expect(report.outcomes[1]?.result).toBe("unresolved");
expect(await owner.read(index)).toEqual([]);
@@ -212,17 +214,18 @@ test("a read that threw resolves as unresolved, naming the error", () => {
test("an unresolved reference is warned about, not only reported", async () => {
const network = new FakeNextGraph();
const owner = indexing(network.portFor("alice"));
const ownerPort = network.portFor("alice");
const owner = await indexing(ownerPort);
const index = await owner.createIndex(FIELD);
const article = await publishObject(network.portFor("bob"), FIELD, "2026-01-01");
await indexing(network.portFor("bob")).refer(index, article);
await (await indexing(network.portFor("bob"))).refer(index, article);
network.breakReadsOf(article, "broker unreachable");
const warn = mock((..._args: unknown[]) => {});
const original = console.warn;
console.warn = warn;
try {
await owner.curate(index);
await curate(ownerPort, index);
} finally {
console.warn = original;
}
@@ -233,17 +236,18 @@ test("an unresolved reference is warned about, not only reported", async () => {
test("a normal run warns about nothing", async () => {
const network = new FakeNextGraph();
const owner = indexing(network.portFor("alice"));
const ownerPort = network.portFor("alice");
const owner = await indexing(ownerPort);
const index = await owner.createIndex(FIELD);
const article = await publishObject(network.portFor("bob"), FIELD, "2026-01-01");
await indexing(network.portFor("bob")).refer(index, article);
await (await indexing(network.portFor("bob"))).refer(index, article);
const warn = mock((..._args: unknown[]) => {});
const original = console.warn;
console.warn = warn;
let report;
try {
report = await owner.curate(index);
report = await curate(ownerPort, index);
} finally {
console.warn = original;
}
@@ -257,13 +261,14 @@ test("a normal run warns about nothing", async () => {
test("an index that could not be read is refused, and says so without blaming the document", async () => {
const network = new FakeNextGraph();
const owner = indexing(network.portFor("alice"));
const ownerPort = network.portFor("alice");
const owner = await indexing(ownerPort);
const index = await owner.createIndex(FIELD);
network.breakReadsOf(index, "broker unreachable");
// The real `readUnion` turns a failed read into `[]`, so the failure arrives
// looking like a blank document. Whatever the shape, nothing may be written.
await expect(owner.curate(index)).rejects.toThrow();
await expect(curate(ownerPort, index)).rejects.toThrow();
await expect(owner.read(index)).rejects.toThrow();
network.healReadsOf(index);