|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * `--hookTimeout` must never return a GREEN from this package again (#18788). |
| 5 | + * |
| 6 | + * ## What was measured, and why the green was the defect |
| 7 | + * |
| 8 | + * `packages/cli` declares vitest `test.projects`. vitest 4.1.11's |
| 9 | + * `resolveProjects` carries a CLOSED twenty-name allowlist of CLI options into a |
| 10 | + * project's test config and applies everything else to the ROOT config only. |
| 11 | + * `testTimeout` is on that list; `hookTimeout` and `teardownTimeout` are not. So |
| 12 | + * a probe `beforeAll` sleeping 500ms passed under `--hookTimeout=1` here — exit |
| 13 | + * 0, `1 passed`, `tests 505ms` — with and without `--project`, while the same |
| 14 | + * probe body under `@objectstack/plugin-dev`, which declares no `projects`, |
| 15 | + * exited 1 with `Hook timed out in 1ms.` |
| 16 | + * |
| 17 | + * ⭐ That flag is the instrument this repo's own prior art reaches for to |
| 18 | + * witness that a cold load has left every clocked window: the header of |
| 19 | + * `packages/plugins/plugin-dev/src/dev-plugin-security-enforcement-warning.test.ts` |
| 20 | + * reads a green under `--hookTimeout=1` AS THE PASS. In this package that green |
| 21 | + * could not fail, and a reading that cannot fail is indistinguishable from one |
| 22 | + * that passed. `packages/cli/src/commands/datasource/envelope-unwrap.test.ts` |
| 23 | + * already records the same measurement and had to fall back to a source |
| 24 | + * assertion because of it. |
| 25 | + * |
| 26 | + * `vitest.config.ts` now REFUSES such a run at config load, through the shared |
| 27 | + * `runProjectCliOverridePreflight` in `packages/qa/vitest-filter-preflight` |
| 28 | + * (whose header carries the mechanism, the full measured table and why the value |
| 29 | + * is NOT forwarded into the projects instead). |
| 30 | + * |
| 31 | + * ## ⛔ Why this pin spawns, and what each leg controls for |
| 32 | + * |
| 33 | + * A source assertion that the config still contains the call would pass against |
| 34 | + * a preflight that refuses nothing. The subject here is a BEHAVIOUR of this |
| 35 | + * package's own config load, so it is asked of a real `vitest` child — the same |
| 36 | + * instrument, and the same `execFileSync` + `childEnv()` shape, that |
| 37 | + * `vitest-tiers-partition.test.ts` already uses to ask vitest for its file lists. |
| 38 | + * |
| 39 | + * ⭐ Every leg below owes a control that can go the other way, because that is |
| 40 | + * the exact error this card is about: |
| 41 | + * |
| 42 | + * - `--hookTimeout` and `--teardownTimeout` must REFUSE — if the wiring is |
| 43 | + * deleted or the preflight stops refusing, these two go green and fail here; |
| 44 | + * - a run naming NO override must PASS — so the refusal is not unconditional, |
| 45 | + * which a pin asserting only the red half could never tell apart; |
| 46 | + * - `--testTimeout` must PASS — it is on vitest's allowlist and does reach the |
| 47 | + * projects (measured in this package: 500ms in a test BODY exits 1 with |
| 48 | + * `Test timed out in 1ms.`). This is what proves the detector discriminates |
| 49 | + * by ALLOWLIST MEMBERSHIP rather than by "a flag whose name ends in |
| 50 | + * Timeout", and a detector that refused it would be breaking a working |
| 51 | + * instrument. |
| 52 | + * |
| 53 | + * Runs in the `unit` tier and needs no built `dist/`: `vitest list --filesOnly` |
| 54 | + * only globs, and the two refusing legs abort at config load before that. |
| 55 | + * Importing `node:child_process` to ask vitest a question is not spawning the |
| 56 | + * CLI — the same classification `vitest-tiers-partition.test.ts` carries. |
| 57 | + * |
| 58 | + * ## ⚠️ The DECLARED overlap with the population sweep |
| 59 | + * |
| 60 | + * `packages/qa/vitest-filter-preflight/test/config-wiring-sweep.test.ts` asks |
| 61 | + * two of these four questions of EVERY package that declares `projects`, this |
| 62 | + * one included: that `--hookTimeout` is really refused, and that a run naming no |
| 63 | + * override really is not. That sweep owns the POPULATION — it derives it, so a |
| 64 | + * ninth package is caught on the PR that adds it — and it is where those two |
| 65 | + * legs are maintained. |
| 66 | + * |
| 67 | + * ⭐ What is NOT duplicated, and why this file is the one that owns it: the |
| 68 | + * DISCRIMINATOR. `--testTimeout` is on vitest's allowlist and does reach the |
| 69 | + * projects here — measured in this package, 500ms in a test body exits 1 with |
| 70 | + * `Test timed out in 1ms.` — so a detector that refused it would be breaking a |
| 71 | + * working instrument, and `--teardownTimeout` is the second knob the same |
| 72 | + * allowlist drops. Both readings are about THIS package's measured table, which |
| 73 | + * is the card's subject, so they are asked here rather than eight times. |
| 74 | + * ⛔ The two overlapping legs are kept rather than deleted because this file |
| 75 | + * must stand on its own as `packages/cli`'s record of the defect: a reader who |
| 76 | + * finds a refusal here needs the control that says it is not unconditional, in |
| 77 | + * the same file, or the record is exactly the kind of one-sided reading this |
| 78 | + * card is about. |
| 79 | + */ |
| 80 | + |
| 81 | +import { execFileSync } from 'node:child_process'; |
| 82 | +import { createRequire } from 'node:module'; |
| 83 | +import { dirname, resolve } from 'node:path'; |
| 84 | +import { fileURLToPath } from 'node:url'; |
| 85 | +import { describe, expect, it } from 'vitest'; |
| 86 | +import { childEnv } from './helpers/serve-process.js'; |
| 87 | + |
| 88 | +const HERE = dirname(fileURLToPath(import.meta.url)); |
| 89 | +const PKG = resolve(HERE, '..'); |
| 90 | +const require = createRequire(import.meta.url); |
| 91 | +const VITEST_ENTRY = resolve(dirname(require.resolve('vitest/package.json')), 'vitest.mjs'); |
| 92 | + |
| 93 | +/** The refusal's own headline, as `renderInertOverrideNotice` prints it. */ |
| 94 | +const REFUSAL = 'TIMEOUT OVERRIDE CANNOT REACH THIS PACKAGE'; |
| 95 | + |
| 96 | +interface ChildRun { |
| 97 | + readonly status: number; |
| 98 | + readonly output: string; |
| 99 | +} |
| 100 | + |
| 101 | +/** |
| 102 | + * `vitest list --filesOnly [flags]` against THIS package, never throwing. |
| 103 | + * |
| 104 | + * ⛔ `execFileSync` throws on a non-zero exit, and the non-zero exit is half of |
| 105 | + * what this file measures — so the status is read off the thrown error rather |
| 106 | + * than allowed to abort the case. `stdout` and `stderr` are joined because the |
| 107 | + * refusal is written to stderr at config load while vitest's own report of it |
| 108 | + * lands separately, and this file asserts on the text, not on the stream. |
| 109 | + */ |
| 110 | +function runVitestList(...flags: string[]): ChildRun { |
| 111 | + const args = [VITEST_ENTRY, 'list', '--filesOnly', ...flags]; |
| 112 | + try { |
| 113 | + const stdout = execFileSync(process.execPath, args, { |
| 114 | + cwd: PKG, |
| 115 | + env: childEnv(), |
| 116 | + encoding: 'utf8', |
| 117 | + stdio: ['ignore', 'pipe', 'pipe'], |
| 118 | + }); |
| 119 | + return { status: 0, output: stdout }; |
| 120 | + } catch (error) { |
| 121 | + const e = error as { status?: number | null; stdout?: string; stderr?: string }; |
| 122 | + // ⛔ A child that never ran (spawn failure) has a null status. Reporting |
| 123 | + // that as a refusal would be this card's own defect in a new place. |
| 124 | + expect(typeof e.status).toBe('number'); |
| 125 | + return { status: e.status ?? -1, output: `${e.stdout ?? ''}${e.stderr ?? ''}` }; |
| 126 | + } |
| 127 | +} |
| 128 | + |
| 129 | +describe('#18788 a CLI timeout override that cannot reach a project is refused, not passed', () => { |
| 130 | + it('refuses `--hookTimeout`, naming the package and a spelling that does bite', () => { |
| 131 | + const run = runVitestList('--hookTimeout=1'); |
| 132 | + |
| 133 | + expect(run.status).not.toBe(0); |
| 134 | + expect(run.output).toContain(REFUSAL); |
| 135 | + expect(run.output).toContain('--hookTimeout'); |
| 136 | + // The #17978 finding against the sibling preflight, held here too: a shared |
| 137 | + // notice must name the package it is refusing FOR, or it sends the reader to |
| 138 | + // a command that runs the wrong suite. |
| 139 | + expect(run.output).toContain('@objectstack/cli'); |
| 140 | + // ⛔ Refusing without a remedy is a bare 404 (AGENTS.md, Route & surface |
| 141 | + // ownership §3). Both spellings named here are measured in this package. |
| 142 | + expect(run.output).toContain('extends: true'); |
| 143 | + expect(run.output).toContain('beforeAll(fn, 1)'); |
| 144 | + }); |
| 145 | + |
| 146 | + it('refuses `--teardownTimeout` — the other knob the same allowlist drops', () => { |
| 147 | + const run = runVitestList('--teardownTimeout=1'); |
| 148 | + |
| 149 | + expect(run.status).not.toBe(0); |
| 150 | + expect(run.output).toContain(REFUSAL); |
| 151 | + expect(run.output).toContain('--teardownTimeout'); |
| 152 | + }); |
| 153 | + |
| 154 | + it('⭐ CONTROL — a run naming no override passes and says nothing', () => { |
| 155 | + // Without this leg the three above are satisfied by a config that refuses |
| 156 | + // everything, which would be a worse defect than the one being fixed. |
| 157 | + const run = runVitestList(); |
| 158 | + |
| 159 | + expect(run.status).toBe(0); |
| 160 | + expect(run.output).not.toContain(REFUSAL); |
| 161 | + // It really listed something, so the zero is a reading and not an empty run. |
| 162 | + expect(run.output).toMatch(/\.test\.ts/); |
| 163 | + }); |
| 164 | + |
| 165 | + it('⭐ CONTROL — `--testTimeout` is on vitest’s allowlist, reaches the projects, and is NOT refused', () => { |
| 166 | + // The discriminator. `--testTimeout=1` bounds test bodies here for real |
| 167 | + // (measured: 500ms in a test body exits 1 with `Test timed out in 1ms.`), |
| 168 | + // so refusing it would break a working instrument. `list` collects without |
| 169 | + // running, so the budget is never applied and this stays a config-load |
| 170 | + // question. |
| 171 | + const run = runVitestList('--testTimeout=1'); |
| 172 | + |
| 173 | + expect(run.status).toBe(0); |
| 174 | + expect(run.output).not.toContain(REFUSAL); |
| 175 | + }); |
| 176 | +}); |
0 commit comments