Skip to content

Commit 72c2036

Browse files
committed
test(runtime): boot the job-data-reach suite on the migrated sqlite :memory: backend
#5704 migrated this project's test backends to sqlite `:memory:` and ruled that only the two files in `scripts/driver-memory-census.ledger.json` keep `@objectstack/driver-memory`, each being one arm of a cross-family pin that cannot run on SQL. This suite is neither — it needs *a* store, not that store — so `pnpm check:driver-memory-census` was right to refuse it as an unledgered arrival (#6664). Migrated rather than ledgered: the census stays at 2 ruled consumers and the ledger is untouched. The card's own reproduction used the in-memory driver because that is what the reporter had in hand; that was a manual probe, never a constraint on this rig. Provisioning `sweep_note` and nothing else makes the engine's single-tenant probe read an absent `sys_organization`, so the expected refusal is withheld and asserted through `expected-read-refusal-noise.ts` (#10629) rather than muted. The probe is memoised behind the first data operation, so the three context-shape tests that never touch the store declare that with `harness({ touchesStore: false })` — the withholding is unconditional either way, only the must-have-fired set narrows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5WBDtaUnoz5XuJ6jk8pQ5
1 parent 95f0cfb commit 72c2036

1 file changed

Lines changed: 77 additions & 12 deletions

File tree

‎packages/runtime/src/app-plugin.job-data-reach.test.ts‎

Lines changed: 77 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@
4141
* boot and the module-scope slot stays empty — silently. A fix proved only on
4242
* the TS-config path would be a second escape with the same blind spot, so the
4343
* artifact boot is driven through the REAL `loadArtifactBundle` here.
44+
*
45+
* ## The backend, and why it is sqlite `:memory:`
46+
*
47+
* These tests need *a* store — nothing here is about any one driver's behaviour.
48+
* #5704 migrated this project's test backends to sqlite `:memory:` and ruled
49+
* that only the two files in `scripts/driver-memory-census.ledger.json` keep
50+
* `@objectstack/driver-memory`, each being one arm of a cross-family pin that
51+
* genuinely cannot run on SQL. This file is neither, so it boots on the migrated
52+
* backend. ⛔ Do not "simplify" it back onto the in-memory driver: that is an
53+
* unledgered arrival and `pnpm check:driver-memory-census` refuses it (#6664).
54+
* The card's own reproduction used the in-memory driver because that is what the
55+
* reporter had in hand — a manual probe, never a constraint on this rig.
4456
*/
4557

4658
import { describe, it, expect, vi, afterEach } from 'vitest';
@@ -51,11 +63,15 @@ import type { PluginContext } from '@objectstack/core';
5163
import type { JobHandler } from '@objectstack/spec/contracts';
5264
import { defineJob } from '@objectstack/spec/system';
5365
import { ObjectQL } from '@objectstack/objectql';
54-
import { InMemoryDriver } from '@objectstack/driver-memory';
66+
import { SqlDriver } from '@objectstack/driver-sql';
5567
import { CronJobAdapter } from '@objectstack/service-job';
5668
import { AppPlugin } from './app-plugin.js';
5769
import { loadArtifactBundle } from './load-artifact-bundle.js';
5870
import type { JobHandlerContext } from './job-handler-context.js';
71+
import {
72+
captureExpectedReadRefusals,
73+
type ExpectedReadRefusalCapture,
74+
} from './expected-read-refusal-noise.js';
5975

6076
/** The record a scheduled sweep is supposed to be able to write. */
6177
const NOTE = {
@@ -85,30 +101,78 @@ interface Harness {
85101
warnLogs: () => string[];
86102
}
87103

88-
const live: Array<{ engine?: ObjectQL; adapter?: CronJobAdapter; dir?: string }> = [];
104+
const live: Array<{
105+
engine?: ObjectQL;
106+
adapter?: CronJobAdapter;
107+
driver?: SqlDriver;
108+
dir?: string;
109+
noise?: ExpectedReadRefusalCapture;
110+
/** The channels this test's path MUST have provoked — see `harness()`. */
111+
requiredChannels?: readonly string[];
112+
}> = [];
113+
114+
/**
115+
* [#10629] This fixture provisions `sweep_note` and nothing else, so the
116+
* engine's own single-tenant probe (`ObjectQL.probeInstallOrganizations`) reads
117+
* a `sys_organization` that was never created. The probe is fail-soft by
118+
* construction, but the driver and the engine each log the fault on the way out.
119+
* Withheld and ASSERTED rather than muted — see `expected-read-refusal-noise.ts`.
120+
*/
121+
const ABSENT_TENANCY_TABLE = 'sys_organization';
89122

90123
afterEach(async () => {
91124
for (const entry of live.splice(0)) {
92125
try { await entry.adapter?.destroy(); } catch { /* noop */ }
93126
try { await entry.engine?.destroy(); } catch { /* noop */ }
127+
try { await entry.driver?.disconnect(); } catch { /* noop */ }
94128
if (entry.dir) rmSync(entry.dir, { recursive: true, force: true });
129+
// A capture nobody asserts is a mute. The probe is memoised behind the
130+
// FIRST data operation, so only the paths that actually touch the store
131+
// provoke it — `silentChannels(required)` is the API's own answer to a
132+
// table read on some of a file's paths and not others. The withholding
133+
// is unconditional either way; only the must-have-fired set narrows.
134+
if (entry.noise) {
135+
expect(entry.noise.silentChannels(entry.requiredChannels ?? [ABSENT_TENANCY_TABLE])).toEqual([]);
136+
}
95137
}
96138
});
97139

98-
/** A real engine over the in-memory driver, with `sweep_note` registered. */
99-
async function bootEngine(): Promise<ObjectQL> {
100-
const driver = new InMemoryDriver();
140+
/**
141+
* A real engine over the MIGRATED test backend — sqlite `:memory:` (#5704) —
142+
* with `sweep_note` provisioned and registered.
143+
*/
144+
async function bootEngine(): Promise<{ engine: ObjectQL; driver: SqlDriver; noise: ExpectedReadRefusalCapture }> {
145+
const driver = new SqlDriver({
146+
client: 'better-sqlite3',
147+
connection: { filename: ':memory:' },
148+
useNullAsDefault: true,
149+
});
150+
// Installed before the driver runs a statement and before the engine issues
151+
// a read — the two sinks the expected refusal travels out on.
152+
const noise = captureExpectedReadRefusals([ABSENT_TENANCY_TABLE]);
153+
noise.captureDriver(driver);
154+
await driver.initObjects([NOTE as never]);
101155
const engine = new ObjectQL();
156+
noise.captureEngine(engine);
102157
engine.registerDriver(driver as never, true);
103158
await engine.init();
104159
engine.registry.registerObject(NOTE as never);
105-
return engine;
160+
return { engine, driver, noise };
106161
}
107162

108-
async function harness(): Promise<Harness> {
109-
const engine = await bootEngine();
163+
/**
164+
* @param opts.touchesStore whether this test's path performs a data operation.
165+
* `true` (the default) requires the tenancy probe to have fired and been
166+
* withheld; a context-shape test that never reads or writes passes `false`,
167+
* which keeps the withholding and drops only the must-have-fired requirement.
168+
*/
169+
async function harness(opts: { touchesStore?: boolean } = {}): Promise<Harness> {
170+
const { engine, driver, noise } = await bootEngine();
110171
const adapter = new CronJobAdapter();
111-
live.push({ engine, adapter });
172+
live.push({
173+
engine, adapter, driver, noise,
174+
requiredChannels: opts.touchesStore === false ? [] : [ABSENT_TENANCY_TABLE],
175+
});
112176

113177
const readyHooks: Array<() => Promise<void>> = [];
114178
const ctx = {
@@ -181,7 +245,8 @@ describe('#14094 — a declarative job handler has data reach (TS-config path)',
181245
});
182246

183247
it('the context is the pre-#14094 set PLUS exactly `ql` and `logger`', async () => {
184-
const h = await harness();
248+
// Reads nothing and writes nothing — this one is about the shape.
249+
const h = await harness({ touchesStore: false });
185250
const seen: Array<Record<string, unknown>> = [];
186251

187252
const plugin = new AppPlugin({
@@ -209,7 +274,7 @@ describe('#14094 — a declarative job handler has data reach (TS-config path)',
209274
});
210275

211276
it('`data` from a manual trigger still reaches the handler beside the new members', async () => {
212-
const h = await harness();
277+
const h = await harness({ touchesStore: false });
213278
const seen: Array<Record<string, unknown>> = [];
214279
const plugin = new AppPlugin({
215280
id: 'com.test.job-reach',
@@ -318,7 +383,7 @@ export const meta = { builtAt: '2026-09-01T00:00:00.000Z' };
318383

319384
describe('#14094 — additivity (Zone 1.1)', () => {
320385
it('a handler written against the PRE-#14094 context runs unchanged, byte for byte', async () => {
321-
const h = await harness();
386+
const h = await harness({ touchesStore: false });
322387
const calls: Array<{ jobId: string; data?: unknown }> = [];
323388

324389
// Verbatim the shape `IJobService`'s `JobHandler` declares — the type an

0 commit comments

Comments
 (0)