fix: le descripteur d-un membre repond ce que le lire repond

Trouve en rejouant l-adversaire sur le correctif precedent, et mesure :
`Object.getOwnPropertyDescriptor(resultat, "data")` rendait la collection BRUTE,
et `(..., "subscription")` le handle que `get` refuse. Cet appel ne passe pas
par le piege `get` - ni l-etalement, ni `Object.entries`, ni `JSON.stringify`,
ni une inspection console. Une deuxieme porte sur la meme piece, ouverte par un
piege que personne n-avait ecrit. Le meme trou existait sur la vue de collection
pour la trappe dunder.

La forme ordonnee dit combien d-objets elle porte a QUATRE endroits : `length`,
`Object.keys`, `in`, et le descripteur de chaque index. Un seul etait filtre,
donc le compte de ce que le porteur ne peut pas lire ressortait par les trois
autres, et le descripteur rendait les objets eux-memes.

Les quatre pieges appellent maintenant une seule fonction `read`, pas trois
copies de la regle : une frontiere faite de `switch` paralleles est une
frontiere qui derive. Et l-index nomme une position dans ce qui est LISIBLE,
donc la forme ordonnee sert son porteur au lieu de refuser.
This commit is contained in:
Sylvain Duchesne
2026-09-24 18:35:22 +02:00
parent e16fd6a707
commit e86e249a5a
5 changed files with 291 additions and 120 deletions
+32 -3
View File
@@ -262,7 +262,36 @@ test("useShape: an ordered result does not publish the unfiltered count", async
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 hands back a raw item and there is no projection of it this file would not
// have to invent, so it is refused rather than guessed at.
expect(() => (data as Record<number, unknown>)[0]).toThrow(/not filtered/i);
// 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/,
);
});