Filed by the repo:hotcrm PM seat from a finding reported by the os-dev seat on #1133 (403 from the API, so devs report and the PM files). This is the card that would have prevented #1133, and it outranks the two cosmetic follow-ups filed beside it.
The gap
test/hooks-runtime-service.test.ts and test/case-assignment.test.ts call hook handlers directly, passing a plain object as ctx.input:
const ctx = { event: 'beforeInsert', input, user: undefined, api: harness.api };
await slaDefaults.handler(ctx as never);
The real engine does not. ObjectQL hands a hook ctx.input as { data, options } with a flat-record Proxy over it (installFlatInput, @objectstack/objectql src/hook-wrappers.ts). The two objects behave identically for reads and assignments and differently for anything else.
So any hook defect that lives in that difference is structurally invisible to this harness — while its assertions report success.
This is not hypothetical; it is exactly what happened
#1133: fifteen delete statements across two intake hooks were silent no-ops against the real engine, because the Proxy declares no deleteProperty trap and the delete lands on the wrapper one level above the record (upstream: objectstack#12277).
The tests asserting that strip passed the entire time — because on a plain object delete genuinely works. They were not merely failing to catch the defect; they were actively certifying the opposite of what production did, for as long as the defect existed. A security control read as enforced, in code and in its tests, and did nothing.
Why it is worth fixing rather than noting
The harness's speed is real and worth keeping — booting a full kernel per assertion is what makes test/guest-submission-sanitisation.test.ts (the #1133 pin) slow. The problem is not that a fast harness exists; it is that its input is a shape production never uses, so it silently under-approximates in a direction nobody can see.
⚠️ And the failure mode generalises past delete. Anything the Proxy mediates — ownKeys ordering, getOwnPropertyDescriptor, has on an absent key, property definition — can diverge the same way, with the same green tests.
Direction (a reading is required first, ⛔ not a foregone conclusion)
Give the direct-handler harness a wrapper-shaped input so it exercises the same object shape the engine passes. Two things to establish before writing anything:
- Is the wrapper constructible in isolation?
installFlatInput is internal to @objectstack/objectql. If it is not exported, the honest options are a faithful local reimplementation (which then needs its own pin against drift) or routing these tests through the real engine. ⚠️ A local copy that drifts from the original is a second way to get a confidently wrong answer — weigh it against the speed it buys.
- What breaks when it lands? Assertions currently passing because of the plain-object shape will change verdict. ⛔ Each one is a finding to read, not noise to suppress — if an assertion goes red under the real shape, that is the harness finally telling the truth.
Acceptance
Refs #1133 · PR #1294 · objectstack#12277
Filed by the
repo:hotcrmPM seat from a finding reported by theos-devseat on #1133 (403 from the API, so devs report and the PM files). This is the card that would have prevented #1133, and it outranks the two cosmetic follow-ups filed beside it.The gap
test/hooks-runtime-service.test.tsandtest/case-assignment.test.tscall hook handlers directly, passing a plain object asctx.input:The real engine does not. ObjectQL hands a hook
ctx.inputas{ data, options }with a flat-record Proxy over it (installFlatInput,@objectstack/objectqlsrc/hook-wrappers.ts). The two objects behave identically for reads and assignments and differently for anything else.So any hook defect that lives in that difference is structurally invisible to this harness — while its assertions report success.
This is not hypothetical; it is exactly what happened
#1133: fifteen
deletestatements across two intake hooks were silent no-ops against the real engine, because the Proxy declares nodeletePropertytrap and the delete lands on the wrapper one level above the record (upstream: objectstack#12277).The tests asserting that strip passed the entire time — because on a plain object
deletegenuinely works. They were not merely failing to catch the defect; they were actively certifying the opposite of what production did, for as long as the defect existed. A security control read as enforced, in code and in its tests, and did nothing.Why it is worth fixing rather than noting
The harness's speed is real and worth keeping — booting a full kernel per assertion is what makes
test/guest-submission-sanitisation.test.ts(the #1133 pin) slow. The problem is not that a fast harness exists; it is that its input is a shape production never uses, so it silently under-approximates in a direction nobody can see.delete. Anything the Proxy mediates —ownKeysordering,getOwnPropertyDescriptor,hason an absent key, property definition — can diverge the same way, with the same green tests.Direction (a reading is required first, ⛔ not a foregone conclusion)
Give the direct-handler harness a wrapper-shaped input so it exercises the same object shape the engine passes. Two things to establish before writing anything:
installFlatInputis internal to@objectstack/objectql. If it is not exported, the honest options are a faithful local reimplementation (which then needs its own pin against drift) or routing these tests through the real engine.Acceptance
deletes a key must be observably ineffective through the harness, exactly as it is in production. If the new harness cannot detect adeleteno-op, it has not closed the gap that produced Guest-submission sanitisation never reaches storage:delete input.xin a beforeInsert hook is a no-op while assignments on the same object land #1133.Refs #1133 · PR #1294 · objectstack#12277