Skip to content

Commit 39eda2f

Browse files
os-warrenclaude
andauthored
docs(tests): record why no console-level capture predicate exists — measured, not assumed (#12145)
The three memory-driver fixtures in @objectstack/trigger-record-change keep their blanket `logger: { level: 'silent' }`. The console-level capture proposed to replace it was measured and captures ZERO of their 21 boot ERROR frames: ObjectLogger.write prefers the process streams and reaches `console` only as a browser/bundler fallback, so under vitest's `environment: 'node'` the console arm is unreachable. Records the measurement in the helper that would have grown the predicate, and at the three `level: 'silent'` lines someone would go to remove. Claude-Session: https://claude.ai/code/session_01W6HFzyH98W1YaQXhJUJt6o Co-authored-by: Claude <noreply@anthropic.com>
1 parent f64668d commit 39eda2f

4 files changed

Lines changed: 88 additions & 0 deletions

File tree

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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,3 +415,73 @@ export function captureExpectedCrossFieldRefusalNoise(
415415
},
416416
};
417417
}
418+
419+
/**
420+
* ═══════════════════════════════════════════════════════════════════════════
421+
* [#11571] ⛔ There is deliberately NO third, console-level predicate — the
422+
* kernel's ERROR frames never pass through `console` under Node
423+
* ═══════════════════════════════════════════════════════════════════════════
424+
*
425+
* ## The case that asked for one
426+
*
427+
* Three memory-driver fixtures in `@objectstack/trigger-record-change`
428+
* (`bulk-write-per-row-context`, `formula-context`, `multilookup-context`)
429+
* boot a kernel with no datasource, so every boot-time fail-soft read refuses.
430+
* Measured on this branch by floating each fixture to `level: 'info'`, their
431+
* ENTIRE ERROR surface is one invariant trio, repeated once per boot:
432+
*
433+
* | fixture | ERROR frames | distinct messages |
434+
* |------------------------------|---------------|-------------------|
435+
* | `bulk-write-per-row-context` | 15 (5 boots) | 3 |
436+
* | `formula-context` | 3 (1 boot) | 3 |
437+
* | `multilookup-context` | 3 (1 boot) | 3 |
438+
*
439+
* 1. `sys_metadata could NOT be read at boot …` (`objectql/src/plugin.ts`);
440+
* 2. `[wait] suspended wait-timer re-arm ABORTED …`
441+
* (`service-automation/src/builtin/wait-node.ts`);
442+
* 3. `[Automation] sys_automation_run could not be read at startup …`
443+
* (`service-automation/src/plugin.ts`).
444+
*
445+
* Neither predicate above can reach them: with no SqlDriver there is no
446+
* `refused a read on '<table>'` line — so no `pending` entry for the engine
447+
* gate to sit above — and no `Find operation failed` frame at all. Those three
448+
* fixtures therefore KEEP the blanket `logger: { level: 'silent' }` that the
449+
* two SqlDriver-backed fixtures were able to drop.
450+
*
451+
* ## Why the obvious third mechanism does not exist
452+
*
453+
* The proposal was a console-level capture: patch `console.error`/`console.warn`
454+
* for the file and float the kernel to `level: 'error'` so INFO/WARN stay
455+
* suppressed. **Measured: it captures ZERO of the 21 frames.**
456+
* `ObjectLogger.write` (`core/src/logger.ts`) prefers the process streams and
457+
* reaches console only as a fallback:
458+
*
459+
* if (stream) { // process.stderr for error/fatal
460+
* stream.write(line + '\n');
461+
* } else if (typeof console !== 'undefined') {
462+
* … console.error … // browsers / bundler shims ONLY
463+
* }
464+
*
465+
* Under vitest's `environment: 'node'`, `process.stderr` always exists, so the
466+
* `console` arm is unreachable there. A probe booting this exact plugin stack
467+
* at `level: 'error'` with all four sinks counted scored `console.error: 0,
468+
* console.warn: 0, process.stderr.write: 3, process.stdout.write: 0`.
469+
*
470+
* ⇒ The only variant that DOES intercept them patches `process.stderr.write`,
471+
* and it is refused here as disproportionate rather than as unworkable. Both
472+
* predicates above patch an OBJECT seam (`driver.logger`, `engine.logger`)
473+
* whose blast radius is one instance the fixture itself owns; a stream patch
474+
* is process-global and sits in the path of everything the worker writes —
475+
* the reporter's own diagnostics included — for as long as it is installed.
476+
* Twenty-one invariant per-boot frames that carry no per-test signal do not
477+
* buy a third capture mechanism of that reach, and #11569 (this module's
478+
* engine pass-through already lands in a silenced logger) means the two
479+
* mechanisms here want repairing before a third is stacked on them.
480+
*
481+
* ⛔ Nor is the repair a predicate pointed at `kernel.logger`: the kernel takes
482+
* a logger CONFIG, builds its own and hands it to the plugin loader and the
483+
* service context BY REFERENCE (`core/src/kernel.ts`), so a post-construction
484+
* swap propagates only partially. That yields a capture which misses frames
485+
* while asserting it does not — the exact inversion this module exists to
486+
* prevent, and strictly worse than the honest silence of a blanket mute.
487+
*/

packages/triggers/trigger-record-change/src/bulk-write-per-row-context.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ function makeDriver(): any {
113113
}
114114

115115
async function bootStack() {
116+
// [#11571] The blanket mute is kept here deliberately. With no datasource
117+
// this boot emits one invariant 3-frame ERROR trio that neither predicate
118+
// in `runtime/src/expected-read-refusal-noise.ts` can recognise, and the
119+
// console-level capture proposed to reach it intercepts ZERO: ObjectLogger
120+
// writes ERROR to `process.stderr`, never through `console`, under Node.
121+
// See that module's closing section for the measurement.
116122
const kernel = new ObjectKernel({ logger: { level: 'silent' } });
117123
await kernel.use(new ObjectQLPlugin());
118124
await kernel.use(new AutomationServicePlugin());

packages/triggers/trigger-record-change/src/formula-context.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ function makeDriver(): any {
6767

6868
describe('record-change context hydrates read-time formula fields (#3426)', () => {
6969
it('resolves a formula field ({record.full_name}) in a seeded flow record', async () => {
70+
// [#11571] The blanket mute is kept here deliberately. With no datasource
71+
// this boot emits one invariant 3-frame ERROR trio that neither predicate
72+
// in `runtime/src/expected-read-refusal-noise.ts` can recognise, and the
73+
// console-level capture proposed to reach it intercepts ZERO: ObjectLogger
74+
// writes ERROR to `process.stderr`, never through `console`, under Node.
75+
// See that module's closing section for the measurement.
7076
const kernel = new ObjectKernel({ logger: { level: 'silent' } });
7177
await kernel.use(new ObjectQLPlugin());
7278
await kernel.use(new AutomationServicePlugin());

packages/triggers/trigger-record-change/src/multilookup-context.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ function makeDriver(): any {
5656

5757
describe('record-change context hydrates multi-lookup from input (#1872)', () => {
5858
it('fires a record-after-create flow gated on a multi-lookup the driver did not echo', async () => {
59+
// [#11571] The blanket mute is kept here deliberately. With no datasource
60+
// this boot emits one invariant 3-frame ERROR trio that neither predicate
61+
// in `runtime/src/expected-read-refusal-noise.ts` can recognise, and the
62+
// console-level capture proposed to reach it intercepts ZERO: ObjectLogger
63+
// writes ERROR to `process.stderr`, never through `console`, under Node.
64+
// See that module's closing section for the measurement.
5965
const kernel = new ObjectKernel({ logger: { level: 'silent' } });
6066
await kernel.use(new ObjectQLPlugin());
6167
await kernel.use(new AutomationServicePlugin());

0 commit comments

Comments
 (0)