fix: les trois mutations posent la meme question, et la vue porte ses propres ecritures
L-ASYMETRIE, une fois de plus, et c-est elle le defaut. `delete` et `clear` interrogeaient `keep`, `add` non - ce qui rendait la phrase publiee vraie par coincidence : le `add` non garde FABRIQUAIT l-etat que les deux autres ne traitaient qu-a moitie. Bob plante un objet dans le document d-ALICE, ne le revoit jamais (la vue le refiltre), la liaison bidirectionnelle le commite la-bas, puis `clear()` le laisse derriere. Les trois posent maintenant la meme question ; ce qu-elles repondent differe parce que les membres different : `delete` a `false`, qui est le mot du contrat Set pour "il n-y etait pas" et qui est vrai de la vue ; `add` n-a aucune valeur de ce sens, donc il leve. LA VUE PORTE SES PROPRES ECRITURES. Aucun piege `set`/`deleteProperty`/ `defineProperty` : contre un tableau ordonne, `view[0] = x`, `delete view[0]` et `view.length = 0` passaient - le dernier defaisant `clear` entierement. Ce qui est livre est un `readOnlyArray` qui les refuse, donc la production etait protegee PAR LA CIBLE : la vue s-appuyait sur une garantie qu-elle ne donne pas, sur une forme qu-elle ne controle pas. C-est l-hypothese empruntee qui est derriere chaque defaut de cette famille. LE PROPRIETAIRE EST SERVI. 18 membres declares d-un resultat ordonne levaient pour celui dont ce sont les donnees, plus `includes` et les sept combinateurs d-ensemble : une fuite echangee contre une panne. Tout ce que `@ng-org/alien-deepsignals` declare non mutant est servi sur les objets lisibles, et un test relit le fichier livre pour que la liste restee ici ne derive pas. Aussi : le descripteur d-un ACCESSEUR routait vers la cible ; le faux construit maintenant les VRAIES collections (`deepSignal`, `readOnlyArray`) au lieu d-un Set et d-un tableau nus, ce qui le rendait plus permissif que ce qui est livre apres l-avoir rendu plus correct au round precedent.
This commit is contained in:
@@ -280,6 +280,112 @@ test("useShape: a page command that handed something back would be refused, not
|
||||
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 two
|
||||
|
||||
Reference in New Issue
Block a user