|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | +// |
| 3 | +// Structural wiring guard for the suite's whole reason to exist (#3583 §5 D5). |
| 4 | +// |
| 5 | +// ## Why a test and not a comment |
| 6 | +// |
| 7 | +// The defect this catches is not a wrong result — it is a MISSING CALL, and a |
| 8 | +// missing call produces no failing assertion anywhere. Every command's own |
| 9 | +// tests pass, the rule's unit tests pass, and the only symptom is that |
| 10 | +// `os validate`, `os lint` and `os compile` disagree about the same stack. |
| 11 | +// |
| 12 | +// That is not hypothetical. `validateReadonlyFlowWrites` was hand-wired into |
| 13 | +// `validate` and `compile` and never into `lint` from #3425 until #4394 — and |
| 14 | +// because it GATES (`flow-update-readonly-field` is an `error`), `os lint` |
| 15 | +// spent that whole time returning clean for stacks `os compile` refuses. The |
| 16 | +// suite's own header had been naming it as the standing proof of the drift the |
| 17 | +// suite exists to end, which is a comment doing a test's job. #4394 removed |
| 18 | +// that instance; this file removes the failure MODE, so the next rule cannot |
| 19 | +// repeat it silently (#4384). |
| 20 | +// |
| 21 | +// ## The invariant |
| 22 | +// |
| 23 | +// `os lint` ⊇ `os compile`'s gate set. `lint` is the cheap pre-flight and |
| 24 | +// `compile` is the gate; a green `lint` followed by a red `compile` makes the |
| 25 | +// pre-flight worthless — and for an agent it is worse than worthless, because |
| 26 | +// the remaining options are re-verifying everything (slow) or learning to |
| 27 | +// distrust the signal (dangerous). |
| 28 | +// |
| 29 | +// ## Why it scans source |
| 30 | +// |
| 31 | +// vitest inlines imports through its transform, so a spy on `@objectstack/lint` |
| 32 | +// cannot prove which symbols a command file actually reaches for — the same |
| 33 | +// reason `lazy-deps.test.ts` scans `src/` rather than probing a module cache. |
| 34 | +// Behavioural coverage of what the suite CONTAINS lives in |
| 35 | +// `@objectstack/lint`'s `reference-integrity-suite.test.ts`; this file guards |
| 36 | +// only the seam between that suite and the three call sites. |
| 37 | + |
| 38 | +import { readFileSync } from 'node:fs'; |
| 39 | +import { dirname, join } from 'node:path'; |
| 40 | +import { fileURLToPath } from 'node:url'; |
| 41 | +import { describe, it, expect } from 'vitest'; |
| 42 | +import { REFERENCE_INTEGRITY_RULES } from '@objectstack/lint'; |
| 43 | + |
| 44 | +const commandsDir = dirname(fileURLToPath(import.meta.url)); |
| 45 | + |
| 46 | +/** The three commands that must hold a stack to the same author-time bar. */ |
| 47 | +const AUTHORING_COMMANDS = ['validate.ts', 'lint.ts', 'compile.ts'] as const; |
| 48 | + |
| 49 | +const sourceOf = (file: string) => readFileSync(join(commandsDir, file), 'utf8'); |
| 50 | + |
| 51 | +describe('reference-integrity wiring (#3583 §5 D5, #4384)', () => { |
| 52 | + it.each(AUTHORING_COMMANDS)('%s runs the suite', (file) => { |
| 53 | + expect(sourceOf(file)).toMatch(/\bvalidateReferenceIntegrity\s*\(/); |
| 54 | + }); |
| 55 | + |
| 56 | + // The regression itself. A second, per-rule call site is how a rule ends up |
| 57 | + // on two commands out of three, and it always starts with importing that rule |
| 58 | + // by name — so the import is what this asserts on. Catching the call instead |
| 59 | + // would miss the window between adding the import and adding the second call. |
| 60 | + it.each(AUTHORING_COMMANDS)('%s does not import a suite member directly', (file) => { |
| 61 | + const source = sourceOf(file); |
| 62 | + const reached = REFERENCE_INTEGRITY_RULES.map((r) => r.name).filter((name) => |
| 63 | + new RegExp( |
| 64 | + String.raw`^\s*import\s[^;]*\b${name}\b[^;]*from\s*['"]@objectstack/lint['"]`, |
| 65 | + 'm', |
| 66 | + ).test(source), |
| 67 | + ); |
| 68 | + expect( |
| 69 | + reached, |
| 70 | + `${file} imports suite member(s) directly — run them via validateReferenceIntegrity instead, ` + |
| 71 | + `or all three commands will drift the way validateReadonlyFlowWrites did (#4394)`, |
| 72 | + ).toEqual([]); |
| 73 | + }); |
| 74 | + |
| 75 | + // Guards the guard: were the suite ever emptied or the export renamed, the |
| 76 | + // assertions above would pass vacuously while checking nothing. |
| 77 | + it('the suite is non-empty, so the assertions above are not vacuous', () => { |
| 78 | + expect(REFERENCE_INTEGRITY_RULES.length).toBeGreaterThan(0); |
| 79 | + // The rule whose absence from `os lint` is the reason this file exists. |
| 80 | + expect(REFERENCE_INTEGRITY_RULES.map((r) => r.name)).toContain('validateReadonlyFlowWrites'); |
| 81 | + }); |
| 82 | +}); |
0 commit comments