fix: quatre écarts entre la surface publiée et ce que NextGraph déclare
Un audit de la surface contre la source amont en a trouvé cinq ; voici les
quatre mécaniques. La cinquième — l'adresse d'inbox, qui traverse sept symboles
— relève du dessin et reste ouverte.
L'identifiant de session bloquait. Amont le déclare string | number
(sdk/js/web/src/index.ts:16) et le binding désérialise un u64 ; nous exigions
une chaîne. Une application ne pouvait donc pas passer la valeur que le SDK
venait de lui remettre. Élargi à ce qu'amont déclare, sur toute la chaîne, et
jamais converti : une chaîne échoue pour de vrai (Deserialization error of
session_id JsValue("1"), observé).
sparqlUpdate annonçait Promise<void> alors qu'il relayait DÉJÀ les commits.
C'était donc un mensonge de typage, pas un comportement — et la doublure de test
qui rendait undefined, un état que le vrai broker ne produit jamais, est ce qui
l'a laissé sans contradicteur.
ng était publié en Record<string, any>, ce qui perdait les 88 membres typés
d'amont — 88, pas 77 : le chiffre de notre propre documentation était faux.
Et materialize, second nom publié de read, sans appelant ni contrepartie amont,
est retiré.
docs/api-contract.md qualifiait docs.* de passthrough « 1:1 ». C'était faux sur
les deux premiers points. Corrigé, pas complété : un document qui se déclare
vérifié et qui ment est pire qu'un document absent, parce qu'on cesse d'aller
voir.
Une déviation assumée : amont type le retour en any, interdit ici ; on rend
unknown, comme sparqlQuery le fait déjà pour le même any amont.
This commit is contained in:
@@ -29,10 +29,16 @@ import { configure } from "../src/index";
|
||||
import { setCurrentUser } from "../src/shared-wallet/bootstrap";
|
||||
import { resetCaps } from "../src/shared-wallet/bootstrap";
|
||||
|
||||
// What a real `ng.sparql_update` answers: the COMMITS the update produced
|
||||
// (`sdk/js/lib-wasm/src/lib.rs:481-483` serialises `AppResponseV0::Commits`). The fake
|
||||
// used to answer `undefined` — a result the broker never returns — which is precisely
|
||||
// what let the surface declare `Promise<void>` unchallenged.
|
||||
const COMMITS = [{ id: "did:ng:c:commit-1" }, { id: "did:ng:c:commit-2" }];
|
||||
|
||||
function fakeNg() {
|
||||
return {
|
||||
doc_create: mock(async (..._a: unknown[]) => "did:ng:o:new-doc"),
|
||||
sparql_update: mock(async (..._a: unknown[]) => undefined),
|
||||
sparql_update: mock(async (..._a: unknown[]) => COMMITS),
|
||||
sparql_query: mock(async (..._a: unknown[]) => ({ results: { bindings: [] } })),
|
||||
// A sentinel: makeNg(), if ever used, would `.bind` and call THIS through
|
||||
// the JS Proxy. We assert the primitives call the raw fns above directly.
|
||||
@@ -83,6 +89,41 @@ test("sparqlQuery forwards (sessionId, query, base, anchor) and returns the raw
|
||||
]);
|
||||
});
|
||||
|
||||
// ── the session id: RELAYED, never converted ──────────────────────────────────
|
||||
// Upstream declares it `string | number` (`Session.session_id`, `index.d.ts:266`) and the
|
||||
// broker hands back a NUMBER, which the wasm binding deserializes as a `u64`. Stringifying
|
||||
// it fails that deserialization for real (`Deserialization error of session_id JsValue("1")`),
|
||||
// so what reaches the boundary must be the very value the caller passed — same `typeof`.
|
||||
test("a NUMERIC session id reaches ng untouched, still a number", async () => {
|
||||
const ng = inject();
|
||||
// Anchor the update and the read on the document just CREATED: creating it mints its
|
||||
// cap, so the reach guard (process-wide once any cap exists) is satisfied the way a real
|
||||
// application satisfies it — rather than by naming a document this user does not hold.
|
||||
const created = await docCreate(1, "Graph", "data:graph", "store", undefined);
|
||||
await sparqlUpdate(2, "INSERT DATA {}", created);
|
||||
await sparqlQuery(3, "SELECT * {}", undefined, created);
|
||||
|
||||
const createdSid = ng.doc_create.mock.calls[0]![0];
|
||||
expect(createdSid).toBe(1);
|
||||
expect(typeof createdSid).toBe("number");
|
||||
|
||||
const updated = ng.sparql_update.mock.calls[0]![0];
|
||||
expect(updated).toBe(2);
|
||||
expect(typeof updated).toBe("number");
|
||||
|
||||
const queried = ng.sparql_query.mock.calls[0]![0];
|
||||
expect(queried).toBe(3);
|
||||
expect(typeof queried).toBe("number");
|
||||
});
|
||||
|
||||
test("sparqlUpdate hands back what the boundary returned", async () => {
|
||||
// The commits must arrive at the caller unchanged — the surface used to declare
|
||||
// `Promise<void>` and throw this answer away.
|
||||
inject();
|
||||
const returned = await sparqlUpdate("sid-c", "INSERT DATA {}", "did:ng:o:a");
|
||||
expect(returned).toBe(COMMITS);
|
||||
});
|
||||
|
||||
test("the primitives do NOT route through the public ng proxy (makeNg)", async () => {
|
||||
// makeNg builds a JS Proxy over the injected ng. If a primitive went through
|
||||
// it, calls would land on the proxy's `get` trap, not on our raw mock fns.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { test, expect, mock, beforeEach, afterAll } from "bun:test";
|
||||
import { post, read, materialize, watch } from "../src/surface/inbox";
|
||||
import { post, read, watch } from "../src/surface/inbox";
|
||||
import * as polyfill from "../src/index";
|
||||
import { userInbox, resetRegistryCache } from "../src/shared-wallet/account-registry";
|
||||
import type { Deposit } from "../src/surface/inbox";
|
||||
import { configure } from "../src/index";
|
||||
@@ -249,14 +250,23 @@ test("from: null makes an anonymous deposit even when a current user is set", as
|
||||
expect(deposits[0]!.from).toBeNull();
|
||||
});
|
||||
|
||||
test("read returns deposits sorted by ts ascending and materialize is an alias", async () => {
|
||||
test("read returns deposits sorted by ts ascending", async () => {
|
||||
await post(TARGET, { from: null, payload: "second", ts: 300 });
|
||||
await post(TARGET, { from: null, payload: "first", ts: 100 });
|
||||
await post(TARGET, { from: null, payload: "third", ts: 500 });
|
||||
const deposits = await materialize(TARGET);
|
||||
const deposits = await read(TARGET);
|
||||
expect(deposits.map((d) => d.payload)).toEqual(["first", "second", "third"]);
|
||||
});
|
||||
|
||||
test("the published inbox surface exposes `read` and no `materialize` alias", () => {
|
||||
// `materialize` was a second published name for `read` — a symbol an application could
|
||||
// learn and would have to unlearn, since upstream has no such member. This asserts the
|
||||
// PUBLISHED entry (`src/index.ts`'s `inbox` namespace), which is what an app imports,
|
||||
// not merely the module it re-exports.
|
||||
expect(typeof polyfill.inbox.read).toBe("function");
|
||||
expect("materialize" in polyfill.inbox).toBe(false);
|
||||
});
|
||||
|
||||
test("read is scoped to one inbox — deposits in another inbox are not returned", async () => {
|
||||
// The OTHER inbox is obtained from the system, not invented. A made-up NURI would be
|
||||
// a target no deposit can legitimately reach (`inbox.post` refuses what is not an
|
||||
|
||||
@@ -83,6 +83,9 @@ test("write guard: passthrough when anchor is omitted (cannot scope the guard)",
|
||||
getCaps().grantWrite(DOC, "alice");
|
||||
setCurrentUser("bob");
|
||||
const proxy = makeNg();
|
||||
await proxy.sparql_update("sid", "INSERT DATA {}"); // no anchor → passthrough
|
||||
// Anchor explicitly `undefined` — upstream declares all three parameters (`index.d.ts:297`),
|
||||
// and the guard reads `args[2]`, which is `undefined` whether the argument is omitted or
|
||||
// passed as such. Same branch, same passthrough.
|
||||
await proxy.sparql_update("sid", "INSERT DATA {}", undefined); // no anchor → passthrough
|
||||
expect(ng.sparql_update).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user