feat!: la lecture quitte le contrat, un index se lit comme un document
This commit is contained in:
+129
-9
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* `@ng-helpers/indexing` against the REAL broker.
|
||||
*
|
||||
* ── What this suite is for ─────────────────────────────────────────────────
|
||||
* What this suite is for
|
||||
* The unit suite proves the indexing rules are consistent with a fake this repository
|
||||
* wrote. It cannot prove NextGraph behaves the way that fake pretends, because the fake
|
||||
* is the very thing in question. Two claims in particular had never met a broker:
|
||||
@@ -19,7 +19,7 @@
|
||||
* reference — but the document exists. The last journey injects that failure and
|
||||
* asks the broker what was left behind.
|
||||
*
|
||||
* ── Two identities, and how the index reference reaches the second ─────────
|
||||
* Two identities, and how the index reference reaches the second
|
||||
* The whole point of an index is that STRANGERS contribute to it. So Bob must reach
|
||||
* Alice's index — and he must reach it the way an application would, not through a
|
||||
* variable in this file. An index is an ordinary document whose NURI an application
|
||||
@@ -28,7 +28,7 @@
|
||||
* never happen — and does not happen here — is an inbox address crossing the identity
|
||||
* boundary through a channel no deployment has.
|
||||
*
|
||||
* ── Reading a failure ──────────────────────────────────────────────────────
|
||||
* Reading a failure
|
||||
* A named deadline, or a message `ng-e2e-helpers` recognises as a browser or frame
|
||||
* failure, is the HOST. A failed check carrying an unexpected value is this code. The
|
||||
* report says which, and the run is repeated rather than anything being loosened.
|
||||
@@ -72,7 +72,7 @@ type BrowserContext = Awaited<ReturnType<typeof launchWatchedContext>>;
|
||||
type Page = Awaited<ReturnType<typeof newPage>>;
|
||||
type Frame = Awaited<ReturnType<typeof setupBrokerPage>>;
|
||||
|
||||
// ── the domain this suite indexes by ───────────────────────────────────────
|
||||
// the domain this suite indexes by
|
||||
//
|
||||
// A date, so the suite exercises the case the package is built around: an index "by a
|
||||
// date" is just an index whose field is a date predicate, and ISO-8601 sorts as a string.
|
||||
@@ -80,7 +80,7 @@ const PUBLISHED_AT = "urn:ng-helpers-e2e:published-at";
|
||||
/** A predicate an index does NOT curate on — for the object that carries nothing usable. */
|
||||
const UNRELATED = "urn:ng-helpers-e2e:unrelated";
|
||||
|
||||
// ── bounds ─────────────────────────────────────────────────────────────────
|
||||
// bounds
|
||||
//
|
||||
// Sized to be generous rather than tight. A bound exists to turn a hang into a named
|
||||
// failure; sized to the median it would instead fail on a slow-but-healthy broker, which
|
||||
@@ -100,7 +100,7 @@ const JOURNEY_MS = 10 * 60_000;
|
||||
/** The whole run. A budget that cannot interrupt anything is not a budget. */
|
||||
const SUITE_MS = 30 * 60_000;
|
||||
|
||||
// ── the report ─────────────────────────────────────────────────────────────
|
||||
// the report
|
||||
|
||||
let actors: BrowserContext | null = null;
|
||||
|
||||
@@ -138,7 +138,7 @@ const { check, journey, finish } = declareSuite({
|
||||
checks: [
|
||||
"Alice reads exactly one entry, and it is Bob's object",
|
||||
"Bob, who does not own the index, reads the same entry",
|
||||
"curating a second time changes nothing, and the index still holds one entry",
|
||||
"connecting a second time changes nothing, and the index still holds one entry",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -162,15 +162,49 @@ const { check, journey, finish } = declareSuite({
|
||||
"the index holds it as one entry, and its own descriptor is untouched",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "The index answers an ordinary SPARQL query",
|
||||
checks: [
|
||||
"readUnion returns both entries and the index's own declaration",
|
||||
"a SELECT for the entry predicate returns both entries, with their values",
|
||||
"a SELECT of the index's own subject returns the field it declares",
|
||||
"a stranger's SELECT returns the same entries",
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
/**
|
||||
* Two readings of the same entries — the same objects, carrying the same values.
|
||||
*
|
||||
* Compared as a WHOLE: a query that answered with a subset, or with a value that changed
|
||||
* shape crossing the round trip, is not the same answer as the document's own content.
|
||||
*/
|
||||
function sameEntries(a: ReadonlyMap<string, string>, b: ReadonlyMap<string, string>): boolean {
|
||||
if (a.size !== b.size) return false;
|
||||
for (const [object, value] of a) {
|
||||
if (b.get(object) !== value) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** The `?object`/`?value` rows of an entries SELECT, as the entries they claim to be. */
|
||||
function entriesOf(rows: ReadonlyArray<Readonly<Record<string, string>>>): Map<string, string> {
|
||||
const found = new Map<string, string>();
|
||||
for (const row of rows) {
|
||||
const object = row["object"];
|
||||
const value = row["value"];
|
||||
if (object !== undefined && value !== undefined) found.set(object, value);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
/** A named step that is both measured and bounded — `evaluate` carries no timeout of its own. */
|
||||
function step<T>(what: string, ms: number, task: () => Promise<T>): Promise<T> {
|
||||
return measured(what, ms, (bound) => within(what, bound, task));
|
||||
}
|
||||
|
||||
// ── an actor ───────────────────────────────────────────────────────────────
|
||||
// an actor
|
||||
|
||||
interface Actor {
|
||||
readonly id: string;
|
||||
@@ -250,7 +284,7 @@ function actorIsUp(id: string, actor: () => Actor | null): Prerequisite {
|
||||
return () => (actor() === null ? `${id} never signed in` : null);
|
||||
}
|
||||
|
||||
// ── the run ────────────────────────────────────────────────────────────────
|
||||
// the run
|
||||
|
||||
async function main(): Promise<void> {
|
||||
armSuiteDeadline("ng-helpers indexing e2e", SUITE_MS, () =>
|
||||
@@ -602,6 +636,92 @@ async function main(): Promise<void> {
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// AFTER the hostile journey, and read-only: by here the index holds SEVERAL entries,
|
||||
// which is the state the question is about — one entry cannot tell a query that
|
||||
// returns everything from one that returns the first thing it finds.
|
||||
//
|
||||
// WHAT IS BEING ASKED. This package documents one way of reading an index
|
||||
// (`readUnion`) and issues no query of its own, so "an index is an ordinary document
|
||||
// anyone queries normally" has never been anything but plausible. These four checks
|
||||
// are a measurement of that sentence, not a feature: an empty answer is a RESULT and
|
||||
// is reported as one, and a rejection is reported apart from it, because "the query
|
||||
// found nothing" and "the query failed" are the two answers this repository keeps
|
||||
// finding folded into one.
|
||||
await journey({
|
||||
name: "The index answers an ordinary SPARQL query",
|
||||
needs: [aliceIsUp, bobIsUp, indexExists],
|
||||
run: async () => {
|
||||
// The REFERENCE the queries below are judged against. "The SELECT came back with
|
||||
// the entries" is only a claim if something independent says what the entries
|
||||
// are — and it is what makes an empty answer below mean something instead of
|
||||
// being indistinguishable from an index that holds nothing.
|
||||
const raw = await step("Alice reading the index before querying it", BRIDGE_MS, () =>
|
||||
alice!.frame.evaluate((d) => window.__indexing.readRaw(d), index!),
|
||||
);
|
||||
const held = new Map<string, string>();
|
||||
for (const subject of raw) {
|
||||
const value = subject.props[ENTRY_VALUE] ?? [];
|
||||
if (value.length === 1 && value[0] !== undefined) held.set(subject.subject, value[0]);
|
||||
}
|
||||
const declares = (raw.find((s) => s.subject === index)?.props[INDEX_FIELD] ?? []).includes(
|
||||
PUBLISHED_AT,
|
||||
);
|
||||
check(
|
||||
"readUnion returns both entries and the index's own declaration",
|
||||
held.size === 2 && declares,
|
||||
`entries=${JSON.stringify([...held.keys()])} declares=${declares}`,
|
||||
);
|
||||
|
||||
// The shape a reader would write, with nothing of this package in it: the entry
|
||||
// predicate, and the anchored default graph the index document is.
|
||||
const entriesQuery = `SELECT ?object ?value WHERE { ?object <${ENTRY_VALUE}> ?value }`;
|
||||
const mine = await step("Alice querying the index for its entries", BRIDGE_MS, () =>
|
||||
alice!.frame.evaluate(
|
||||
([a, q]) => window.__indexing.select(a!, q!),
|
||||
[index!, entriesQuery],
|
||||
),
|
||||
);
|
||||
const answered = entriesOf(mine.rows);
|
||||
check(
|
||||
"a SELECT for the entry predicate returns both entries, with their values",
|
||||
mine.failed === null && sameEntries(held, answered),
|
||||
`failed=${mine.failed} rows=${mine.rows.length} ` +
|
||||
`objects=${JSON.stringify([...answered.keys()])} raw=${mine.raw}`,
|
||||
);
|
||||
|
||||
// The index document's OWN subject, which is the other half of what an index
|
||||
// holds — and the half a reader needs to know what the values mean.
|
||||
const fieldQuery = `SELECT ?field WHERE { <${index!}> <${INDEX_FIELD}> ?field }`;
|
||||
const declared = await step("Alice querying the index's declaration", BRIDGE_MS, () =>
|
||||
alice!.frame.evaluate(([a, q]) => window.__indexing.select(a!, q!), [index!, fieldQuery]),
|
||||
);
|
||||
check(
|
||||
"a SELECT of the index's own subject returns the field it declares",
|
||||
declared.failed === null &&
|
||||
declared.rows.length === 1 &&
|
||||
declared.rows[0]?.["field"] === PUBLISHED_AT,
|
||||
`failed=${declared.failed} rows=${declared.rows.length} raw=${declared.raw}`,
|
||||
);
|
||||
|
||||
// The reader who matters: an index exists to be read by people who own neither it
|
||||
// nor anything in it. `readUnion` already answers him (the journey above); whether
|
||||
// a query does is a separate question, and it is the one an application asks.
|
||||
const theirs = await step("Bob querying the index he does not own", BRIDGE_MS, () =>
|
||||
bob!.frame.evaluate(
|
||||
([a, q]) => window.__indexing.select(a!, q!),
|
||||
[index!, entriesQuery],
|
||||
),
|
||||
);
|
||||
const strangers = entriesOf(theirs.rows);
|
||||
check(
|
||||
"a stranger's SELECT returns the same entries",
|
||||
theirs.failed === null && sameEntries(held, strangers),
|
||||
`failed=${theirs.failed} rows=${theirs.rows.length} ` +
|
||||
`objects=${JSON.stringify([...strangers.keys()])} raw=${theirs.raw}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
if (ctx !== null) await closeContext("actors", ctx);
|
||||
if (closeServer !== null) {
|
||||
|
||||
Reference in New Issue
Block a user