Skip to content

Commit 5293ddc

Browse files
committed
test(runtime): migrate the notification-conformance fixture onto the shared read-refusal capture
The file carried its own copy of the expected-read-refusal capture and wrapped the engine's `error` channel only. Since #13273/#13327 `ObjectQL.reportFindFailure` picks the level from the cause, so a read whose table was never provisioned -- which is every read this capture is declared over -- is logged at `debug`. The inline recognition arm could therefore no longer match a single frame: measured on this tree before the change, the engine `error` channel was invoked 0 times while 63 `Find operation failed` frames arrived on `debug`, all 63 satisfying that arm's own predicate. The file stayed green because everything it asserts is fed by the driver channel, so the engine-side suppression was dead code reading as live protection. It now uses `captureExpectedReadRefusals` (#10629), which wraps both channels. The `afterAll` assertion moves from the driver-only `withheld.has(table)` loop to `silentChannels(ALWAYS_READ_AUTHZ_TABLES)`, which is strictly stronger: it requires both channels to have fired for every always-read table. The header's counts are re-measured on this tree rather than carried forward, and its prose no longer claims the engine frame arrives on `error`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k
1 parent 0783d7b commit 5293ddc

1 file changed

Lines changed: 54 additions & 91 deletions

File tree

packages/runtime/src/notification-schema-conformance.integration.test.ts

Lines changed: 54 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import type { IHttpServer } from '@objectstack/spec/contracts';
4848

4949
import { createDispatcherPlugin } from './dispatcher-plugin.js';
5050
import { DriverPlugin } from './driver-plugin.js';
51+
import { captureExpectedReadRefusals } from './expected-read-refusal-noise.js';
5152

5253
// One inbox per concern. The mark-read routes MUTATE read-state, so a shared
5354
// user would make these suites order-dependent — the unread fixture the gap
@@ -63,18 +64,46 @@ const declaredMarkReadKeys = () => new Set(Object.keys((MarkNotificationsReadRes
6364
const declaredMarkAllReadKeys = () => new Set(Object.keys((MarkAllNotificationsReadResponseSchema as any).shape));
6465

6566
// ═══════════════════════════════════════════════════════════════════════════
66-
// [#10380] The authz resolver's expected read failures: WITHHELD from the
67-
// shared log, and ASSERTED instead
67+
// [#10380 → #10629 → #13325] The authz resolver's expected read failures:
68+
// WITHHELD from the shared log, and ASSERTED instead
6869
// ═══════════════════════════════════════════════════════════════════════════
6970
//
7071
// This fixture provisions the messaging objects and nothing else, so every
7172
// request's `resolveUserAuthzGrants` (`core/src/security/resolve-authz-context.ts`)
7273
// reads six `sys_*` tables that were never created. `tryFind` swallows each
7374
// one by design — the resolver is fail-closed and must always resolve — but on
74-
// the way out the driver and the engine each log it. Measured on `origin/main`:
75-
// 63 `[sql-driver] DATABASE_ERROR — the backend refused a read on '…'` lines
76-
// and 63 matching `ERROR Find operation failed` frames, out of a suite whose
77-
// eight tests all PASS.
75+
// the way out the driver and the engine each log it.
76+
//
77+
// ⚠️ [#13273] WHICH ENGINE CHANNEL — and why this file stopped rolling its own
78+
// capture. `ObjectQL.reportFindFailure` now picks the level from the CAUSE: a
79+
// read whose table was never provisioned — i.e. every read this block is
80+
// declared over — is logged at `debug`, carrying a
81+
// `reason: 'table-not-provisioned'` meta and no stack; every other read failure
82+
// keeps `error` with the stack. This file used to carry its own copy of the
83+
// capture and wrapped the engine's `error` channel ONLY, so from that change
84+
// onward its recognition arm could not match a single frame — measured on this
85+
// tree before the migration, the engine's `error` channel was invoked 0 times
86+
// while 63 `Find operation failed` frames arrived on `debug`, every one of
87+
// which satisfied that arm's own predicate. Dead suppression that read as live
88+
// protection, and the file stayed green throughout because everything it
89+
// asserted was fed by the DRIVER channel. It now uses the shared
90+
// `captureExpectedReadRefusals` (#10629), which wraps BOTH channels, so which
91+
// channel a frame arrives on is the ENGINE's classification and never this
92+
// fixture's problem.
93+
//
94+
// Counts RE-MEASURED on this tree (#13325), not transcribed:
95+
//
96+
// pnpm --filter @objectstack/runtime exec vitest run \
97+
// src/notification-schema-conformance.integration.test.ts
98+
//
99+
// → 63 `[sql-driver] DATABASE_ERROR — the backend refused a read on '…'`
100+
// refusals withheld on the DRIVER channel, and 63 matching engine
101+
// `Find operation failed` frames withheld on the `debug` channel, out of a
102+
// suite whose eight tests all PASS. ⛔ Those two totals are PROSE, not a pin
103+
// — they move with the routes this file drives, and re-deriving them means
104+
// reading `noise.totalRefusals()` / `noise.totalEngineFrames()` off a run,
105+
// never copying the numbers forward. What IS asserted is
106+
// `silentChannels()`: every always-read table fired on BOTH channels.
78107
//
79108
// Turbo interleaves package logs without attribution, so in the `Test Core`
80109
// shard log those are indistinguishable from a real failure — they were lifted
@@ -84,10 +113,10 @@ const declaredMarkAllReadKeys = () => new Set(Object.keys((MarkAllNotificationsR
84113
// ⛔ Not a mute. A capture that only silences would make this file blind: if
85114
// those reads ever started SUCCEEDING (someone provisions the tables) or
86115
// stopped happening (the resolver drops a read), the log would go quiet and
87-
// nothing would notice. So the sinks below withhold ONLY the expected fault —
88-
// each line must name one of the six tables AND carry that same table's
89-
// `no such table` reason — and record what they withheld, which `afterAll`
90-
// asserts. Everything else reaches the console untouched.
116+
// nothing would notice. So the shared capture withholds ONLY the expected
117+
// fault — each line must name one of the six tables AND carry that same
118+
// table's `no such table` reason — and COUNTS what it withheld, per table and
119+
// per channel, which `afterAll` asserts.
91120

92121
/**
93122
* The six tables the resolver reads and this fixture does not provision.
@@ -111,76 +140,6 @@ const ABSENT_AUTHZ_TABLES = [
111140
*/
112141
const ALWAYS_READ_AUTHZ_TABLES = ABSENT_AUTHZ_TABLES.filter((t) => t !== 'sys_setting');
113142

114-
/** Installs the two sinks and hands back the tables they withheld. */
115-
function captureExpectedAbsentTableNoise() {
116-
const withheld = new Set<string>();
117-
/** Recognised driver refusals not yet consumed by their engine frame. */
118-
const pending = new Map<string, number>();
119-
120-
/** The refusal envelope AND the dialect reason must name the SAME table. */
121-
const expectedRefusal = (line: string): string | undefined =>
122-
ABSENT_AUTHZ_TABLES.find(
123-
(t) => line.includes(`refused a read on '${t}'`) && line.includes(`no such table: ${t}`),
124-
);
125-
126-
return {
127-
withheld,
128-
129-
/**
130-
* The driver's sink. Mirrors the default's `{ warn, error }` shape so
131-
* `logDurabilityFailure` still finds an `error` channel.
132-
*/
133-
install(driver: unknown): void {
134-
(driver as { logger: unknown }).logger = {
135-
warn: (msg: string, meta?: unknown): void => {
136-
const table = expectedRefusal(String(msg));
137-
if (table) {
138-
withheld.add(table);
139-
pending.set(table, (pending.get(table) ?? 0) + 1);
140-
return;
141-
}
142-
console.warn(msg, meta ?? '');
143-
},
144-
error: (msg: string, meta?: unknown): void => console.error(msg, meta ?? ''),
145-
};
146-
},
147-
148-
/**
149-
* The engine's `error` channel, through a Proxy so every other logger
150-
* method stays the engine's own.
151-
*
152-
* ⛔ Gated on `pending`: an engine frame is withheld only when it sits
153-
* directly above a driver refusal this capture already recognised. A
154-
* `DATABASE_ERROR` on one of these six tables arising from any OTHER cause
155-
* is not recognised by the driver sink, so its frame is not withheld here
156-
* either — it reaches the log with both halves intact.
157-
*/
158-
wrapEngine(engine: unknown): void {
159-
const base = (engine as { logger: any }).logger;
160-
(engine as { logger: any }).logger = new Proxy(base, {
161-
get: (target: any, key: string) =>
162-
key === 'error'
163-
? (msg: string, err?: unknown, meta?: unknown) => {
164-
const object = (meta as { object?: string } | undefined)?.object;
165-
const outstanding = object ? (pending.get(object) ?? 0) : 0;
166-
const detail = String((err as { message?: string } | undefined)?.message ?? '');
167-
if (
168-
msg === 'Find operation failed' &&
169-
object &&
170-
outstanding > 0 &&
171-
detail.includes(`refused to run this query for object '${object}'`)
172-
) {
173-
pending.set(object, outstanding - 1);
174-
return;
175-
}
176-
target.error(msg, err, meta);
177-
}
178-
: target[key],
179-
});
180-
},
181-
};
182-
}
183-
184143
/** Minimal `auth` service — `x-test-user` names the principal, absent = anonymous. */
185144
function fakeAuthPlugin(): Plugin {
186145
return {
@@ -205,15 +164,19 @@ describe('[#5792] the notification wire bodies conform to the schemas the catalo
205164
let kernel: ObjectKernel;
206165
let baseUrl: string;
207166
let messaging: MessagingService;
208-
/** [#10380] The expected-noise capture, asserted in `afterAll`. */
209-
const noise = captureExpectedAbsentTableNoise();
167+
/**
168+
* [#10629] The expected-noise capture, asserted in `afterAll`. The SHARED
169+
* one — see the block above for what the per-fixture copy this replaced
170+
* could no longer do.
171+
*/
172+
const noise = captureExpectedReadRefusals([...ABSENT_AUTHZ_TABLES]);
210173

211174
beforeAll(async () => {
212175
kernel = new ObjectKernel({ logger: { level: 'silent' } });
213176
// [#10380] The driver is named rather than inlined so its logger can be
214177
// scoped before it ever runs a statement.
215178
const driver = new SqliteWasmDriver({ filename: ':memory:' });
216-
noise.install(driver);
179+
noise.captureDriver(driver);
217180
await kernel.use(new DriverPlugin(driver));
218181
await kernel.use(new ObjectQLPlugin());
219182
// Inline delivery so `emit()` materializes the inbox row synchronously.
@@ -225,7 +188,7 @@ describe('[#5792] the notification wire bodies conform to the schemas the catalo
225188

226189
// [#10380] The engine only exists once the kernel has bootstrapped; the
227190
// reads this scopes all happen later, per request.
228-
noise.wrapEngine(kernel.getService<unknown>('objectql'));
191+
noise.captureEngine(kernel.getService<unknown>('objectql'));
229192

230193
const httpServer = kernel.getService<IHttpServer>('http.server');
231194
baseUrl = `http://127.0.0.1:${httpServer.getPort!()}`;
@@ -255,13 +218,13 @@ describe('[#5792] the notification wire bodies conform to the schemas the catalo
255218
// right repair is to re-derive the list above, NOT to relax this: a
256219
// resolver read that stopped happening is a finding, and a table that
257220
// started resolving means this fixture now provisions it.
258-
for (const table of ALWAYS_READ_AUTHZ_TABLES) {
259-
expect(
260-
noise.withheld.has(table),
261-
`the expected '${table}' read failure was never emitted — resolveUserAuthzGrants ` +
262-
'no longer reads it, or it no longer fails, and this capture has stopped measuring it',
263-
).toBe(true);
264-
}
221+
//
222+
// ⚠️ [#13325] STRICTLY MORE than the per-table `withheld.has(table)` loop
223+
// this replaced, never less: that loop read the DRIVER channel only, which
224+
// is exactly why it kept passing while the engine half of the same capture
225+
// was dead. `silentChannels()` requires BOTH channels to have fired for
226+
// each table, and a silent one NAMES ITSELF in the diff.
227+
expect(noise.silentChannels(ALWAYS_READ_AUTHZ_TABLES)).toEqual([]);
265228
}, 30_000);
266229

267230
/** Drive one route as `user`, asserting the shared envelope, and hand back `data`. */

0 commit comments

Comments
 (0)