Skip to content

Commit 9c922b5

Browse files
test(cli): write oclif.manifest.json once per integration run, so a CLI spawn reads its command table (#26256)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X7p8QUpB5pvxJHFoRiWTJwu)_ Part of #23181 (slice 14). The card is a series and stays open. Clause-②: no Metadata-protocol: none Test infrastructure only: `skip-changeset`. `@objectstack/cli`'s `files[]` is `dist`, `README.md` and `CHANGELOG.md`, unchanged, and nothing in it changes. The diff touches `packages/cli/vitest.config.ts`, one new module under `packages/cli/test/helpers/`, one test file under `packages/cli/test/`, and the root `.gitignore`. No build step, no new gate, no product code. ## What this does Seat ruling A on slice 11's open question (report 6418998910 on #23181). A vitest `globalSetup`, `packages/cli/test/helpers/oclif-manifest.global-setup.ts`, writes `packages/cli/oclif.manifest.json` from source once per run of the `integration` project and removes it at teardown. The file is gitignored. This package ships no oclif manifest, so every CLI process builds its command table by importing all 72 command modules. Through `tsx bin/run-dev.js` that is a floor of about 8 s per spawn. With the manifest beside `package.json`, oclif reads the table from it and imports only the command it runs. Outside the one file ruling A1 changed (below), spawn shape, the `bin/run-dev.js` entry and `NODE_ENV` are unchanged. ### Why the written manifest is transparent - It is the object oclif's own root plugin builds in memory when no manifest exists (`new Plugin({ isRoot: true, root })`, loaded in a `tsx` child under `NODE_ENV=development`, as `bin/run-dev.js` runs), with `errorOnManifestCreate` on so that a command module that fails to load fails the generation loudly, naming the module. - Two keys are removed from every command: `isESM` and `relativePath`. This is required. When a cached command carries both, `@oclif/core` 5.1.2 imports `root + relativePath` verbatim, and a manifest generated from source records `src/commands/ID.ts`. Measured control: with those keys kept, `node bin/run.js generate object order_line --dry-run` imports `packages/cli/src/commands/generate.ts` and exits 1 (`ModuleLoadError`). With them removed, oclif takes its no-manifest path. Its debug log reads `(import) CLI/dist/commands/generate.js` under `bin/run.js` and `(import) CLI/src/commands/generate.ts` under `bin/run-dev.js`. So the manifest supplies the command table and nothing about where a command's code comes from. - Byte comparison, without and with the manifest, 9 invocations x 2 entries (`tsx bin/run-dev.js` and `node bin/run.js`): `--version`, `--help`, `serve --help`, `generate object --help`, `generate object order_line --dry-run`, `generate object class` (exit 1), `generate object` (missing argument, exit 1), `validate` in `examples/app-todo`, and an unknown command (exit 2). stdout, stderr and exit code are identical in 52 of 54 streams. The 2 that differ are `validate`'s stdout on both entries, and they differ only in the two printed elapsed-time lines (`Load time: 148ms` against `164ms`, and `Validation passed (279ms)` against `(278ms)`). ### Scope: the queue population only Under `OS_TEST_TIERS=nightly` neither project carries the setup. The e2e tier holds the suites that pin what a manifest changes, which is `Config.load()` importing every command module (`run-dev-unbuilt-workspace.e2e`, the two `*-stderr-nonblocking.e2e` files). The product ships no manifest, so that is the shape those pins keep seeing. The setup is wired per project, not at the root: with `extends: true` a root `globalSetup` would run for the root project and both tier projects, three times. ### Ownership, nesting and cleanup (the order's three hypotheses) - **Written once, before any worker spawns.** vitest runs every project's `globalSetup` before `pool.runTests`. The file is written to a temporary sibling and renamed over the target, so a spawn reads a whole manifest or none. - **Nested vitest runs leave it alone.** The owning run sets `OS_TEST_OCLIF_MANIFEST_OWNER` in its own process. Workers and `childEnv()` children inherit it, and a vitest started inside the run does nothing. Measured with a throwaway integration test that spawned `vitest run --project integration` through `childEnv()`: the worker and the child env both carried the owner pid, the manifest's owner was the same before and after (3443), and the nested run took 3.5 s, with no 9 s generation. - **Two runs on one checkout.** Each run records its pid in the file (`writtenBy.pid`) and removes the file only while that pid is still its own. Measured with two `--project integration` runs started 4 s apart: the owner went 3691 then 3788, both runs were green (5/5 and 7/7), and after both the file was gone. The run left without a manifest continues at the old floor with the same output. - **Crash.** Every integration run regenerates unconditionally, and a `unit`-only run removes a file whose writer is dead. Measured: after a `kill -9` of the owning vitest, the file stayed with a dead owner. The next `--project unit` run removed it. A planted dead-owner file (`pid 999999`) was regenerated by the next integration run (owners seen: 999999, then 4631) and removed at its teardown. After a `SIGINT` mid-run the file was gone (teardown, or the `exit` listener for a run that ends without one). ## One test file changed, by seat ruling A1 `test/published-entry-node-env-source-reroute.test.ts` (#12271) spawned `node bin/run.js --version`. That reached the reroute only because, with no manifest, `Config.load()` imports every command module. With the manifest present, `--version` loads no command. In the full run below, its CONTROL stopped reproducing the card, and its three positive legs passed over a fixture that armed nothing. Its spawns now run `explain object`, a real command whose module imports the trapped `chalk`, with `OCLIF_COLUMNS=100000` so oclif renders the load failure on one line. With a manifest that failure is the error `handle()` prints, hard-wrapped at 80 columns on a pipe, and a wrap can split the signature or the `src/commands` path. No assertion was changed or loosened. Legs measured both ways, against the same armed fixture (`exit` / count of the card signature / count of `packages/cli/src/commands`): | leg | no manifest (main's shape) | manifest present | |:--|:--|:--| | CONTROL, neutralised, `NODE_ENV=development` | 1 / 4 / 3 | 1 / 1 / 1 | | CONTROL, neutralised, `NODE_ENV=production` | 0 / 0 / 0 | 0 / 0 / 0 | | `NODE_ENV=development` | 0 / 0 / 0 | 0 / 0 / 0 | | `NODE_ENV=test` | 0 / 0 / 0 | 0 / 0 / 0 | | `NODE_ENV=production` | 0 / 0 / 0 | 0 / 0 / 0 | So the control still goes red on the trap on main's shape, and also with the manifest. The file under vitest: 5/5 passed twice under this config (manifest written) and 5/5 twice under the base config (no manifest). ## Measurements All on one shared 4-vCPU container under `os-verify-lock`. Per-file numbers are the default reporter's module line. **Per spawn** (the byte-comparison runs above): `tsx bin/run-dev.js` 7.6 to 9.1 s without, and with the manifest 0.48 to 0.55 s for `--version` and help, 1.7 to 3.9 s for commands that run. `node bin/run.js` 2.1 to 2.6 s without, 0.15 to 2.1 s with. One generation: 8.8 to 9.9 s. **Slice 11's spawn family** (PR #26174's ten files), interleaved A/B, `--project integration --maxWorkers=2`, BASE = `c7aa9cde` `vitest.config.ts`, HEAD = this branch, N=3: | file | base, 3 runs (s) | head, 3 runs (s) | median ratio | |:--|--:|--:|--:| | `test/build-callable-names.test.ts` | 87.6 / 87.4 / 80.9 | 35.2 / 35.2 / 37.3 | 2.5x | | `test/generate-refuses-unimportable-alias.test.ts` | 67.6 / 64.0 / 61.4 | 15.7 / 15.8 / 16.6 | 4.1x | | `test/build-runtime-bundle-bodyless.test.ts` | 63.2 / 59.2 / 54.8 | 24.4 / 23.1 / 24.9 | 2.4x | | `test/build-strict-body-count.test.ts` | 56.0 / 55.9 / 54.6 | 23.0 / 23.7 / 25.8 | 2.4x | | `test/generate-refuses-unparseable-name.test.ts` | 46.7 / 47.6 / 43.6 | 11.2 / 10.3 / 11.4 | 4.2x | | `test/generate-refuses-namespace-prefix.test.ts` | 48.2 / 46.0 / 43.7 | 11.8 / 10.6 / 12.6 | 3.9x | | `test/build-view-container-name.test.ts` | 48.2 / 46.0 / 45.2 | 19.4 / 18.9 / 21.6 | 2.4x | | `test/generate-refuses-retired-generator.test.ts` | 38.0 / 35.5 / 38.1 | 8.8 / 8.0 / 8.2 | 4.6x | | `test/generate-refuses-name-outside-charset.test.ts` | 35.6 / 37.8 / 37.7 | 9.8 / 8.3 / 9.6 | 3.9x | | `test/build-text-face-advisory-count.test.ts` | 28.5 / 27.8 / 26.6 | 12.5 / 11.1 / 12.1 | 2.3x | | sum of file lines | 519.5 / 507.1 / 486.5 | 172.0 / 164.9 / 180.1 | 2.9x | Run wall: base 285.3 / 279.6 / 258.2 s, head 110.8 / 106.8 / 115.7 s (generation included). Tests 91/91 every run, and no manifest was left after any run. The `build-*` files keep more of their wall because their build work is real, not spawn floor. **Budget headers:** slice 11's `// test-budget:` headers (PR #26174) are not lowered here. A later refresh re-sizes them. ## Proof - **Full `@objectstack/cli` suite with the setup** (both tiers, queue population, `vitest run --maxWorkers=2`, at `e4b56419`, before the A1 change): Test Files 1 failed, 504 passed, 1 skipped (506), Tests 1 failed, 6324 passed, 9 skipped. The one red was the #12271 CONTROL described above. A watcher saw one manifest owner for the whole 37-minute run (the vitest pid), so there was no second generation, and the file was absent after teardown. - **At `d12d287fe`** (the branch before the merge): the #12271 file 5/5 under both configs (above). `vitest run --project unit --maxWorkers=2`: Test Files 333 passed (333), Tests 4854 passed (4854), lock VERDICT command-exit 0. `pnpm --filter @objectstack/cli typecheck`: exit 0, `check:test-typecheck: OK` (the new module is in the test program through `test/**`). - `pnpm exec eslint --no-inline-config packages/cli`: exit 0. - Gates at the final head `8e5065109` (current `main` merged in, with no overlap with this diff): `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 56 commands over the same 4 paths, each run with its exit code captured by redirect-then-`$?`, and all 56 exited 0. `--ran` with every exit recorded: "56 derived famil(ies) accounted for — 56 run, 0 NOT-MEASURED (a DERIVED zero)". The run at `d12d287fe` before the merge was the same, after two prerequisites were met: `check-plugin-teardown-shape --self-test` needed `621a4876` fetched into the shallow clone, and `check:dual-build-cjs-loads` needed 10 unbuilt packages built. `check:cli-test-child-env` (the new module spawns its generator with `childEnv()`) and `check:cross-package-test-inputs` are among the 56. Repo-wide `pnpm lint` and the full CI farm belong to CI. - After merging `main`: rebuilt the cli closure, then ran the 18 cli test files changed since the branch point (17 by `main`, plus the #12271 pin) and `test/vitest-tiers-partition.test.ts` with this config. Test Files 19 passed (19), Tests 188 passed (188), lock VERDICT command-exit 0. That covers slice 11's ten files now carrying their budget headers, slice 12's migrate files and #26047's `hook-timeout-override-refusal`, which spawns vitest children. ## Acceptance notes - With both tiers in one run, the `unit` project's in-process `Config.load({ root })` calls (for example `exit-signal.pin`, `db/clean.operator-text-21418`) also find the manifest, because every project's `globalSetup` runs before any test. They load commands through the same no-manifest path (no `relativePath`), and the full run above was green there. A unit-only run generates nothing; it only removes a file whose writer is dead. - A flag default computed at module load is frozen into the manifest at generation, and only help text reads it. One exists: `serve --port`'s default reads `OS_PORT` / `PORT`. `os serve --help` therefore shows the generation-time value, which is the run's own environment. Parsing still uses the loaded class. No test runs `--help` with `PORT` set. - The setup's failure mode is loud by design. If a command module cannot load, the integration run fails at setup with the generator's stderr (the `ModuleLoadError` naming the module), instead of every spawn test failing on "command not found". --- _Generated by [Claude Code](https://claude.ai/code/session_012YTKj54wfJ886NzwKz1oba)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent be531f6 commit 9c922b5

4 files changed

Lines changed: 207 additions & 9 deletions

File tree

‎.gitignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,7 @@ examples/app-crm/storage/
148148
# committed (#22449 B′, #22485); `files[]` ships whatever the lane wrote.
149149
packages/spec/spec-changes.json
150150
packages/spec/protocol-upgrade-guide.md
151+
152+
# #23181 slice 14: written by packages/cli/test/helpers/oclif-manifest.global-setup.ts for a test run and removed at its teardown; never committed, never published
153+
packages/cli/oclif.manifest.json
154+
packages/cli/oclif.manifest.json.*.tmp
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* The vitest `globalSetup` that writes `packages/cli/oclif.manifest.json` once
5+
* per run, from source, and removes it at teardown. Test infrastructure only:
6+
* the file is gitignored and `files[]` (`dist`, `README.md`, `CHANGELOG.md`)
7+
* never names it, so nothing published changes.
8+
*
9+
* ## Why (#23181, seat ruling A on report 6418998910)
10+
*
11+
* This package ships no oclif manifest, so every CLI process builds its command
12+
* table by importing all 72 command modules. Through `tsx bin/run-dev.js` that
13+
* is a floor of 8.2-8.5 s per spawn, `--version` included, and the files that
14+
* spawn the CLI held 89.4% of this package's file wall. With a manifest beside
15+
* `package.json` oclif reads the table from it and imports only the command it
16+
* runs: the same spawn takes 2.0-2.3 s.
17+
*
18+
* ## What is written, and why it is transparent
19+
*
20+
* The manifest is the object oclif's own root plugin builds in memory when no
21+
* manifest exists — `new Plugin({ isRoot: true, root })`, loaded in a `tsx`
22+
* child under `NODE_ENV=development`, which is what `bin/run-dev.js` runs — with
23+
* two keys removed from every command: `isESM` and `relativePath`.
24+
*
25+
* ⛔ Those two keys are not optional to remove. When a cached command carries
26+
* both, `Plugin.findCommand` imports `root + relativePath` VERBATIM
27+
* (`cachedCommandCanBeUsed` in @oclif/core 5.1.2), and a manifest generated from
28+
* source records `src/commands/<id>.ts`. The built entry (`bin/run.js`, which
29+
* the published-entry suites spawn with `NODE_ENV` unset) would then run the
30+
* SOURCE, and those suites would test something other than the build. Without
31+
* the two keys oclif falls back to the path it takes with no manifest at all —
32+
* the commands directory `tsPath` resolves, `src/` under `run-dev.js` and
33+
* `dist/` under `run.js` — so the manifest supplies the command TABLE and
34+
* nothing about where a command's code comes from.
35+
*
36+
* What still differs from a run without the file: `Config.load()` no longer
37+
* imports every command module, so a module that fails to load is reported when
38+
* it is run rather than as a load-time warning on every spawn. The suites that
39+
* pin that load-time behaviour are `*.e2e.test.ts` files, and the e2e tier runs
40+
* only in the nightly population, which `vitest.config.ts` leaves without this
41+
* setup. A command module that fails to load here fails the GENERATION instead
42+
* (`errorOnManifestCreate`), naming the module, before any test runs.
43+
*
44+
* ## Ownership, nesting and cleanup
45+
*
46+
* - Written to a temporary sibling and renamed over the target, so a spawn
47+
* reads either a whole manifest or none (oclif then builds its table as
48+
* before). Every run regenerates, so a manifest left by a killed run is never
49+
* what a later integration run uses.
50+
* - `writtenBy.pid` records the vitest process that owns the file. Teardown, and
51+
* an `exit` listener for a run that ends without teardown, remove it only
52+
* while that pid is still this process's: two runs on one checkout each
53+
* remove their own file, and the run that is left continues without a
54+
* manifest, at the old floor and with the same output.
55+
* - A vitest started INSIDE a run (`test/vitest-tiers-partition.test.ts` spawns
56+
* `vitest list`) inherits {@link OWNER_ENV} through `childEnv()` and does
57+
* nothing, so it neither pays a second generation nor removes the file under
58+
* the run that wrote it.
59+
* - A `unit`-only run generates nothing (no unit file spawns the CLI) and only
60+
* removes a manifest whose writer is no longer alive, so an in-process
61+
* `Config.load()` there never trusts a file a killed run left behind.
62+
*/
63+
64+
import { execFile } from 'node:child_process';
65+
import { readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
66+
import { resolve } from 'node:path';
67+
import { fileURLToPath } from 'node:url';
68+
import { promisify } from 'node:util';
69+
import type { TestProject } from 'vitest/node';
70+
import { childEnv, TSX } from './serve-process.js';
71+
72+
const HERE = resolve(fileURLToPath(import.meta.url), '..');
73+
74+
/** `packages/cli` — the root oclif reads `oclif.manifest.json` from. */
75+
const CLI_ROOT = resolve(HERE, '../..');
76+
77+
export const MANIFEST = resolve(CLI_ROOT, 'oclif.manifest.json');
78+
79+
/** Set in the owning run's process, and so in every worker and child below it. */
80+
export const OWNER_ENV = 'OS_TEST_OCLIF_MANIFEST_OWNER';
81+
82+
/** The argv flag that makes this module the generator, inside the `tsx` child. */
83+
const WRITE_FLAG = '--write-oclif-manifest';
84+
85+
const THIS_FILE = fileURLToPath(import.meta.url);
86+
87+
/** The pid recorded in the manifest at `path`, or `undefined` for none or not ours. */
88+
function writerOf(path: string): number | undefined {
89+
try {
90+
const pid = JSON.parse(readFileSync(path, 'utf8'))?.writtenBy?.pid;
91+
return Number.isInteger(pid) ? pid : undefined;
92+
} catch {
93+
return undefined;
94+
}
95+
}
96+
97+
function isAlive(pid: number): boolean {
98+
try {
99+
process.kill(pid, 0);
100+
return true;
101+
} catch (error) {
102+
return (error as NodeJS.ErrnoException).code === 'EPERM';
103+
}
104+
}
105+
106+
function removeIfOurs(): void {
107+
if (writerOf(MANIFEST) === process.pid) rmSync(MANIFEST, { force: true });
108+
}
109+
110+
/** Runs in the `tsx` child: build the table the way an unmanifested run does. */
111+
async function writeManifest(out: string, ownerPid: number): Promise<void> {
112+
const { Plugin } = await import('@oclif/core');
113+
const plugin = new Plugin({ isRoot: true, root: CLI_ROOT, ignoreManifest: true, errorOnManifestCreate: true });
114+
await plugin.load();
115+
const commands = Object.fromEntries(
116+
Object.entries(plugin.manifest.commands).map(([id, { isESM: _isESM, relativePath: _relativePath, ...cached }]) => [
117+
id,
118+
cached,
119+
]),
120+
);
121+
writeFileSync(out, JSON.stringify({ ...plugin.manifest, commands, writtenBy: { pid: ownerPid, by: 'packages/cli/test/helpers/oclif-manifest.global-setup.ts' } }));
122+
}
123+
124+
export default async function setup(project: TestProject): Promise<(() => void) | void> {
125+
if (process.env[OWNER_ENV]) return;
126+
127+
if (project.name !== 'integration') {
128+
const writer = writerOf(MANIFEST);
129+
if (writer !== undefined && !isAlive(writer)) rmSync(MANIFEST, { force: true });
130+
return;
131+
}
132+
133+
const tmp = `${MANIFEST}.${process.pid}.tmp`;
134+
try {
135+
await promisify(execFile)(TSX, [THIS_FILE, WRITE_FLAG, tmp, String(process.pid)], {
136+
cwd: CLI_ROOT,
137+
env: childEnv({ NODE_ENV: 'development' }),
138+
maxBuffer: 64 * 1024 * 1024,
139+
});
140+
} catch (error) {
141+
rmSync(tmp, { force: true });
142+
const { stderr } = error as { stderr?: string };
143+
throw new Error(`could not write ${MANIFEST} from source:\n${stderr || String(error)}`);
144+
}
145+
renameSync(tmp, MANIFEST);
146+
process.env[OWNER_ENV] = String(process.pid);
147+
process.once('exit', removeIfOurs);
148+
return () => {
149+
process.off('exit', removeIfOurs);
150+
removeIfOurs();
151+
};
152+
}
153+
154+
if (process.argv[1] === THIS_FILE && process.argv[2] === WRITE_FLAG) {
155+
await writeManifest(process.argv[3], Number(process.argv[4]));
156+
}

‎packages/cli/test/published-entry-node-env-source-reroute.test.ts‎

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ const CHILD_BUDGET_MS = 120_000;
139139
const CARD_SIGNATURE = "Cannot find module './registry'";
140140

141141
/**
142-
* The reroute itself, visible in oclif's own warning block: it names the module
143-
* it failed to load, and under the reroute that path is under `src/commands`.
142+
* The reroute itself, visible in oclif's own report: it names the module it
143+
* failed to load, and under the reroute that path is under `src/commands`.
144144
*/
145145
const SOURCE_COMMANDS = join('packages', 'cli', 'src', 'commands');
146146

@@ -170,10 +170,26 @@ afterAll(() => {
170170
/**
171171
* Run the published entry from the armed fixture directory.
172172
*
173-
* `--version` is the probe on purpose: it is the cheapest invocation that still
174-
* makes oclif build its command table, which is where the reroute happens.
175-
* Measured — every command tried (`--version`, `doctor --help`, `compile
176-
* --help`) reproduces identically, so the cheapest one is the honest one.
173+
* `explain object` is the probe because it RUNS a command: oclif has to resolve
174+
* that command's module and import it, which is where the reroute happens, and
175+
* `explain.ts` imports the trapped `chalk` at its top. It prints a static
176+
* reference page and exits 0 with nothing to read from the fixture directory.
177+
*
178+
* ⛔ Not `--version`, which this file used until #23181. `--version` loads no
179+
* command; it reached the reroute only because, with no `oclif.manifest.json`
180+
* beside `package.json`, `Config.load()` builds the command table by importing
181+
* every command module. The `integration` project now writes that manifest for
182+
* the run (`helpers/oclif-manifest.global-setup.ts`), and with it present
183+
* `--version` imports nothing: this leg's CONTROL stopped reproducing the card
184+
* and the three legs above it passed over a fixture that armed nothing. A real
185+
* command reaches the reroute with the manifest and without it, measured both
186+
* ways at the slice that made this change.
187+
*
188+
* `OCLIF_COLUMNS` is set wide so oclif renders the load failure on one line.
189+
* With a manifest the failure arrives as the error `handle()` prints, which is
190+
* word-wrapped at 80 columns on a pipe, and a wrap can fall inside the card's
191+
* signature or a path. A split signature fails the CONTROL; worse, it would
192+
* let every `not.toContain` above pass over text it can no longer match.
177193
*
178194
* ⚠️ `TSX_TSCONFIG_PATH` is cleared explicitly. It is what `bin/run-dev.js`
179195
* sets to pin tsx away from the CWD's tsconfig, and a developer running the
@@ -202,12 +218,12 @@ function runPublishedEntry(
202218
const startedAt = Date.now();
203219
const result = spawnSync(
204220
process.execPath,
205-
[...(options.neutralise ? [`--import=${NEUTRALISER}`] : []), PUBLISHED_ENTRY, '--version'],
221+
[...(options.neutralise ? [`--import=${NEUTRALISER}`] : []), PUBLISHED_ENTRY, 'explain', 'object'],
206222
{
207223
cwd: fixtureCwd,
208224
encoding: 'utf8',
209225
timeout: CHILD_BUDGET_MS,
210-
env: childEnv({ TSX_TSCONFIG_PATH: undefined, ...env }),
226+
env: childEnv({ TSX_TSCONFIG_PATH: undefined, OCLIF_COLUMNS: '100000', ...env }),
211227
},
212228
);
213229
const elapsedMs = Date.now() - startedAt;
@@ -257,7 +273,7 @@ describe('#12271 - the published entry does not reroute to src/ on an ambient NO
257273
* ⚠️ It arms only if oclif can REGISTER tsx (`registerTsx` in `ts-path`).
258274
* When that fails, oclif logs `Could not find tsx` under
259275
* `DEBUG=oclif:config:ts-path` and stays on `dist/`. This leg then prints
260-
* the version line and nothing else, and the legs above pass with the
276+
* the reference page and nothing else, and the legs above pass with the
261277
* declaration deleted. tsx 4.23.15's CommonJS ESM API fails that way on any
262278
* Node without tsx's `registerHooks` path (below 22.22.3 on the 22 line).
263279
* That is why the workspace patches it: see `tsx@4.23.15` under

‎packages/cli/vitest.config.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,7 @@ import {
649649
runProjectCliOverridePreflight,
650650
} from '../qa/vitest-filter-preflight/src/index.js';
651651
import { integrationTestFiles, unitTestFiles } from './vitest-tiers.js';
652+
import { readTierMode } from '../../scripts/nightly-tiers.mjs';
652653

653654
// The two tiers, DERIVED from what the files DO — never written down — over
654655
// the population `OS_TEST_TIERS` selects. `vitest-tiers.ts` holds the
@@ -739,6 +740,25 @@ runProjectCliOverridePreflight({
739740
// lives at its use site in the `test` block below, not in a helper binding
740741
// that could outlive the spread.
741742

743+
// #23181 slice 14 — THE OCLIF MANIFEST SETUP. One `globalSetup` module writes
744+
// `oclif.manifest.json` from source when the `integration` project runs (the
745+
// tier every CLI-spawning file is derived into) and removes it at teardown, so
746+
// a `tsx bin/run-dev.js` spawn reads its command table instead of importing all
747+
// 72 command modules: 8.2-8.5 s -> 2.0-2.3 s per spawn, same bytes out, same
748+
// exit code. The `unit` project carries the same module and generates nothing;
749+
// it only clears a manifest a killed run left behind. The module's header is
750+
// the authority on what is written, why it is transparent, and who removes it.
751+
//
752+
// ⛔ The QUEUE population only. Under `OS_TEST_TIERS=nightly` both projects run
753+
// without it, because the e2e tier holds the suites that pin what a manifest
754+
// would change: oclif importing every command module inside `Config.load()`
755+
// (`run-dev-unbuilt-workspace.e2e`, the `*-stderr-nonblocking.e2e` pair). The
756+
// product ships no manifest, so that is the shape those pins must keep seeing.
757+
// Set per project rather than at the root, where `extends: true` would hand it
758+
// to both projects AND to the root project, running it three times.
759+
const OCLIF_MANIFEST_SETUP =
760+
readTierMode() === 'queue' ? [path.resolve(__dirname, 'test/helpers/oclif-manifest.global-setup.ts')] : [];
761+
742762
export default defineConfig({
743763
resolve: {
744764
// Array form with an ANCHORED pattern, per the trap the gate documents:
@@ -873,6 +893,7 @@ export default defineConfig({
873893
test: {
874894
name: 'unit',
875895
disableConsoleIntercept: true,
896+
globalSetup: OCLIF_MANIFEST_SETUP,
876897
// #13517: quiet the registry's per-item registration chatter — the
877898
// engine's own `OS_REGISTRY_LOG` seam, not a change to its shipped
878899
// default. Enforced by scripts/check-registry-log-declared.mjs.
@@ -885,6 +906,7 @@ export default defineConfig({
885906
test: {
886907
name: 'integration',
887908
disableConsoleIntercept: true,
909+
globalSetup: OCLIF_MANIFEST_SETUP,
888910
// #13517: quiet the registry's per-item registration chatter — the
889911
// engine's own `OS_REGISTRY_LOG` seam, not a change to its shipped
890912
// default. Enforced by scripts/check-registry-log-declared.mjs.

0 commit comments

Comments
 (0)