Skip to content

Commit 408c915

Browse files
committed
test(spec): pin the prerequisite exit code on both the unit and end-to-end legs
`expectRefusal` in the adoption test pinned `status` to 1, so the roster moves with the gate in the same PR. It now pins `EXIT_PREREQUISITE_NOT_MET` and the two sentences that make the number legible. `dist-freshness.test.ts` gains the spec-side half of the one-vocabulary pin: the constant is 3, it is distinct from a finding's 1, it agrees with the root frame read from `scripts/import-prerequisite.mjs` itself (inside the `scripts/**` radius `packages/spec` already declares), and the refusal text carries the fleet phrase plus the "Nothing was measured" paragraph. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
1 parent 84596ff commit 408c915

2 files changed

Lines changed: 102 additions & 7 deletions

File tree

‎packages/spec/scripts/dist-freshness-adoption.test.ts‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import os from 'node:os';
4343
import path from 'node:path';
4444
import { fileURLToPath } from 'node:url';
4545

46+
import { EXIT_PREREQUISITE_NOT_MET } from './lib/dist-freshness';
47+
4648
const HERE = path.dirname(fileURLToPath(import.meta.url));
4749
const PKG = path.resolve(HERE, '..');
4850
const REPO_ROOT = path.resolve(PKG, '../..');
@@ -181,9 +183,20 @@ function runGate(spec: string, script: string, args: string[] = []): SpawnSyncRe
181183
});
182184
}
183185

184-
/** Both halves of the refusal: it fired, and it named the right gate. */
186+
/**
187+
* All three halves of the refusal: it fired, it named the right gate, and it
188+
* answered with the code that says NOTHING WAS MEASURED.
189+
*
190+
* The third half moved in #19227 and this helper is where its pin lives. It was
191+
* `1` — a real finding's code — which is what let `dispatch-gates --ran`
192+
* reconcile a gate that refused before its first `.d.ts` read as a family that
193+
* ran. ⛔ Do not relax it back to "non-zero": that assertion passes for exactly
194+
* the defect this card removed.
195+
*/
185196
function expectRefusal(run: SpawnSyncReturns<string>, rerun: string): void {
186-
expect(run.status).toBe(1);
197+
expect(run.status).toBe(EXIT_PREREQUISITE_NOT_MET);
198+
expect(run.stderr).toContain('PREREQUISITE NOT MET');
199+
expect(run.stderr).toContain('Nothing was measured');
187200
expect(run.stderr).toContain('OLDER than packages/spec/src');
188201
expect(run.stderr).toContain('pnpm --filter @objectstack/spec build');
189202
expect(run.stderr).toContain(rerun);
@@ -245,7 +258,9 @@ describe('check:dual-source-exports refuses a stale dist (#7181)', () => {
245258
seedDualBaseline(tree.spec);
246259

247260
const run = runGate(tree.spec, DUAL, ['--update']);
248-
expect(run.status).toBe(1);
261+
// The WRITING half refuses with the same code (#19227): it wrote nothing,
262+
// so it has no ratchet verdict to report and did not measure one.
263+
expect(run.status).toBe(EXIT_PREREQUISITE_NOT_MET);
249264
expect(run.stderr).toContain('WRITE a baseline');
250265
expect(fs.readFileSync(path.join(tree.spec, DUAL_BASELINE), 'utf8')).toBe(SENTINEL);
251266
});

‎packages/spec/scripts/dist-freshness.test.ts‎

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,13 @@ import os from 'node:os';
3131
import path from 'node:path';
3232
import { fileURLToPath } from 'node:url';
3333

34-
import { inspectDistFreshness, packageDirLabel } from './lib/dist-freshness';
34+
import {
35+
EXIT_FINDINGS,
36+
EXIT_PREREQUISITE_NOT_MET,
37+
inspectDistFreshness,
38+
packageDirLabel,
39+
prerequisiteNotMetText,
40+
} from './lib/dist-freshness';
3541
import { declarationStamp } from '../../../scripts/check-regen-pending.mjs';
3642

3743
const HERE = path.dirname(fileURLToPath(import.meta.url));
@@ -470,7 +476,7 @@ describe('build-api-surface.ts refuses a stale dist end to end (#7122)', () => {
470476
seed(tree.spec, { distMtime: OLD, srcMtime: NEW });
471477

472478
const run = runGenerator(tree.spec, []);
473-
expect(run.status).toBe(1);
479+
expect(run.status).toBe(EXIT_PREREQUISITE_NOT_MET);
474480
expect(run.stderr).toContain('OLDER than packages/spec/src');
475481
expect(run.stderr).toContain('pnpm --filter @objectstack/spec build');
476482
expect(baseline(tree.spec)).toBe(SENTINEL);
@@ -484,7 +490,7 @@ describe('build-api-surface.ts refuses a stale dist end to end (#7122)', () => {
484490
seed(tree.spec, { distMtime: OLD, srcMtime: NEW });
485491

486492
const run = runGenerator(tree.spec, ['--check']);
487-
expect(run.status).toBe(1);
493+
expect(run.status).toBe(EXIT_PREREQUISITE_NOT_MET);
488494
expect(run.stderr).toContain('FALSE GREEN');
489495
expect(run.stdout).not.toContain('unchanged');
490496
});
@@ -495,9 +501,83 @@ describe('build-api-surface.ts refuses a stale dist end to end (#7122)', () => {
495501

496502
for (const args of [[], ['--check']]) {
497503
const run = runGenerator(tree.spec, args);
498-
expect(run.status).toBe(1);
504+
expect(run.status).toBe(EXIT_PREREQUISITE_NOT_MET);
499505
expect(run.stderr).toContain('no .d.ts declarations');
500506
}
501507
expect(baseline(tree.spec)).toBe(SENTINEL);
502508
});
503509
});
510+
511+
// ── The exit code the refusal answers with (#19227) ──────────────────────────
512+
//
513+
// Until this card the five dist-reading gates refused with `process.exit(1)` —
514+
// a real finding's code — while `scripts/import-prerequisite.mjs:250` declared
515+
// `EXIT_PREREQUISITE_NOT_MET = 3` for these exact two words. The consumer that
516+
// makes the difference concrete is `scripts/pm/dispatch-gates.mjs --ran`: it
517+
// derives its NOT-MEASURED class from the recorded code and counts every other
518+
// non-kill code as a family that RAN.
519+
//
520+
// These pins are the spec-side half of keeping one vocabulary. The root-side
521+
// half is that module's own `--self-test`, which asserts the same 3 there; the
522+
// value is hand-carried between the two (the reason is in the constant's
523+
// docblock), so BOTH pins have to exist or the two declarations can fork in
524+
// silence.
525+
describe('the prerequisite exit code, and the refusal text that explains it (#19227)', () => {
526+
it('is 3, the number this repo already means by PREREQUISITE NOT MET', () => {
527+
expect(EXIT_PREREQUISITE_NOT_MET).toBe(3);
528+
});
529+
530+
it("is distinct from a finding's — the whole point, and the half a machine reads", () => {
531+
expect(EXIT_FINDINGS).toBe(1);
532+
expect(EXIT_PREREQUISITE_NOT_MET).not.toBe(EXIT_FINDINGS);
533+
expect(EXIT_PREREQUISITE_NOT_MET).not.toBe(0);
534+
});
535+
536+
it('agrees with the root frame that declares it, read from that module', async () => {
537+
// The drift check, against the authority rather than against a copy of it.
538+
// `packages/spec` already declares `scripts/**` in CROSS_PACKAGE_TEST_INPUTS,
539+
// so this read is inside a declared radius.
540+
const frame = (await import('../../../scripts/import-prerequisite.mjs')) as {
541+
EXIT_PREREQUISITE_NOT_MET: number;
542+
EXIT_FINDINGS: number;
543+
};
544+
expect(EXIT_PREREQUISITE_NOT_MET).toBe(frame.EXIT_PREREQUISITE_NOT_MET);
545+
expect(EXIT_FINDINGS).toBe(frame.EXIT_FINDINGS);
546+
});
547+
548+
it('carries the fleet phrase, so it greps beside its siblings', () => {
549+
// `check-dts-closure`, `check-dual-build-cjs-loads`, `check-i18n-bundles`,
550+
// `check-i18n-coverage` and `check-closing-target-claim` all print
551+
// `<gate>: PREREQUISITE NOT MET — …`. A reader who learned the phrase from
552+
// one of those must find this one with the same grep.
553+
write('dist/contracts/index.d.ts', 'export {};', OLD);
554+
write('src/contracts/job-service.ts', 'export interface JobRunOutcome { ok: boolean }', NEW);
555+
556+
const verdict = inspectDistFreshness(sandbox, 'check', 'pnpm run check:example');
557+
expect(verdict.fresh).toBe(false);
558+
if (verdict.fresh) throw new Error('unreachable: the sandbox dist is stale');
559+
560+
const text = prerequisiteNotMetText('check:example', verdict);
561+
expect(text).toContain('check:example: PREREQUISITE NOT MET');
562+
// The verdict's own words are passed through, not paraphrased.
563+
expect(text).toContain(verdict.message);
564+
// And the sentence a number alone cannot carry.
565+
expect(text).toContain('Nothing was measured');
566+
expect(text).toContain(`Exit code ${EXIT_PREREQUISITE_NOT_MET}`);
567+
expect(text).toContain(`distinct from a finding's ${EXIT_FINDINGS}`);
568+
});
569+
570+
it('wraps a caller-composed refusal the same way — the partial-dist arm', () => {
571+
// `check:skill-examples` refuses a self-package whose `.d.ts` was never
572+
// emitted, which the mtime rule reads as fresh, so that refusal is not an
573+
// `inspectDistFreshness` verdict at all. Same trailer, one source.
574+
const text = prerequisiteNotMetText('check:skill-examples [spec]', {
575+
headline: 'a self-package on this surface has no declarations',
576+
detail: '\n - packages/client-react/dist/index.d.ts (missing)',
577+
});
578+
expect(text).toContain('check:skill-examples [spec]: PREREQUISITE NOT MET');
579+
expect(text).toContain('a self-package on this surface has no declarations');
580+
expect(text).toContain('packages/client-react/dist/index.d.ts (missing)');
581+
expect(text).toContain('Nothing was measured');
582+
});
583+
});

0 commit comments

Comments
 (0)