Skip to content

Commit f192c3b

Browse files
committed
test(drivers): a conformance run that discovers zero drivers is a failure, not an OK (#4646)
check-driver-conformance discovers driver packages from disk under a hardcoded DRIVERS_DIR. listDir swallows ENOENT and returns [], and all three invariants iterate the discovered set — CONSUMED over `drivers`, RECONCILED over LEDGER (empty since #4405, the intended steady state), CLASSIFIED not over drivers at all. A stale DRIVERS_DIR therefore printed `OK — 0 covered cell(s)` and exit 0. CI never had this exposure: lint.yml runs `pnpm check:driver-conformance`, which is `--self-test && audit`, and the self-test carried a discovery assertion. The false green was on the bare `node scripts/check-driver-conformance.mjs` the script's own header documents as a usage. Leaving the guard there was wrong twice over. It read `drivers.length >= 3 && drivers.includes('driver-sql')` — a hardcoded name and count inside the one script whose stated rule is that drivers come from disk and are never listed, so both wanted hand-editing on the next driver added or package moved, which is when the guard earns its keep. And its failure text named neither DRIVERS_DIR nor the stale path. DISCOVERED is now a fourth invariant in audit(), and its message names the directory searched. The self-test drives the invariant in both directions instead of standing in for it, and asserts nothing about which drivers exist. The case-set axis cannot rot this way and is left alone: CASE_SETS is a declared expectation, so a vanished spec/src/data fails CLASSIFIED's reverse direction with one error per case-set. The driver axis is disk-discovery with nothing declared to reconcile against — that asymmetry is why zero was reachable on one axis and not the other, and it is what DISCOVERED supplies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VTj56JLhVk585TA8Ez3yGK
1 parent 0a936ea commit f192c3b

2 files changed

Lines changed: 72 additions & 3 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
---
3+
4+
test(drivers): a conformance run that discovers zero drivers is a failure, not an OK (#4646)
5+
6+
`scripts/check-driver-conformance.mjs` discovers driver packages from disk under a
7+
hardcoded `DRIVERS_DIR`. `listDir` swallows ENOENT and returns `[]`, and all three
8+
invariants iterate the discovered set — CONSUMED over `drivers`, RECONCILED over
9+
`LEDGER` (empty since #4405, the intended steady state), CLASSIFIED not over drivers
10+
at all. So a stale `DRIVERS_DIR` produced `OK — 0 covered cell(s)` and exit 0.
11+
12+
CI never had this exposure: `lint.yml` runs `pnpm check:driver-conformance`, which is
13+
`--self-test && audit`, and the self-test carried a driver-discovery assertion. The
14+
false green was on the bare `node scripts/check-driver-conformance.mjs` the script's
15+
own header documents as a usage.
16+
17+
Two things were wrong with leaving the guard there. It read
18+
`drivers.length >= 3 && drivers.includes('driver-sql')` — a hardcoded name and count
19+
inside the one script whose stated rule is that drivers come from disk and are never
20+
listed, so both needed hand-editing on the next driver added or package moved, which
21+
is precisely when the guard earns its keep. And its failure text ("discovers driver
22+
packages from disk") named neither `DRIVERS_DIR` nor the stale path, leaving whoever
23+
tripped it to find that themselves.
24+
25+
DISCOVERED is now a fourth invariant in `audit()`, and the message names the directory
26+
it searched. The self-test drives the invariant in both directions instead of standing
27+
in for it, and asserts nothing about which drivers exist.
28+
29+
The case-set axis cannot rot this way and is left alone: `CASE_SETS` is a declared
30+
expectation, so a vanished `spec/src/data` fails CLASSIFIED's reverse direction with
31+
one error per case-set. The driver axis is disk-discovery with nothing declared to
32+
reconcile against — that asymmetry is why zero was reachable on one axis and not the
33+
other, and it is what DISCOVERED supplies.

scripts/check-driver-conformance.mjs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@
3434
//
3535
// ## Invariants
3636
//
37+
// DISCOVERED at least one driver package was found. Zero is not an empty
38+
// matrix, it is a broken run: the other three invariants iterate
39+
// the discovered set, so they all pass vacuously and this script
40+
// prints OK while checking nothing. The case-set axis cannot fail
41+
// this way -- CASE_SETS is a declared expectation, so a vanished
42+
// `spec/src/data` fails CLASSIFIED's reverse direction -- but the
43+
// driver axis is disk-discovery with nothing declared to
44+
// reconcile against, and RECONCILED's reverse direction walks
45+
// LEDGER, which is empty in the intended steady state.
3746
// CONSUMED every (driver x case-set) cell is either covered -- some file
3847
// under the package's `src/` imports the case-set's marker export
3948
// from `@objectstack/spec/data` -- or carries a DEBT/EXEMPT entry
@@ -157,6 +166,27 @@ function discoverDrivers() {
157166
.sort();
158167
}
159168

169+
/**
170+
* DISCOVERED — the errors for a discovery that found nothing.
171+
*
172+
* Split out from `audit()` so the self-test can drive the invariant itself
173+
* rather than a proxy for it. The previous guard lived only in the self-test
174+
* and read `drivers.length >= 3 && drivers.includes('driver-sql')` — a
175+
* hardcoded name and count inside the one script whose stated rule is that
176+
* drivers come from disk and are never listed. Both would have needed editing
177+
* the next time a driver is added or the packages move, which is exactly when
178+
* the guard matters.
179+
*/
180+
function discoveredErrors(drivers) {
181+
if (drivers.length) return [];
182+
return [
183+
`DISCOVERED: no driver package found under ${DRIVERS_DIR.slice(ROOT.length + 1)}/. `
184+
+ 'Either these packages moved and DRIVERS_DIR is stale, or they are gone. '
185+
+ 'Every other invariant iterates the discovered set, so a zero-driver run '
186+
+ 'reports OK having checked nothing — it fails here instead.',
187+
];
188+
}
189+
160190
/** Every `*-conformance.ts` under spec/src/data, and the case-set exports in it. */
161191
function discoverCaseSets() {
162192
const found = [];
@@ -217,6 +247,9 @@ function audit() {
217247
const errors = [];
218248
const rows = [];
219249

250+
// DISCOVERED — the precondition the other three iterate over.
251+
errors.push(...discoveredErrors(drivers));
252+
220253
// CLASSIFIED — both directions between CASE_SETS and the files on disk.
221254
const onDisk = discoverCaseSets();
222255
const classified = new Set(CASE_SETS.map((c) => c.marker));
@@ -370,9 +403,12 @@ function selfTest() {
370403
expect('discovers TEMPORAL_CASES on disk', found.includes('TEMPORAL_CASES'));
371404
expect('discovers PAGINATION_UNORDERED_CASES on disk', found.includes('PAGINATION_UNORDERED_CASES'));
372405

373-
// Discovery must find the drivers, for the same reason.
374-
const drivers = discoverDrivers();
375-
expect('discovers driver packages from disk', drivers.length >= 3 && drivers.includes('driver-sql'));
406+
// DISCOVERED: the invariant itself, in both directions, then against the
407+
// real tree. No driver name or count is asserted — the point of the gate is
408+
// that the set comes from disk.
409+
expect('a discovery that found nothing is an error', discoveredErrors([]).length === 1);
410+
expect('a discovery that found something is not', discoveredErrors(['driver-anything']).length === 0);
411+
expect('discovers driver packages from disk', discoverDrivers().length > 0);
376412

377413
if (failures.length) {
378414
for (const f of failures) console.error(` x self-test: ${f}`);

0 commit comments

Comments
 (0)