Files
ng-eventually/packages/polyfill/test/use-shape.test.ts
Sylvain Duchesne a4ec2546bf fix: ce qui n-est pas DECIDE est refuse, et les deux vues portent la meme regle
LE PRINCIPE ETAIT FAUX, pas un nom manquant. "Un primitif ne peut atteindre
aucun objet, donc il passe" : or un COMPTE est un primitif qui DECRIT les
objets. Le fichier le savait deja, puisque `size` et `length` sont calcules - par
une liste de deux noms, c-est-a-dire la liste de noms interdite reapparue en
repli des que le principe a cede. Tout ce qui s-epelle autrement passait :
mesure, un porteur qui peut lire 1 objet sur 3 lisait `totalCount` a 3. Et LA
SUITE EPINGLAIT LA REPONSE QUI FUIT COMME CORRECTE, six lignes au-dessus d-un
test dont le commentaire appelle ce meme compte non filtre le defaut.

Aucun test de TYPE ne separe un primitif qui est une valeur d-un primitif derive
de la collection : un nombre est un nombre. Dit clairement, parce que la reponse
precedente pretendait le contraire. Le defaut s-inverse donc : ce que ce fichier
a DECIDE, il le sert ; tout le reste refuse, primitif ou non. C-est sur pour une
forme que personne n-a vue PAR CONSTRUCTION. Deux exceptions, aucune n-etant une
epellation du SDK : un membre que la cible ne porte pas rend `undefined`, et les
cles de protocole du LANGAGE, fermees par la specification.

LES DEUX VUES, PAS UNE. L-enveloppe avait deux pieges contre neuf : `delete
resultat.data`, `resultat.isLoading = x`, `setPrototypeOf` et
`preventExtensions` atteignaient l-objet du SDK - dont les deux derniers avaient
ete fermes sur la collection au round precedent. Les cinq pieges d-ecriture
vivent maintenant dans UN objet etale dans les deux.

`add` demandait `keep`, qui repond vrai pour un objet ne nommant AUCUN document :
juste pour une lecture, vide de sens pour une ecriture. L-objet atterrissait dans
la collection partagee, et seul l-amont l-arretait - la garantie empruntee que ce
fichier avait justement retiree un round plus tot.

Enfin `copyWithin`, que la liste amont classe non mutant a tort et que son propre
`readOnlyArray` refuse : il n-est plus servi.
2026-09-24 20:10:13 +02:00

546 lines
25 KiB
TypeScript

/**
* The published `useShape` - what an identity may see of what the SDK handed back.
*
* -- Why this file exists, and what it is the answer to --------------------
* One shared wallet hosts every identity, so the hook's own answer is every identity's
* objects and the capability filter is the whole of the isolation. Until 2026-09-24 that
* filter was written for the collection `@ng-org/orm@0.1.2-alpha.18` returned, while the
* version this package builds against - `alpha.21` - returns a `UseShapeResult` ENVELOPE
* around it. Three plain members of that envelope (`data`, `promise`, `subscription`) each
* carried the items and each came back RAW, and no test in this repository could see it,
* because no fake returned the shape the SDK ships. `test/use-shape-fake.ts` now does, read
* off the installed package's own `.d.ts`, and this file is what it made possible.
*
* Every document below is OBTAINED through the published placement call under the identity
* that owns it - never named by the test - so "hers" and "his" are facts of the system.
* Nothing hands a `ReadCap` across the boundary: the stranger holds what a stranger holds.
*/
import { test, expect, afterEach } from "bun:test";
import { configure, storeRegistry, useShape } from "../src/index";
import {
configureStoreRegistry,
resetCaps,
resetConfig,
resetStoreRegistry,
setCurrentUser,
} from "../src/shared-wallet/bootstrap";
import { resetRegistryCache } from "../src/shared-wallet/account-registry";
import { makeWallet, SESSION, type Quad } from "./wallet-fake";
import { makeUseShapeHook } from "./use-shape-fake";
import type { FakeShapeHook } from "./use-shape-fake";
import type { NgLike, Nuri, ShapeResult, UseShapeLike } from "../src/model/types";
import type { BaseType, ShapeType } from "@ng-org/shex-orm";
// TYPE-only: no `@ng-org/orm` module is loaded at runtime here. What this line checks is
// the claim the injection type MAKES - that the SDK's OWN React hook is assignable to
// `UseShapeLike` as it stands, with no wrapper and no cast, so `configure({ useShape })`
// is the whole of what a caller writes. It is also what keeps that type honest: it was
// `(...args: any[]) => any`, which no shape could ever contradict.
import type { reactUseShape } from "@ng-org/orm/all";
type _TheSdkHookFits = typeof reactUseShape extends UseShapeLike ? true : never;
const _sdkFits: _TheSdkHookFits = true;
void _sdkFits;
const NoteShape = {
shape: "urn:use-shape-test:NoteShape",
schema: {
"urn:use-shape-test:NoteShape": {
iri: "urn:use-shape-test:NoteShape",
predicates: [
{
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
readablePredicate: "@type",
dataTypes: [{ valType: "iri", literals: ["urn:use-shape-test:Note"] }],
minCardinality: 1,
maxCardinality: 1,
},
],
},
},
} satisfies ShapeType<BaseType>;
/** Everything the local dataset holds - the scope under which this filter is the only
* thing between the caller and every identity's objects. */
const EVERYTHING = { graphs: "did:ng:i" };
function inject(hook: FakeShapeHook): void {
const quads: Quad[] = [];
const ng: NgLike = makeWallet(quads);
configure({ ng, useShape: hook.hook });
configureStoreRegistry({ getSession: async () => SESSION, normalizeId: (id) => id.trim() });
resetRegistryCache();
resetCaps();
setCurrentUser(null);
}
/** A document that really belongs to `who` - created by them, through the published call,
* which is also what files its cap in their keyring. */
async function documentOf(who: string): Promise<Nuri> {
setCurrentUser(who);
return storeRegistry.createEntityDoc(await storeRegistry.protectedStore());
}
/** One ORM object placed in `doc`, as the ORM itself carries it. */
function noteIn(doc: Nuri, id: string): BaseType {
return { "@id": `${doc}:q:${id}`, "@graph": doc } as BaseType;
}
afterEach(() => {
resetConfig();
resetStoreRegistry();
resetCaps();
setCurrentUser(null);
});
// -- THE RIGHTS CASE: the stranger ------------------------------------------
// The defect, stated as a test. Each of the three members below is one the `alpha.21`
// declaration gives the items to, and each used to hand back the unfiltered collection.
test("useShape: no member of the result reaches a document this identity holds no cap for", async () => {
const hook = makeUseShapeHook();
inject(hook);
const hers = await documentOf("alice");
const his = await documentOf("bob");
hook.items.push(noteIn(hers, "a1"), noteIn(his, "b1"));
setCurrentUser("bob");
const result: ShapeResult = useShape(NoteShape, EVERYTHING);
// `data` - what the SDK's own example destructures first.
expect([...(result.data as Iterable<BaseType>)].map((o) => o["@graph"])).toEqual([his]);
expect((result.data as { size: number }).size).toBe(1);
// `promise` - the same collection, one await away.
expect([...((await result.promise) as Iterable<BaseType>)].map((o) => o["@graph"])).toEqual([his]);
// `subscription` - the SDK's own handle, which reads and writes with no filter at all.
expect(() => result.subscription).toThrow(/RdfOrmSubscription/);
// and the collection underneath really does hold both, so it is the filter that
// removed one rather than the fake never having offered it.
expect([...(hook.raw() as Iterable<BaseType>)]).toHaveLength(2);
});
// Iterating is not the only way in, and the members a reactive set puts forward are the
// ones a consumer reaches for first. Same rule as the collection view's own regression.
test("useShape: the item-yielding members of `data` are filtered too, not just iteration", async () => {
const hook = makeUseShapeHook();
inject(hook);
const hers = await documentOf("alice");
const his = await documentOf("bob");
const foreign = noteIn(hers, "a1");
hook.items.push(foreign, noteIn(his, "b1"));
setCurrentUser("bob");
const data = useShape(NoteShape, EVERYTHING).data as {
values(): Iterable<BaseType>;
forEach(cb: (v: BaseType) => void): void;
has(v: BaseType): boolean;
};
expect([...data.values()].map((o) => o["@graph"])).toEqual([his]);
const seen: string[] = [];
data.forEach((o) => seen.push(o["@graph"]));
expect(seen).toEqual([his]);
expect(data.has(foreign)).toBe(false);
});
// -- THE OTHER DIRECTION: the holder is served ------------------------------
// A repair that closes a leak by making the call unusable has traded one defect for
// another. Every member the declaration offers a holder is exercised here on their OWN
// data, and the two page commands with it.
test("useShape: the holder reads their own items through every member the SDK offers", async () => {
const hook = makeUseShapeHook({ paginated: true });
inject(hook);
const hers = await documentOf("alice");
const his = await documentOf("bob");
hook.items.push(noteIn(hers, "a1"), noteIn(his, "b1"), noteIn(hers, "a2"));
setCurrentUser("alice");
const result: ShapeResult = useShape(NoteShape, EVERYTHING);
expect([...(result.data as Iterable<BaseType>)].map((o) => o["@graph"])).toEqual([hers, hers]);
expect((result.data as { size: number }).size).toBe(2);
expect([...((await result.promise) as Iterable<BaseType>)]).toHaveLength(2);
// The load state is a boolean and passes on its own, with no entry naming it. It reads
// `true` on a payload that HAS arrived, because the shipped hook computes it as
// `ormSubscription.isReady` while the declaration beside it says the opposite. That
// inversion is upstream's; the fake reproduces it rather than correcting it, and this
// assertion is what stops anyone here quietly making the target nicer than it is.
expect(result.isLoading).toBe(true);
// The page commands forward: they return `void`, and what they load lands in a
// collection only ever read back through the filter.
result.nextPage?.();
result.previousPage?.();
expect(hook.pages).toEqual(["next", "previous"]);
// Reading `data` twice hands back the same view, so a caller may put it in a
// dependency array without it looking like new data on every read.
expect(result.data).toBe(result.data);
});
// Before the first cap exists the emulation is not in force, exactly as `getObjects` and
// `useShapeQuery` behave: a consumer that never touches caps keeps reading everything,
// and gets the SDK's own object back untouched.
test("useShape: passthrough while no cap has been issued", () => {
const hook = makeUseShapeHook();
inject(hook);
hook.items.push(noteIn("did:ng:o:someone" as Nuri, "x"));
const result: ShapeResult = useShape(NoteShape, EVERYTHING);
expect([...(result.data as Iterable<BaseType>)]).toHaveLength(1);
expect(result.subscription).not.toBeUndefined();
});
// Upstream: "If the conf is explicitly set to `undefined`, the returned data is
// `undefined` and `isLoading` is false". Nothing was opened, so there is nothing to
// refuse, and `subscription` answers the SDK's own value rather than throwing.
test("useShape: an undefined config answers undefined rather than refusing", async () => {
const hook = makeUseShapeHook();
inject(hook);
await documentOf("alice");
setCurrentUser("alice");
const result: ShapeResult = useShape(NoteShape, undefined);
expect(result.data).toBeUndefined();
expect(result.promise).toBeUndefined();
expect(result.subscription).toBeUndefined();
expect(result.isLoading).toBe(false);
});
// -- A MUTATION MAY NOT REACH PAST WHAT THE VIEW SHOWS ---------------------
// THE RIGHTS CASE, second kind, and it is the member the SDK's own example reaches for
// first (`expenses.add({...})`). `Set.prototype.add` RETURNS THE COLLECTION so calls can
// be chained - measured, not assumed - so a mutation forwarded bound to the target handed
// the unfiltered collection back as its return value. The view filtered every way IN and
// gave the whole set away on the way OUT.
test("useShape: adding an item does not hand back the unfiltered collection", async () => {
const hook = makeUseShapeHook();
inject(hook);
const hers = await documentOf("alice");
const his = await documentOf("bob");
hook.items.push(noteIn(hers, "a1"), noteIn(his, "b1"));
setCurrentUser("bob");
const data = useShape(NoteShape, EVERYTHING).data as {
add(item: BaseType): unknown;
[Symbol.iterator](): Iterator<BaseType>;
};
expect([...data].map((o) => o["@graph"])).toEqual([his]); // the way in is filtered
const handedBack = data.add(noteIn(his, "b2"));
expect(handedBack).not.toBe(hook.raw());
// Chaining still works, and what it chains onto is the VIEW: still bob's documents only.
expect([...(handedBack as Iterable<BaseType>)].map((o) => o["@graph"])).toEqual([his, his]);
// and the write really did reach the collection underneath.
expect([...(hook.raw() as Iterable<BaseType>)]).toHaveLength(3);
});
// `has(item)` has always refused to answer about an item the holder may not read. Acting
// on one is the same question with a worse consequence, and the two disagreed: `has` said
// no while `delete` removed it.
test("useShape: deleting reaches only what the view shows", async () => {
const hook = makeUseShapeHook();
inject(hook);
const hers = await documentOf("alice");
const his = await documentOf("bob");
const hersItem = noteIn(hers, "a1");
hook.items.push(hersItem, noteIn(his, "b1"));
setCurrentUser("bob");
const data = useShape(NoteShape, EVERYTHING).data as {
delete(item: BaseType): boolean;
has(item: BaseType): boolean;
clear(): void;
};
expect(data.has(hersItem)).toBe(false);
expect(data.delete(hersItem)).toBe(false);
expect([...(hook.raw() as Iterable<BaseType>)]).toHaveLength(2); // hers survives
// and emptying the view empties the view, not the collection every identity shares.
data.clear();
expect([...(hook.raw() as Iterable<BaseType>)].map((o) => o["@graph"])).toEqual([hers]);
});
// The page commands forward, and their RESULT is held to the same rule as every other
// member. `nextPage` is declared `() => void` and yields nothing today, so this pins the
// RULE rather than a live defect - which is the point: "it returns void" was the written,
// never-executed ground on which `add` was forwarded, and `add` returns the collection.
test("useShape: a page command that handed something back would be refused, not forwarded", async () => {
const base = makeUseShapeHook({ paginated: true });
const leaky: FakeShapeHook = {
...base,
hook: ((shapeType, conf) => ({
...base.hook(shapeType, conf),
nextPage: (): void => base.raw() as undefined,
})) as UseShapeLike,
};
inject(leaky);
const hers = await documentOf("alice");
const his = await documentOf("bob");
base.items.push(noteIn(hers, "a1"), noteIn(his, "b1"));
setCurrentUser("bob");
const result: ShapeResult = useShape(NoteShape, EVERYTHING);
expect(() => result.nextPage?.()).toThrow(/not filtered/i);
});
// THE THIRD MUTATION, and the one that was ungated. `delete` and `clear` asked `keep` and
// `add` did not, which made the published rule true by coincidence: the ungated `add`
// MANUFACTURED the state the other two then handled partially. Bob plants an object into
// ALICE's document, never sees it again - the view filters it straight back out - and the
// ORM's two-way binding commits it there. Then `clear()` leaves it behind.
//
// Each of the three is run here against a document this identity does not hold, and the
// collection underneath is checked afterwards. That is the contract's mutation sentence,
// clause by clause, executed rather than read.
test("useShape: none of the three mutations reaches a document this identity holds no cap for", async () => {
const hook = makeUseShapeHook();
inject(hook);
const hers = await documentOf("alice");
const his = await documentOf("bob");
const hersItem = noteIn(hers, "a1");
hook.items.push(hersItem, noteIn(his, "b1"));
setCurrentUser("bob");
const data = useShape(NoteShape, EVERYTHING).data as {
add(item: BaseType): unknown;
delete(item: BaseType): boolean;
clear(): void;
has(item: BaseType): boolean;
};
// add - refuses, because it has no return value meaning "it did not happen".
expect(() => data.add(noteIn(hers, "PLANT"))).toThrow(/does not hold the cap/i);
// delete - answers `false`, which is the Set contract's own word for "not there" and is
// true of the view.
expect(data.has(hersItem)).toBe(false);
expect(data.delete(hersItem)).toBe(false);
// clear - empties the view, which holds none of hers.
data.clear();
// The collection underneath is exactly as it was, minus bob's own item.
expect([...(hook.raw() as Iterable<BaseType>)].map((o) => o["@graph"])).toEqual([hers]);
// And the holder's OWN write still goes through and still reaches the collection.
expect([...(data.add(noteIn(his, "b2")) as Iterable<BaseType>)].map((o) => o["@graph"]))
.toEqual([his]);
expect([...(hook.raw() as Iterable<BaseType>)]).toHaveLength(2);
});
// A write to the collection itself is refused. Upstream's ordered `readOnlyArray` refuses
// these too, so here the two agree - which is the point of the fake now building the REAL
// collection kinds. That the VIEW refuses on its own account, whatever the target allows,
// is proved one floor down in `read-filter.test.ts` against a plain array, because relying
// on the target's guarantee is relying on something this view does not promise and does
// not control. `length = 0` in particular defeated the gated `clear` outright.
test("useShape: a write to the view itself is refused, whatever the collection underneath allows", async () => {
const hook = makeUseShapeHook();
inject(hook);
const hers = await documentOf("alice");
await documentOf("bob");
hook.items.push(noteIn(hers, "a1"), noteIn(hers, "a2"));
setCurrentUser("bob");
const data = useShape(NoteShape, { ...EVERYTHING, orderBy: { liveMark: "asc" } }).data as
Record<number, unknown> & { length: number };
expect(() => {
data[0] = { "@id": "SPY", "@graph": "did:ng:o:spy" };
}).toThrow(/not filtered/i);
expect(() => {
delete data[0];
}).toThrow(/not filtered/i);
expect(() => {
data.length = 0;
}).toThrow(/not filtered/i);
// The collection underneath is untouched.
expect([...(hook.raw() as Iterable<BaseType>)].map((o) => o["@graph"])).toEqual([hers, hers]);
});
// The descriptor trap handed an ACCESSOR back untouched, so the getter behind it - which
// reads the TARGET - was a door around the view. Not reachable on the shape that ships,
// where every member reports a data descriptor; reachable on the next one, which is what
// this view claims to be safe for.
test("useShape: an accessor member is read through the view, not around it", async () => {
const base = makeUseShapeHook();
const withGetter: FakeShapeHook = {
...base,
hook: ((shapeType, conf) => {
const built = base.hook(shapeType, conf);
const raw = base.raw();
return {
isLoading: built.isLoading,
promise: built.promise,
subscription: built.subscription,
get data(): unknown {
return raw;
},
};
}) as UseShapeLike,
};
inject(withGetter);
const hers = await documentOf("alice");
const his = await documentOf("bob");
base.items.push(noteIn(hers, "a1"), noteIn(his, "b1"));
setCurrentUser("bob");
const result: ShapeResult = useShape(NoteShape, EVERYTHING);
const described = Object.getOwnPropertyDescriptor(result, "data");
expect(typeof described?.get).toBe("function");
const throughTheDescriptor = described?.get?.();
expect(throughTheDescriptor).not.toBe(base.raw());
expect([...(throughTheDescriptor as Iterable<BaseType>)].map((o) => o["@graph"])).toEqual([his]);
});
// -- SAFE FOR A SHAPE NOBODY HAS SEEN --------------------------------------
/** What a later `@ng-org/orm` might hand back: the six members of `alpha.21` plus three
* nobody has declared - one holding the collection, one COUNTING it, one yielding an item. */
interface LaterResult extends ShapeResult {
readonly rows: unknown;
readonly totalCount: number;
readonly sample: () => unknown;
}
// WHAT A MEMBER NOBODY HAS DECIDED ANSWERS, and the third version of this test, because the
// first two were wrong in the same way. It asserted that `totalCount` PASSING was correct,
// on the ground that "a primitive cannot hold a reference to anything" - six lines above a
// test whose own comment calls an unfiltered count the defect. The suite asserted both that
// the leak was correct and that it was the defect, which is worse than the leak: it would
// have kept it green for ever.
//
// A count IS a primitive and DOES describe the items, and no type-level test separates it
// from a page size. So nothing undecided passes now, whatever its type.
test("useShape: no member this package has not decided is answered, whatever its type", async () => {
const base = makeUseShapeHook();
const later: FakeShapeHook = {
...base,
hook: ((shapeType, conf) => {
const result: LaterResult = {
...base.hook(shapeType, conf),
rows: base.raw(),
totalCount: 7,
sample: () => [...(base.raw() as Iterable<BaseType>)][0],
};
return result;
}) as UseShapeLike,
};
inject(later);
const hers = await documentOf("alice");
const his = await documentOf("bob");
base.items.push(noteIn(hers, "a1"), noteIn(his, "b1"));
setCurrentUser("bob");
const result = useShape(NoteShape, EVERYTHING) as LaterResult;
// An OBJECT member holds the collection, so it is refused at the read - there is no
// later moment to refuse it at.
expect(() => result.rows).toThrow(/not filtered/i);
// A FUNCTION member is refused at the CALL, so reading it (a logger, a `typeof` guard)
// is not itself refused.
expect(typeof result.sample).toBe("function");
expect(() => result.sample()).toThrow(/not filtered/i);
// A PRIMITIVE is refused too, and this is the case the earlier version of this test had
// backwards: `totalCount` counts items the holder may not read. Three exist, one may be
// read, and the number that may not be published is 3.
expect([...(result.data as Iterable<BaseType>)]).toHaveLength(1);
expect(() => result.totalCount).toThrow(/not filtered/i);
// A member the SDK does not carry at all answers `undefined`: there is nothing behind it
// to derive anything from, so refusing would refuse a question nobody asked.
expect((result as unknown as { neverShipped?: unknown }).neverShipped).toBeUndefined();
});
// An `orderBy` config makes the collection an ARRAY (upstream `SubscriptionData`), whose
// count is spelled `length` and is a number - so it used to pass as a harmless primitive
// while publishing how many items the holder may NOT read.
test("useShape: an ordered result does not publish the unfiltered count", async () => {
const hook = makeUseShapeHook();
inject(hook);
const hers = await documentOf("alice");
const his = await documentOf("bob");
hook.items.push(noteIn(hers, "a1"), noteIn(his, "b1"), noteIn(his, "b2"));
setCurrentUser("alice");
const data = useShape(NoteShape, { ...EVERYTHING, orderBy: { liveMark: "asc" } }).data;
expect(Array.isArray(hook.raw())).toBe(true);
expect((hook.raw() as BaseType[]).length).toBe(3);
expect((data as { length: number }).length).toBe(1);
expect([...(data as Iterable<BaseType>)].map((o) => o["@graph"])).toEqual([hers]);
// An index names a position in the READABLE items, so the holder reads their own by
// index and the positions past them do not exist at all. `Object.keys` and `in` have to
// agree with `length`, or the count comes back out through the other three.
expect((data as Record<number, BaseType>)[0]!["@graph"]).toBe(hers);
expect((data as Record<number, BaseType>)[1]).toBeUndefined();
expect(Object.keys(data as object)).toEqual(["0"]);
expect(1 in (data as object)).toBe(false);
expect(JSON.parse(JSON.stringify(data)) as BaseType[]).toHaveLength(1);
});
// REGRESSION, found by re-running the adversary on the first fix (2026-09-24).
// `Object.getOwnPropertyDescriptor` reads a property WITHOUT the `get` trap, so a view
// that filtered `get` alone handed the raw value straight back to whoever asked for the
// descriptor - including `subscription`, the one member `get` refuses outright. Everything
// built on it goes the same way: a spread, `Object.entries`, a console inspection.
test("useShape: the descriptor of a member answers exactly what reading it answers", async () => {
const hook = makeUseShapeHook();
inject(hook);
const hers = await documentOf("alice");
const his = await documentOf("bob");
hook.items.push(noteIn(hers, "a1"), noteIn(his, "b1"));
setCurrentUser("bob");
const result: ShapeResult = useShape(NoteShape, EVERYTHING);
const described = Object.getOwnPropertyDescriptor(result, "data");
expect(described?.value).toBe(result.data);
expect(described?.value).not.toBe(hook.raw());
expect([...(described?.value as Iterable<BaseType>)].map((o) => o["@graph"])).toEqual([his]);
expect(() => Object.getOwnPropertyDescriptor(result, "subscription")).toThrow(
/RdfOrmSubscription/,
);
});
// REGRESSION (fourth adversarial pass). The envelope view had TWO traps where the
// collection view had nine, so the SDK's own result object could be written to through it -
// the same fix applied in one of two places, which is the shape of every defect this file
// has had. Both views now take the write traps from ONE object, so one cannot be repaired
// without the other.
test("useShape: the result object cannot be written to through the view either", async () => {
const hook = makeUseShapeHook();
inject(hook);
const hers = await documentOf("alice");
await documentOf("bob");
hook.items.push(noteIn(hers, "a1"));
setCurrentUser("bob");
const result: ShapeResult = useShape(NoteShape, EVERYTHING);
const asRecord = result as unknown as Record<string, unknown>;
expect(() => {
delete asRecord["data"];
}).toThrow(/not filtered/i);
expect(() => {
asRecord["isLoading"] = true;
}).toThrow(/not filtered/i);
expect(() => Object.setPrototypeOf(result, { stolen: 1 })).toThrow(/not filtered/i);
expect(() => Object.preventExtensions(result)).toThrow(/not filtered/i);
// The request state still reads, because it is DECIDED rather than passed: it describes
// the request, not the items.
expect(typeof result.isLoading).toBe("boolean");
});
// REGRESSION (fourth adversarial pass). The write gate asked `keep`, which answers `true`
// for an object naming NO document - right for a read, where there is no cap to ask about,
// and VACUOUS for a write: the object landed in the collection every identity shares. The
// only thing stopping it was upstream raising "@graph is missing", which is a guarantee
// borrowed from the target - the very thing removed for `set`/`deleteProperty` one round
// earlier and left standing here.
test("useShape: adding an object that names no document at all is refused", async () => {
const hook = makeUseShapeHook();
inject(hook);
const hers = await documentOf("alice");
const his = await documentOf("bob");
hook.items.push(noteIn(hers, "a1"));
setCurrentUser("bob");
const data = useShape(NoteShape, EVERYTHING).data as { add(item: BaseType): unknown };
expect(() => data.add({ "@id": "PLANT" } as BaseType)).toThrow(/names no document/i);
expect([...(hook.raw() as Iterable<BaseType>)]).toHaveLength(1);
// A write that names a document this identity holds still goes through.
data.add(noteIn(his, "b1"));
expect([...(hook.raw() as Iterable<BaseType>)]).toHaveLength(2);
});