Skip to content

Commit c21f09e

Browse files
committed
test(objectql): teach the commit-history double $or, conjoined (#7819 tier 1, part of #7620)
CI red on PR #7857: `packages/objectql/src/protocol-commit-history.test.ts` had two org-scoped revert cases fail with COMMIT_NOT_FOUND. Measured rather than assumed: its `matchesWhere` was pure flat equality, so the widened lookup `{ id, $or: [{organization_id: <org>}, {organization_id: null}] }` compared `row['$or']` against the array and matched nothing. The double is the blind party, not the fix. Both failing rows carry the CALLER'S OWN org (`organization_id: 'org_a'`, request org `'org_a'`), so they match the FIRST `$or` branch outright — the same row the strict equality already accepted. No real behaviour changed, and neither case's subject (#6602's registry org-asymmetry) involves the commit lookup at all; it is merely the door they enter through. Conjoined with the sibling keys in the entries loop, matching the corrected form #7846 landed across six doubles in this package an hour earlier. Not the early-returning `if ($or) return …some(…)` shape those six carried before it: that discards sibling keys, so `{ id, $or: [...] }` would stop constraining `id` and could return some other commit whose org matched. This file was not among #7846's six because it had no operator handling to correct, so it is a new member of the #7620 lane rather than a regression of it. `undefined` normalises to `null` on comparison, same as the six, because a column a row never set reads as NULL out of a real driver. @objectstack/objectql: 185 files / 3274 tests passing (was 184/3272 with the two failures) — exactly the two cases restored, nothing else moved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019hxiiv8qFCUmDThHU1k7HV
1 parent b4ea578 commit c21f09e

2 files changed

Lines changed: 79 additions & 1 deletion

File tree

‎.changeset/revert-commit-org-scope.md‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,34 @@ handoff assertion, and leaves both negative directions and both no-org doors
9090
green, since strict equality is *narrower* than the `$or`. Measured: 3 failed |
9191
11 passed, exactly those three.
9292

93+
## A blind test double, taught rather than accommodated
94+
95+
`packages/objectql/src/protocol-commit-history.test.ts` went red on two
96+
org-scoped revert cases. Measured, not assumed: its `matchesWhere` was pure flat
97+
equality, so it compared `row['$or']` against the array and matched nothing.
98+
99+
The double was the blind party, not the fix — both failing rows carry the
100+
**caller's own** org (`organization_id: 'org_a'`, request org `'org_a'`), so
101+
they match the first `$or` branch outright: the same row the strict equality
102+
already accepted. Neither case's subject (#6602's registry org-asymmetry)
103+
involves the commit lookup at all; it is merely the door they enter through.
104+
105+
It now understands `$or`/`$and`, **conjoined with the sibling keys in the
106+
entries loop** — the corrected form #7846 landed across six doubles in this
107+
package (part of #7620), not the early-returning `if ($or) return …some(…)`
108+
shape those six carried before it. That shape discards sibling keys, so
109+
`{ id, $or: [...] }` would stop constraining `id` and the lookup could return
110+
some *other* commit whose org matched. This file was not among #7846's six
111+
because it had no operator handling to correct, so it reads as a new member of
112+
the #7620 lane rather than a regression of it.
113+
114+
⚠️ Recorded deliberately: this makes the double a *reimplementation* of `$or`,
115+
so any assertion whose **subject** is the org predicate would be measuring the
116+
double rather than the protocol. No case in that file has that subject — which
117+
is exactly why it could never see this family — and a comment there says so and
118+
asks that org-scoping cases not be added. The operator's real behaviour against
119+
a real driver stays pinned on the real engine in `packages/runtime`.
120+
93121
## Scope
94122

95123
Tier 1 of #7819 only. The two remaining strict equalities in this file —

‎packages/objectql/src/protocol-commit-history.test.ts‎

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,10 +247,60 @@ describe('ADR-0067 — publishPackageDrafts records a commit', () => {
247247
/** A Studio authoring workspace id — writable under ADR-0070. */
248248
const APP_PKG = 'app.myapp';
249249

250+
/**
251+
* [#7819] `$or` / `$and` are understood, because a double that silently drops
252+
* an operator does not answer "no match" — it answers a WRONG match.
253+
*
254+
* `revertCommit` and `rollbackToPackageCommit` resolve their target commit with
255+
* `{ id, $or: [{ organization_id: <org> }, { organization_id: null }] }` so an
256+
* org-scoped caller can reach a commit recorded env-wide. This helper was flat
257+
* equality, so it compared `row['$or']` against the array and every lookup
258+
* missed — including the two org-scoped cases below, whose seeded rows carry
259+
* the caller's OWN org and therefore match the FIRST branch outright. Nothing
260+
* about their subject (#6602's registry org-asymmetry) changed; the double just
261+
* could not evaluate the predicate that now guards the door they enter through.
262+
*
263+
* ⚠️ CONJOINED with the sibling keys, in the entries loop — the corrected form
264+
* #7846 just landed across six doubles in this package (part of #7620), and
265+
* deliberately NOT the early-returning `if ($or) return …some(…)` shape those
266+
* six carried before it. That shape discards every sibling equality key, so
267+
* `{ id, $or: [...] }` would stop constraining `id` at all and this lookup
268+
* would return SOME OTHER commit whose org happened to match. With one seeded
269+
* commit per harness that is invisible today — which is exactly what makes it
270+
* worth ruling out here rather than discovering later.
271+
*
272+
* This file was not among #7846's six because it had no operator handling at
273+
* all to correct (pure flat equality), so it reads as a new member of the same
274+
* #7620 lane rather than a regression of it.
275+
*
276+
* ⚠️ What this helper does NOT do is pin `$or` semantics — it is a
277+
* reimplementation of them, so any assertion whose SUBJECT is the org predicate
278+
* would be measuring this function rather than the protocol. No case in this
279+
* file has that subject (which is precisely why this file could never see the
280+
* #7705/#7779/#7819 family), and the operator's real behaviour against a real
281+
* driver — whether `organization_id = 'org'` matches a NULL column — is pinned
282+
* on a real engine in `packages/runtime/src/package-revert-commit-org-scope.
283+
* integration.test.ts`. Keep it that way: do not add org-scoping cases here.
284+
*/
250285
const matchesWhere = (r: Record<string, unknown>, w: Record<string, unknown>): boolean => {
286+
if (!w || typeof w !== 'object') return true;
251287
for (const [k, v] of Object.entries(w)) {
288+
if (k === '$and' && Array.isArray(v)) {
289+
if (!v.every((s: any) => matchesWhere(r, s))) return false;
290+
continue;
291+
}
292+
if (k === '$or' && Array.isArray(v)) {
293+
if (!v.some((s: any) => matchesWhere(r, s))) return false;
294+
continue;
295+
}
296+
if (k.startsWith('$')) continue;
252297
if (v === undefined) continue;
253-
if (r[k] !== v) return false;
298+
// A column a row never set reads as NULL out of a real driver, so an
299+
// absent field must satisfy `{ organization_id: null }` — the env-wide
300+
// branch of the `$or`. Comparing `undefined !== null` would make this
301+
// double refuse rows SQLite returns.
302+
const actual = r[k] === undefined ? null : r[k];
303+
if (actual !== v) return false;
254304
}
255305
return true;
256306
};

0 commit comments

Comments
 (0)