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:
@@ -32,7 +32,7 @@ import type { Nuri } from "../model/types";
|
||||
* nothing else. Never exported from the package.
|
||||
*/
|
||||
export async function registerUpdate(
|
||||
sessionId: string,
|
||||
sessionId: string | number,
|
||||
query: string,
|
||||
anchor: Nuri,
|
||||
label = "registerUpdate",
|
||||
@@ -66,7 +66,7 @@ export async function registerUpdate(
|
||||
* only caller, and reading is guarded separately (`inbox.read`).
|
||||
*/
|
||||
export async function depositInto(
|
||||
sessionId: string,
|
||||
sessionId: string | number,
|
||||
query: string,
|
||||
targetInbox: Nuri,
|
||||
label = "deposit",
|
||||
|
||||
@@ -139,7 +139,15 @@ export type { EventuallyConfig } from "./shared-wallet/bootstrap";
|
||||
export { ensureIdentity } from "./shared-wallet/access-gate";
|
||||
export type { SharedWalletConfig } from "./shared-wallet/access-gate";
|
||||
|
||||
import type { NG } from "@ng-org/web";
|
||||
|
||||
import { makeNg } from "./surface/ng-proxy";
|
||||
|
||||
/** SDK-identical `ng` (wrapped). Drop-in replacement for `@ng-org/web`'s `ng`. */
|
||||
export const ng: Record<string, any> = makeNg();
|
||||
/**
|
||||
* SDK-identical `ng` (wrapped). Drop-in replacement for `@ng-org/web`'s `ng`.
|
||||
*
|
||||
* Declared `NG` — upstream's own type for its `ng` (`index.d.ts:138`). It was published as
|
||||
* `Record<string, any>` until 2026-08-14, which announced a different surface from the one
|
||||
* it forwards to: an application got no completion and no check on any of the 88 members.
|
||||
*/
|
||||
export const ng: NG = makeNg();
|
||||
|
||||
@@ -250,7 +250,8 @@ export function accountKey(id: string): string {
|
||||
|
||||
/** Minimal session shape the registry needs — provided by the consumer. */
|
||||
export interface RegistrySession {
|
||||
sessionId: string;
|
||||
/** Relayed untouched to `ng` — upstream declares it `string | number` (`index.d.ts:266`). */
|
||||
sessionId: string | number;
|
||||
/** The shared wallet's private store id — the pointer anchor. */
|
||||
privateStoreId: string;
|
||||
/** The shared wallet's protected store id (native store). Optional: only the
|
||||
|
||||
@@ -55,7 +55,7 @@ import type { Nuri } from "../model/types";
|
||||
* afterwards, which the caller decides by filing the cap among the caps that holder holds.
|
||||
*/
|
||||
export async function physicalCreate(
|
||||
sessionId: string,
|
||||
sessionId: string | number,
|
||||
crdt = "Graph",
|
||||
cls = "data:graph",
|
||||
dest = "store",
|
||||
@@ -81,7 +81,7 @@ export async function physicalCreate(
|
||||
* read a virtual user's content — that is `docs.sparqlQuery`, which is confined.
|
||||
*/
|
||||
export async function physicalQuery(
|
||||
sessionId: string,
|
||||
sessionId: string | number,
|
||||
query: string,
|
||||
base: string | undefined,
|
||||
anchor: Nuri,
|
||||
@@ -95,7 +95,7 @@ export async function physicalQuery(
|
||||
|
||||
/** Write as the PHYSICAL user — the shim's own records. See {@link physicalQuery}. */
|
||||
export async function physicalUpdate(
|
||||
sessionId: string,
|
||||
sessionId: string | number,
|
||||
query: string,
|
||||
anchor: Nuri,
|
||||
label = "physicalUpdate",
|
||||
|
||||
@@ -95,15 +95,15 @@ export function sessionIsComing(): boolean {
|
||||
*
|
||||
* ── The session id is RELAYED, never rebuilt — and that is load-bearing ────
|
||||
* Upstream declares it `string | number` (`Session`, `index.d.ts:266`) and the broker
|
||||
* returns a NUMBER; the whole chain below here types it `string` and hands it to `ng.*`,
|
||||
* returns a NUMBER; the chain below here now types it the same way and hands it to `ng.*`,
|
||||
* whose binding takes it as-is. So this reads the field and passes it on untouched. It is
|
||||
* not a detail: normalizing it to a string was written here first, and the applicative e2e
|
||||
* refused every call in the batch with `Deserialization error of session_id JsValue("1")`
|
||||
* — the wasm side deserializes the id by its own type, and a stringified number is not it.
|
||||
*
|
||||
* The `string` in the declared shape is therefore inherited, not asserted: the inaccuracy
|
||||
* is the chain's and predates this module (every consumer's thunk declared it the same way
|
||||
* and relayed the same value). Widening it belongs to the chain, not to the capture.
|
||||
* The chain used to narrow it to `string`, which made the value the SDK hands an application
|
||||
* unpassable through this library. Widened to upstream's own declared type on 2026-08-14, at
|
||||
* the capture and along every hop that relays it; the id is still only ever RELAYED.
|
||||
*/
|
||||
export function captureSession(event: unknown): boolean {
|
||||
if (typeof event !== "object" || event === null) return false;
|
||||
@@ -116,7 +116,7 @@ export function captureSession(event: unknown): boolean {
|
||||
protected_store_id: protectedStoreId,
|
||||
public_store_id: publicStoreId,
|
||||
} = session as {
|
||||
session_id?: string;
|
||||
session_id?: string | number;
|
||||
private_store_id?: string;
|
||||
protected_store_id?: string;
|
||||
public_store_id?: string;
|
||||
|
||||
@@ -44,8 +44,15 @@ function rowCount(result: unknown): number {
|
||||
* document in the (shared) private store: `docCreate(sid, "Graph", "data:graph",
|
||||
* "store")` (store_repo left undefined → private store).
|
||||
*/
|
||||
// The session id is `string | number` because that is what upstream DECLARES for it
|
||||
// (`Session.session_id`, `sdk/js/web/src/index.ts:16` and the installed `index.d.ts:266`),
|
||||
// and the wasm side deserializes it as a `u64` (`sdk/js/lib-wasm/src/lib.rs:352-358`
|
||||
// `sparql_query`, `:452-457` `sparql_update`, `:1575` `doc_create`). It only ever TRAVELS
|
||||
// through this chain — never normalise it, and above all never stringify it: a JS string
|
||||
// fails that deserialization, observed live as
|
||||
// `Deserialization error of session_id JsValue("1")`.
|
||||
export async function docCreate(
|
||||
sessionId: string,
|
||||
sessionId: string | number,
|
||||
crdt: string,
|
||||
cls: string,
|
||||
dest: string,
|
||||
@@ -82,13 +89,21 @@ export async function docCreate(
|
||||
*
|
||||
* Mirrors `ng.sparql_update(session_id, query, anchor?)`, where `anchor` is the
|
||||
* document NURI the update is scoped/base'd to (optional).
|
||||
*
|
||||
* Returns what the real method returns: upstream answers the COMMITS the update
|
||||
* produced (`sdk/js/lib-wasm/src/lib.rs:481-483` serialises `AppResponseV0::Commits`;
|
||||
* the installed `index.d.ts:297` types it `Promise<any>`). This function already
|
||||
* relayed that value at runtime — only the declared type said `void`, which threw the
|
||||
* answer away for every caller. Typed `unknown` rather than `any`, exactly as
|
||||
* {@link sparqlQuery} already renders the same upstream `Promise<any>`: the value is
|
||||
* the broker's to shape, and a caller that ignores it is unaffected.
|
||||
*/
|
||||
export async function sparqlUpdate(
|
||||
sessionId: string,
|
||||
sessionId: string | number,
|
||||
query: string,
|
||||
anchorLike?: NuriLike,
|
||||
label = "sparqlUpdate",
|
||||
): Promise<void> {
|
||||
): Promise<unknown> {
|
||||
const { ng } = getConfig();
|
||||
const anchor = anchorLike === undefined ? undefined : toNuri(anchorLike, "docs.sparqlUpdate");
|
||||
// The boundary, in two questions that are NOT the same one.
|
||||
@@ -117,7 +132,7 @@ export async function sparqlUpdate(
|
||||
* query base IRI (usually `undefined`); `anchor` is the document NURI to query.
|
||||
*/
|
||||
export async function sparqlQuery(
|
||||
sessionId: string,
|
||||
sessionId: string | number,
|
||||
query: string,
|
||||
base?: string,
|
||||
anchorLike?: NuriLike,
|
||||
|
||||
@@ -84,7 +84,7 @@ const P = {
|
||||
|
||||
/** The inbox documents live in the shared wallet, so we reuse the registry's
|
||||
* injected session provider for the sessionId. Disappears at migration. */
|
||||
async function sessionId(): Promise<string> {
|
||||
async function sessionId(): Promise<string | number> {
|
||||
return (await getStoreRegistryDeps().getSession()).sessionId;
|
||||
}
|
||||
|
||||
@@ -519,8 +519,10 @@ export async function read(targetInboxLike: NuriLike): Promise<Deposit[]> {
|
||||
return delivered;
|
||||
}
|
||||
|
||||
/** Alias for {@link read} — the name that reads as "process the inbox now". */
|
||||
export const materialize = read;
|
||||
// `materialize` — a second published name for {@link read} — was REMOVED from the surface
|
||||
// on 2026-08-14. It was an alias and nothing more: no call site anywhere, and upstream has
|
||||
// no such member, so it was a symbol an application could learn and would have to unlearn.
|
||||
// `read` is the name; the word "materialize" survives only as prose in the docs.
|
||||
|
||||
/**
|
||||
* COLD, BARRIER-GATED read of `targetInbox` — the reliable "process the inbox at
|
||||
|
||||
@@ -4,61 +4,79 @@
|
||||
* surface stays identical to `@ng-org/web`'s `ng`.
|
||||
*/
|
||||
|
||||
import type { NG } from "@ng-org/web";
|
||||
|
||||
import { getConfig, getCaps, getCurrentUser } from "../shared-wallet/bootstrap";
|
||||
import type { Nuri } from "../model/types";
|
||||
|
||||
export function makeNg(): Record<string, any> {
|
||||
return new Proxy({} as Record<string, any>, {
|
||||
get(_target, prop: string) {
|
||||
/**
|
||||
* Typed `NG` — upstream's own type for `ng` (`NG = typeof NGModule`, 88 exported members,
|
||||
* installed `index.d.ts:136-231`). The proxy adds no member and removes none, so this is
|
||||
* the honest declaration; it used to be `Record<string, any>`, which silently dropped all
|
||||
* 88 signatures and let any misspelt member typecheck.
|
||||
*
|
||||
* The empty object is only the Proxy's TARGET: every property is answered by the `get`
|
||||
* trap below, never by the target, so there is nothing to put in it. Typing the target is
|
||||
* how `new Proxy` carries a type to its result — not an escape hatch, and the surface it
|
||||
* announces is checked against upstream's for every caller.
|
||||
*/
|
||||
export function makeNg(): NG {
|
||||
return new Proxy({} as NG, {
|
||||
get(_target, prop: string | symbol) {
|
||||
const { ng } = getConfig();
|
||||
// The overrides below are keyed by NAME, so they are only consulted for a string
|
||||
// property. A symbol (`Symbol.toStringTag`, `Symbol.asyncIterator`, …) skips them and
|
||||
// falls to the same passthrough as everything else at the bottom — bound identically,
|
||||
// so the proxy stays transparent for every kind of key.
|
||||
if (typeof prop === "string") {
|
||||
// session_start → open the SHARED wallet invisibly.
|
||||
//
|
||||
// `login` used to be listed here too. `@ng-org/web` exposes no such method —
|
||||
// zero occurrences in the installed declarations and in `sdk/js/lib-wasm/src/lib.rs`
|
||||
// — so the proxy FABRICATED a member: `ng.login` answered a function instead of
|
||||
// `undefined`, and calling it threw. The one place this wrapper added to the SDK
|
||||
// surface, against its own header. Removed 2026-08-03.
|
||||
if (prop === "session_start") {
|
||||
return (...args: any[]) => {
|
||||
// TODO(polyfill): supply shared-wallet credentials so no wallet UI
|
||||
// is shown. For now, passthrough.
|
||||
return ng[prop]!(...args);
|
||||
};
|
||||
}
|
||||
|
||||
// session_start → open the SHARED wallet invisibly.
|
||||
//
|
||||
// `login` used to be listed here too. `@ng-org/web` exposes no such method —
|
||||
// zero occurrences in the installed declarations and in `sdk/js/lib-wasm/src/lib.rs`
|
||||
// — so the proxy FABRICATED a member: `ng.login` answered a function instead of
|
||||
// `undefined`, and calling it threw. The one place this wrapper added to the SDK
|
||||
// surface, against its own header. Removed 2026-08-03.
|
||||
if (prop === "session_start") {
|
||||
return (...args: any[]) => {
|
||||
// TODO(polyfill): supply shared-wallet credentials so no wallet UI
|
||||
// is shown. For now, passthrough.
|
||||
return ng[prop]!(...args);
|
||||
};
|
||||
// sparql_update → write guard (emulated write-cap check).
|
||||
// Mirrors the target broker/verifier: a write is refused unless the wallet
|
||||
// holds the document's WRITE cap. Emulated per-document via CapRegistry.
|
||||
// args = (session_id, query, anchor?) — `anchor` is the target doc NURI.
|
||||
if (prop === "sparql_update") {
|
||||
return (...args: any[]) => {
|
||||
const anchor = args[2] as Nuri | undefined;
|
||||
const caps = getCaps();
|
||||
// Passthrough (no regression) unless a WRITE policy exists AND this
|
||||
// specific document is governed by it. Ungoverned docs (mono-store
|
||||
// default, no cap declared) flow through exactly as before.
|
||||
if (
|
||||
typeof anchor === "string" &&
|
||||
caps.hasWritePolicy() &&
|
||||
caps.governsWrite(anchor) &&
|
||||
!caps.canWrite(anchor, getCurrentUser())
|
||||
) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
`[ng-eventually] write denied: current user lacks the write cap for ${anchor}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
return ng.sparql_update!(...args);
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(anticipated API): a sealed inbox deposit + capability operations — expose
|
||||
// here with their anticipated signatures, emulated for now.
|
||||
}
|
||||
|
||||
// sparql_update → write guard (emulated write-cap check).
|
||||
// Mirrors the target broker/verifier: a write is refused unless the wallet
|
||||
// holds the document's WRITE cap. Emulated per-document via CapRegistry.
|
||||
// args = (session_id, query, anchor?) — `anchor` is the target doc NURI.
|
||||
if (prop === "sparql_update") {
|
||||
return (...args: any[]) => {
|
||||
const anchor = args[2] as Nuri | undefined;
|
||||
const caps = getCaps();
|
||||
// Passthrough (no regression) unless a WRITE policy exists AND this
|
||||
// specific document is governed by it. Ungoverned docs (mono-store
|
||||
// default, no cap declared) flow through exactly as before.
|
||||
if (
|
||||
typeof anchor === "string" &&
|
||||
caps.hasWritePolicy() &&
|
||||
caps.governsWrite(anchor) &&
|
||||
!caps.canWrite(anchor, getCurrentUser())
|
||||
) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
`[ng-eventually] write denied: current user lacks the write cap for ${anchor}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
return ng.sparql_update!(...args);
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(anticipated API): a sealed inbox deposit + capability operations — expose
|
||||
// here with their anticipated signatures, emulated for now.
|
||||
|
||||
// Everything else: passthrough to the real SDK, unchanged.
|
||||
const real = ng[prop];
|
||||
const real = Reflect.get(ng, prop);
|
||||
return typeof real === "function" ? real.bind(ng) : real;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -97,7 +97,7 @@ function bindings(
|
||||
return anyRes.results?.bindings ?? [];
|
||||
}
|
||||
|
||||
async function sessionId(): Promise<string> {
|
||||
async function sessionId(): Promise<string | number> {
|
||||
return (await getStoreRegistryDeps().getSession()).sessionId;
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ async function sessionId(): Promise<string> {
|
||||
* store repo by cap is a native broker fetch (`verifier.rs:1423` `OpenRepo` TODO).
|
||||
*/
|
||||
async function readDoc(
|
||||
sid: string,
|
||||
sid: string | number,
|
||||
doc: Nuri,
|
||||
): Promise<Array<Record<string, { value: string } | undefined>>> {
|
||||
try {
|
||||
|
||||
@@ -79,7 +79,7 @@ export function docChangeType(resp: DocChange): DocChangeType {
|
||||
/** An unsubscribe function — idempotent (calling it twice is a no-op). */
|
||||
export type Unsubscribe = () => void;
|
||||
|
||||
async function sessionId(): Promise<string> {
|
||||
async function sessionId(): Promise<string | number> {
|
||||
return (await getStoreRegistryDeps().getSession()).sessionId;
|
||||
}
|
||||
|
||||
|
||||
@@ -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