Skip to content

Commit 6a4e929

Browse files
os-elonclaude
andauthored
runtime: withhold the cross-field refusal's own engine noise (#10983) (#11246)
extend expected-read-refusal-noise.ts with a second predicate keyed on the cross-field refusal's own message identity, since the refusal never reaches the driver's backendStatementFault and so has no table to key on. Counts per object instead of per table; the caller asserts the counts in its own afterAll, same withhold-and-assert discipline as #10629/#10630. Co-authored-by: Claude <noreply@anthropic.com>
1 parent 064d484 commit 6a4e929

2 files changed

Lines changed: 202 additions & 0 deletions

File tree

packages/runtime/src/cross-field-refusal-operand-withhold.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import type { AggregationNode, Cube, FilterCondition } from '@objectstack/spec/d
8181
import { markFilterSubtreeProvenance } from '@objectstack/spec/data';
8282

8383
import { createDispatcherPlugin } from './dispatcher-plugin.js';
84+
import { captureExpectedCrossFieldRefusalNoise } from './expected-read-refusal-noise.js';
8485

8586
const OBJECT = 'cross_field_deal';
8687

@@ -335,9 +336,25 @@ describe('[#7929] a cross-field refusal keeps its envelope and stops disclosing
335336
* caller cannot see, which is the entire premise of this card.
336337
*/
337338
let crudScope: FilterCondition | null = null;
339+
/**
340+
* [#10983] Every read below that the driver refuses reaches this engine's
341+
* `find()`, so its `catch` logs an `ERROR Find operation failed` frame
342+
* BEFORE rethrowing (`engine.ts`) — a green-test noise instance of the
343+
* same defect class #10629/#10630 closed, just without a table to key on
344+
* (the refusal never reaches `backendStatementFault`; see
345+
* `expected-read-refusal-noise.ts`'s second predicate for why and how).
346+
* Withheld and COUNTED here, never muted — `silentChannels()` and
347+
* `totalFrames()` are asserted in this describe's own `afterAll`.
348+
*/
349+
let crossFieldNoise: ReturnType<typeof captureExpectedCrossFieldRefusalNoise>;
338350

339351
beforeAll(async () => {
340352
ql = new ObjectQL();
353+
// Installed before the first read this describe block issues — the
354+
// engine's `logger` is a private field with no setter, same access
355+
// discipline `captureExpectedReadRefusals` documents.
356+
crossFieldNoise = captureExpectedCrossFieldRefusalNoise([OBJECT]);
357+
crossFieldNoise.captureEngine(ql);
341358
ql.registerDriver(driver as never, true);
342359
await ql.init();
343360
ql.registerObject({
@@ -369,6 +386,19 @@ describe('[#7929] a cross-field refusal keeps its envelope and stops disclosing
369386
});
370387
});
371388

389+
afterAll(() => {
390+
// [#10983] The pin, not the mute: every declared object's channel fired
391+
// at least once, AND the count is exactly what this describe block's
392+
// five `it`s produce — six `ql.find()` calls refused (the sixth test
393+
// below drives the driver directly, bypassing this engine on purpose,
394+
// so it contributes no seventh). A capture that withheld 0, or any
395+
// number other than 6, means either the predicate stopped matching or
396+
// a call this accounting did not expect appeared — either way a
397+
// silent-mute reading is exactly what this assertion exists to catch.
398+
expect(crossFieldNoise.silentChannels()).toEqual([]);
399+
expect(crossFieldNoise.totalFrames()).toBe(6);
400+
});
401+
372402
const readWithScope = async (
373403
scope: FilterCondition | null,
374404
where?: FilterCondition,

packages/runtime/src/expected-read-refusal-noise.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,3 +243,175 @@ export function captureExpectedReadRefusals(
243243
},
244244
};
245245
}
246+
247+
/**
248+
* ═══════════════════════════════════════════════════════════════════════════
249+
* [#10983] A SECOND, independent predicate: cross-field `{ $field }` refusal
250+
* engine noise (#7929) — the sibling {@link captureExpectedReadRefusals}
251+
* cannot recognise, because it has no table to key on
252+
* ═══════════════════════════════════════════════════════════════════════════
253+
*
254+
* ## Why this cannot reuse the table-keyed predicate above
255+
*
256+
* `captureExpectedReadRefusals` withholds an engine frame only when it sits
257+
* directly above a driver refusal ITS OWN `captureDriver` sink already
258+
* recognised ({@link pending} in that closure) — the engine frame alone never
259+
* carries enough identity to withhold safely, so the driver channel supplies
260+
* it via the caller's declared TABLE plus that table's `no such table` reason.
261+
*
262+
* A cross-field `{ $field }` refusal (#7929, `uncompilableFieldReferenceError`
263+
* in `driver-sql/src/sql-driver.ts`) never goes through that path at all: it is
264+
* a validation refusal raised while COMPILING the filter, not a backend
265+
* statement fault, so it never reaches `SqlDriver.backendStatementFault` and
266+
* there is no driver-channel line — and therefore no `pending` entry — for an
267+
* engine frame to sit above. There is no table involved either: the refusal is
268+
* about which FIELDS a filter compared, not which table was queried.
269+
*
270+
* ## What replaces the table: the engine frame's own message, measured
271+
*
272+
* Unlike the generic "no such table" case, THIS engine frame's `error.message`
273+
* already carries the refusal's own identity — so the message itself is the
274+
* correlation, and no driver-channel line is needed to supply it. Measured
275+
* directly off a real run of `cross-field-refusal-operand-withhold.test.ts`
276+
* (`--reporter=default`, the reporter this feature bypasses regardless — see
277+
* the file's own header): its 6 ERROR frames carry not one message shape but
278+
* TWO, both produced by `uncompilableFieldReferenceError` —
279+
*
280+
* * the WITHHELD generic wording (4 of the 6): `"A cross-field comparison
281+
* ({ \"$field\": … }) in this filter cannot be compiled here. …"`;
282+
* * the [#8220] author-DISCLOSED wording (2 of the 6, from the one case in
283+
* that file where the predicate is positively marked `'author'` and the
284+
* driver restores the full diagnostic): `"Operator \"$gt\" on field
285+
* \"amount\" compares against another field ({ \"$field\":
286+
* \"secret_policy_column\" }), which cannot be compiled here: …"`.
287+
*
288+
* A predicate keyed on the withheld wording alone would silently miss the
289+
* disclosed half — 4 of 6 withheld and 2 loud is not "expected-noise
290+
* withholding", it is a partial mute with a passing assertion sitting on top
291+
* of it. Both strings share exactly two substrings and no third candidate
292+
* does: **`cannot be compiled here`** (verified by source grep to appear
293+
* NOWHERE else in `driver-sql/src/sql-driver.ts` — it is
294+
* `uncompilableFieldReferenceError`'s own wording and no other builder's) and
295+
* **`$field`** (present on every `{ $field }`-shaped refusal, cross-field
296+
* comparison or not). Requiring BOTH — mirroring the sibling predicate's
297+
* "table AND reason" pairing above — is what keeps a sibling `{ $field }`
298+
* family (`bareFieldReferenceError`'s "bare field reference … with no
299+
* operator", which contains `$field` but never "cannot be compiled here")
300+
* OUTSIDE the match: same operand syntax, different refusal, stays loud.
301+
*
302+
* ## ⛔ Still not a mute: the counting dimension moves, the discipline does not
303+
*
304+
* There is no driver-channel table to count against, so what is counted
305+
* changes from "per table, across two correlated channels" to "per OBJECT,
306+
* on the one channel that exists" — the engine frame's own `object` meta,
307+
* scoped to the caller's declared list exactly as the table list scopes the
308+
* sibling predicate. What does NOT change: a frame is withheld only on an
309+
* exact identity match (declared object AND both message markers), the
310+
* withholding is COUNTED per object, and the caller MUST assert those counts
311+
* ({@link ExpectedCrossFieldRefusalCapture.silentChannels}) — a capture
312+
* nobody asserts is a mute, whichever dimension it counts by.
313+
*
314+
* ⛔ Test-only, same reason as above: not exported from `src/index.ts`.
315+
*/
316+
317+
export interface ExpectedCrossFieldRefusalCapture {
318+
/** Per-object count of engine `Find operation failed` frames withheld. */
319+
readonly frames: WithheldByTable;
320+
/** Total engine frames withheld, across every declared object. */
321+
totalFrames(): number;
322+
/** The declared objects that were seen at least once. */
323+
objectsSeen(): string[];
324+
/**
325+
* The declared objects whose expected cross-field frame never fired — the
326+
* assertion surface, mirroring
327+
* {@link ExpectedReadRefusalCapture.silentChannels}. ⛔ Repairing a failure
328+
* here means finding out why the refusal stopped firing (or reproducing on
329+
* a new message shape this predicate has not been taught) — NEVER loosening
330+
* the match in {@link captureExpectedCrossFieldRefusalNoise} to make it pass.
331+
*
332+
* @param required the subset that must have fired. Defaults to every
333+
* declared object.
334+
*/
335+
silentChannels(required?: readonly string[]): string[];
336+
/**
337+
* Install the engine sink. Same access pattern and same call-before-reads
338+
* discipline as {@link ExpectedReadRefusalCapture.captureEngine} — the
339+
* engine's logger is a private field with no setter.
340+
*/
341+
captureEngine(engine: unknown): void;
342+
}
343+
344+
/**
345+
* Build a capture for the cross-field `{ $field }` refusal family (#7929) on
346+
* a fixture's declared objects.
347+
*
348+
* @param objects the object names whose cross-field `{ $field }` refusals are
349+
* EXPECTED here. Derive them by measurement (as the file header does)
350+
* rather than assumption, so an object this predicate was not told about
351+
* stays loud rather than silently swallowed.
352+
*/
353+
export function captureExpectedCrossFieldRefusalNoise(
354+
objects: readonly string[],
355+
): ExpectedCrossFieldRefusalCapture {
356+
const frames = new Map<string, number>();
357+
358+
const bump = (object: string): void => {
359+
frames.set(object, (frames.get(object) ?? 0) + 1);
360+
};
361+
362+
const sum = (): number => {
363+
let n = 0;
364+
for (const v of frames.values()) n += v;
365+
return n;
366+
};
367+
368+
/**
369+
* Both markers must be present — see the docblock above for why each one
370+
* alone is either not unique enough (`$field` alone also matches
371+
* `bareFieldReferenceError`) or, on its own, unverified against a message
372+
* shape this predicate has never seen.
373+
*/
374+
const isExpectedCrossFieldRefusal = (detail: string): boolean =>
375+
detail.includes('cannot be compiled here') && detail.includes('$field');
376+
377+
return {
378+
frames,
379+
totalFrames: () => sum(),
380+
objectsSeen: () => [...frames.keys()].sort(),
381+
382+
silentChannels(required: readonly string[] = objects): string[] {
383+
const out: string[] = [];
384+
for (const o of required) {
385+
if ((frames.get(o) ?? 0) === 0) {
386+
out.push(
387+
`the engine's 'Find operation failed' frame for the cross-field refusal on '${o}' was never emitted`,
388+
);
389+
}
390+
}
391+
return out;
392+
},
393+
394+
captureEngine(engine: unknown): void {
395+
const base = (engine as { logger: Record<string, any> }).logger;
396+
(engine as { logger: unknown }).logger = new Proxy(base, {
397+
get: (target: Record<string, any>, key: string) =>
398+
key === 'error'
399+
? (msg: string, err?: unknown, meta?: unknown) => {
400+
const object = (meta as { object?: string } | undefined)?.object;
401+
const detail = String((err as { message?: string } | undefined)?.message ?? '');
402+
if (
403+
msg === 'Find operation failed' &&
404+
object !== undefined &&
405+
objects.includes(object) &&
406+
isExpectedCrossFieldRefusal(detail)
407+
) {
408+
bump(object);
409+
return;
410+
}
411+
target.error(msg, err, meta);
412+
}
413+
: target[key],
414+
});
415+
},
416+
};
417+
}

0 commit comments

Comments
 (0)