Skip to content

Commit 83e31cd

Browse files
test(cli): tmpdir fixtures that run pnpm reach no registry (dev-no-watch pin, doctor readers, init leg) (#24455)
Fixes #24419 Clause-②: no Basis for the Clause-② line: every changed file is under `packages/cli/test/**`, the CLI's test harness. `@objectstack/cli` publishes `dist`, `README.md` and `CHANGELOG.md` (its `files[]`). Measured on head `8994da181`, after `pnpm turbo run build --filter='@objectstack/cli...'`: the identifiers this PR adds (`derivePackageManagerPin`, `packageManagerPin`, `PACKAGE_ROOT`, `STUB_PNPM_VERSION`, `writeStubPnpm`, `stubBinDir`, `fixtureRoot`) occur in 0 of those files. The positive control `devWatchActive`, which comes from `src/`, occurs in 2. Nothing published moves, so this PR carries the `skip-changeset` label and no changeset. ## The pin's shape: derived from pnpm (option B, the seat's decision) The first head (`bd0651500`) read the `packageManager` pin from the root `package.json`. That read leaves `packages/cli`, so `check:cross-package-test-inputs` went red, and declaring it needed three files outside the claim's file surface. The work stopped there, and the seat chose option B (PR thread, patch round 1). The pin is now derived. `dev-no-watch.pin.test.ts` runs `pnpm --version` with `cwd` = `packages/cli` and writes `pnpm@VERSION` into the fixtures. That answer is the root pin, reached through Corepack's own walk. No root file is read and no new turbo input is declared. `check:cross-package-test-inputs` exits 0 on `8994da181`, and `turbo.json`, `scripts/cross-package-test-inputs.mjs` and `scripts/check-cross-package-test-inputs.mjs` are untouched (`git diff db1f1fd HEAD` is empty for all three). Why B: - The card's acceptance is that the test reaches no network. B meets it, and the pin is derived rather than copied by hand. - A would have made the root `package.json` an input of `@objectstack/cli#test`. Every PR that edits it would then add cli's PR-tier suite to Test Core. Measured: a root-`package.json`-only edit gives `turbo ls --affected` 0 packages today (the control, a `packages/types/package.json` edit, gives 69 including cli). On `origin/main`, 42 of the last 300 commits (since 2026-10-16) touched the root `package.json` and no `packages/`, `examples/` or `apps/` path. What B leaves, stated in the file's own comment: the pin is an undeclared input, so a pin-only bump does not re-run this file. This suite's other `pnpm -v` and `pnpm pack` callers have the same blind spot, so B adds no new class of it. The fixture spec carries no integrity hash, and the derivation costs one extra spawn. Option A's patch was measured and not taken. On `bd0651500`, three edits turned the gate green with `check-ci-filter-parity` also green, and were then restored (blob equal to HEAD, `git diff HEAD` empty for all three): - `turbo.json`: `"$TURBO_ROOT$/package.json"` in the `@objectstack/cli#test` inputs; - `scripts/cross-package-test-inputs.mjs`: `'package.json'` in the cli globs; - `scripts/check-cross-package-test-inputs.mjs`: `'package.json'` in the pinned `TURBO_ROOT_FILE_INPUTS` roster. ## The premise, measured on `db1f1fd20` before any change CI's runner shape was rebuilt locally as `.github/actions/setup-pnpm` leaves it: Corepack shims first on `PATH`, and a private `COREPACK_HOME` holding exactly the pinned `pnpm@10.31.0` (materialised with `corepack install` in the repo root, with no `lastKnownGood.json`). The registry was made unreachable: `COREPACK_NPM_REGISTRY` and `npm_config_registry` point at a local server that answers HTTP 429 to every request and logs each one. A logging `pnpm` shim in front of Corepack's records every pnpm call's cwd and argv. - **Mechanism.** In a bare `tmpdir()` directory, `pnpm --version` sent `GET /pnpm/latest` and exited 1 with `Server answered with HTTP 429 when performing the request to …/pnpm/latest`. That is CI's signature on PR #24415. The same directory with a `package.json` carrying the root pin: `10.31.0`, 0 requests. - **`dev-no-watch.pin.test.ts`, unchanged:** the same two tests CI failed went red. "and the failure is pnpm refusing the PACKAGE…" failed with `AssertionError: os dev ended with code 1`. "the fixture starts its own dev script when asked — the control" failed with `expected 1 to be +0`. Both outputs carry the 429 line. The `--no-watch` spawn never runs pnpm, because the refusal comes first. "`os dev --watch=false` exits 1 instead of 0" stayed GREEN under the 429, because Corepack's failure exits 1 too. Only its sibling assertion on pnpm's own sentence tells the two apart. ## The shape, and why The fixtures carry the repository's own pin as `pnpm@VERSION`, where VERSION is what `pnpm --version` answers in `packages/cli` (see the section above). Nothing is copied by hand. It is the one version CI's Corepack store already holds, so it resolves with no network call. - **Where the pin sits.** One fixture root holds both fixtures, and its `package.json` carries the pin. `workspace/package.json` carries it too, because it is the nearest manifest. `project/` holds no manifest. Measured with pnpm 10.31.0: a `package.json` inside the project directory changes pnpm's answer from `No projects found in "…"` (the sentence this file pins) to `No projects matched the filters in "…"`. With the pin one level up, the answer is unchanged. - **Why not the env route.** `COREPACK_DEFAULT_TO_LATEST=0` does not remove the network reach in CI's shape. It swaps `latest` for Corepack's own bundled default, which the store does not hold. Measured: `GET /pnpm/10.13.1`, 429, exit 1. `COREPACK_ENABLE_NETWORK=0` would only turn the fetch into a refusal. - The two tests keep their subjects: pnpm's no-projects refusal, and the `--no-watch` refusal against a fixture whose `dev` script runs. ## The family census Method: the CLI paths that run pnpm were enumerated from every child-process call site in `packages/cli/src`. There are three: `os dev` monorepo mode (`pnpm … dev`), `os doctor` (`pnpm -v`), and `os init` (`PM install`). Tests that spawn pnpm directly, or drive one of those three paths, were enumerated by `git grep` over the command literals. Each candidate was then run in the CI shape above, with the registry refused. | File | Runs pnpm? | cwd | Pin in reach? | Registry, measured before | Verdict | |---|---|---|---|---|---| | `test/dev-no-watch.pin.test.ts` | `os dev` → `pnpm --filter false … dev`, `pnpm dev` | `tmpdir()` | no | 2 × `GET /pnpm/latest`, 2 tests red | **fixed**: pin derived from `pnpm --version` in `packages/cli` | | `test/package-owned-command-readers.test.ts` | `os doctor` → `pnpm -v` (2 children) | `tmpdir()` | no | 2 × `GET /pnpm/latest`, tests GREEN (doctor turns a failed probe into a row) | **fixed**: a stub `pnpm` on `PATH` answers `-v` and refuses anything else. A new `it` per shape asserts the stub was reached | | `test/create-refuses-invalid-project-name.e2e.test.ts` (nightly tier) | `os init NAME` (no `--no-install`) → `pnpm install` | `tmpdir()` scaffold (no pin, on purpose) | no | 1 × `GET /pnpm/latest`, 1 test red. With a working registry it installs the scaffold's dependencies from npm | **fixed**: `--no-install`. The leg's subject is that `init` accepts the name | | `test/init.test.ts` | no: renders templates in-process | n/a | n/a | 0 pnpm calls | no reach | | `test/init-created-files-summary.e2e.test.ts` | a fake `pnpm` leads `PATH` | `tmpdir()` | n/a | 0 real pnpm calls, 0 requests | no reach | | `test/published-subpath-console.pin.test.ts` | `pnpm pack` | `packages/cli` | yes | 0 requests | no reach (`tmpdir()` is only `--pack-destination`) | | `test/published-subpath-hook-body.pin.test.ts` | `pnpm pack` | `packages/cli` | yes | 0 requests | no reach | | `src/commands/doctor-*.test.ts` (10 files, in-process `Doctor.run`) | `pnpm -v` | the process's real cwd, `packages/cli` (they spy `process.cwd()`, which a child never reads) | yes | 87 calls, 0 requests | no reach | | `test/create-objectstack-stack-reach.test.ts` (found by the whole-tier run below; the grep missed it) | `pnpm --version` | `packages/cli/node_modules/.create-objectstack-reach-*` | yes | 0 requests (measured at `bd0651500`) | no reach | Other `os dev` spawns refuse before pnpm (`--no-watch`) or boot one environment (`--artifact`, a config in cwd). Other `os init` spawns refuse before the install, pass `--no-install`, or stub `npm`. `exit-signal.pin.test.ts` mocks `child_process`. **Whole PR tier, after the fix, in the CI shape with the registry refused** (head `bd0651500`): - `--project unit`: 311 files, 4610 tests passed, **0 registry requests**. The only pnpm calls were the two `pnpm pack` runs and 87 × `pnpm -v`, all in `packages/cli`. - `--project integration`: 160 files, 159 passed + 1 skipped (a pre-existing skip in a file this PR does not touch), 1375 tests passed + 9 skipped, **0 registry requests**. pnpm was called by dev-no-watch's two spawns (pinned) and by `create-objectstack-stack-reach.test.ts`'s `pnpm --version` in `packages/cli/node_modules/…`, where the root pin is in reach. That file is the table's last row. Patch round 1 (`8994da181`) changes only `dev-no-watch.pin.test.ts`. That file was re-proved on the new head (see Proof); the whole-tier readings above were taken on `bd0651500`. The nightly tier was not run whole. Its pnpm-reaching candidates are the two `.e2e` rows above, each measured. ## Proof: the converted tests with the registry refused - Head `8994da181`: `dev-no-watch.pin` + `package-owned-command-readers` 16/16 passed, **0 registry requests**. The derivation's `pnpm --version` ran in `packages/cli`. The pnpm calls in `/tmp/os-dev-no-watch-*/project` and `…/workspace` resolved to `10.31.0` from the store. - Head `bd0651500`, files unchanged since: `create-refuses-invalid-project-name.e2e` + `init-created-files-summary.e2e` (`OS_TEST_TIERS=nightly`) 10/10 passed, **0 registry requests**. - The Corepack store still holds exactly `v1/pnpm/10.31.0`, with no `lastKnownGood.json`. - Also green in this container's default shape (a standalone pnpm 10.28.0, not Corepack), with both registries refused, on `8994da181`: `dev-no-watch.pin` 8/8, 0 requests. ## Ablation (`scripts/ablation-replace.mjs`, wrap mode, fix committed first, head `8994da181`) - Anchor `packageManager: packageManagerPin` (2 hits, `--expect 2`), replaced with `packageManager: undefined`. On disk: anchor 2 → 0, replacement 0 → 2, blob `0881d577b041` → `1df76c152cb3`. - With the registry refused, it went RED, the expected direction. The same two tests failed as in the premise run: "and the failure is pnpm refusing the PACKAGE…" and "the fixture starts its own dev script when asked — the control". 6 passed, and there were 2 × `GET /pnpm/latest`. - Restore: blob after restore `0881d577b041` equals HEAD, `git diff HEAD` empty, `git status --porcelain` empty. - The same ablation on `bd0651500`'s read-the-manifest shape gave the same red: 2 failed, 2 requests, restore proven. ## Tiers The tiers are unchanged. `vitest-tiers.ts`'s `tierSignals` gives the same answer for each changed file before (`db1f1fd20`), on `bd0651500` and on `8994da181`. For `dev-no-watch.pin` and `create-refuses-…e2e`: integration (`childProcess, entryBasename, tsxBin`). For `package-owned-command-readers`: integration (`childProcess, helperCliOrTsx`). The e2e file keeps its nightly name. `test/vitest-tiers-partition.test.ts` passed (22/22 on `8994da181`). ## Gates (local, head `8994da181`) - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 52 derived commands, each run with its exit code captured before any pipe. All 52 exited 0, `pnpm check:cross-package-test-inputs` included. - `pnpm check:dual-build-cjs-loads` first answered 3, PREREQUISITE NOT MET: the fresh worktree had no `dist/` for packages outside cli's closure, so that run measured nothing. A later gate in the battery (`check:type-check-debt`) built them, and the re-run exited 0. - The head is behind `origin/main`, and the derivation flagged two gate files changed there. The same derivation on a tree at `origin/main` `725a5b131`, with this diff applied, gives the identical 52 commands. - The 11 declared-wide families were each run and exited 0. `pnpm check:cli-test-child-env` exited 0. - `--ran` reconciliation: 52 derived, 52 run, 0 NOT-MEASURED (a derived zero: all 52 recorded an exit code), 0 UNRUN. - `eslint --no-inline-config` over the 3 changed files: exit 0, 0 errors, 0 warnings, none ignored. - `pnpm --filter @objectstack/cli typecheck`: exit 0. `tsc -p tsconfig.test.json --listFiles` lists all 3 changed files. Its 28 errors are exactly the ledgered `test-typecheck-debt.json` set, none in these files. - CI on `8994da181`, read before this reply: 33 success, 7 skipped, 0 failures. All five required contexts are green (`Lint & Repo Gates`, `TypeScript Type Check`, `Test Core`, `Build Core`, `Governed Surface Queue Guard`). ## Acceptance notes - `os doctor`'s pnpm row misreports a Corepack fetch failure as `✗ pnpm Not found → Install pnpm: npm install -g pnpm@10.28.1`, and prints Corepack's stack trace above the report. Measured in a `tmpdir()` with no pin under Corepack, with the registry answering 429. Filed as #24464, not fixed here (`packages/cli/src/**` is held off). - In the 10 in-process doctor tests, the `process.cwd()` spy does not reach `doctor`'s `pnpm -v` child, which runs in the real cwd. That is harmless here (the pin is in reach) and is noted only. --- _Generated by [Claude Code](https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9847f87 commit 83e31cd

3 files changed

Lines changed: 137 additions & 11 deletions

File tree

‎packages/cli/test/create-refuses-invalid-project-name.e2e.test.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,16 @@ describe('os create: a name npm refuses is refused before anything is written',
182182

183183
// ⛔ The asymmetry is correct, not a second defect: `init`'s argument is
184184
// the package name, so npm's ceiling is already measured against it.
185-
const inited = await runCli(['init', COMPOSED_TOO_LONG], initCwd);
185+
//
186+
// `--no-install`: what is judged is that `init` ACCEPTS the name, and
187+
// it refuses an invalid one before the first write, long before any
188+
// install. Without the flag, `init` ran `<pm> install` in this
189+
// `tmpdir()` scaffold, which carries no `packageManager` pin on
190+
// purpose: under Corepack that resolved `pnpm@latest` from the public
191+
// registry and then fetched the scaffold's dependencies from it —
192+
// measured: a registry answering HTTP 429 turned this exit 0 into a
193+
// failure with nothing about the name changed.
194+
const inited = await runCli(['init', COMPOSED_TOO_LONG, '--no-install'], initCwd);
186195
expect(inited.code).toBe(0);
187196
} finally {
188197
rmSync(createCwd, { recursive: true, force: true });

‎packages/cli/test/dev-no-watch.pin.test.ts‎

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@
4646
*/
4747

4848
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
49-
import { execFile } from 'node:child_process';
50-
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
49+
import { execFile, execFileSync } from 'node:child_process';
50+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
5151
import { tmpdir } from 'node:os';
5252
import { join, resolve } from 'node:path';
5353
import { fileURLToPath } from 'node:url';
@@ -59,6 +59,54 @@ const HERE = resolve(fileURLToPath(import.meta.url), '..');
5959
const CLI = resolve(HERE, '../bin/run-dev.js');
6060
const TSX = resolve(HERE, '../../../node_modules/.bin/tsx');
6161

62+
const PACKAGE_ROOT = resolve(HERE, '..');
63+
64+
/**
65+
* The `packageManager` pin both fixtures carry, as `pnpm@VERSION`.
66+
*
67+
* Why the fixtures carry one: both live under `tmpdir()`, outside the
68+
* repository, and the spawned `os dev` runs `pnpm … dev` inside them. Under
69+
* Corepack (CI's `setup-pnpm`), a `pnpm` run with no `packageManager` in reach
70+
* resolves `pnpm@latest` from the public registry — measured: a registry
71+
* answering HTTP 429 failed this file's two pnpm-running cases with
72+
* `os dev ended with code 1` and `expected 1 to be +0`, the shape CI saw.
73+
* `COREPACK_DEFAULT_TO_LATEST=0` is NOT the same fix: it swaps `latest` for
74+
* Corepack's own bundled default, which CI's store does not hold, and the
75+
* registry is reached for that instead.
76+
*
77+
* Why the pin is DERIVED and not read: VERSION is what `pnpm --version`
78+
* answers with this package as its cwd, which is the repository's root pin
79+
* reached through Corepack's own walk up to the nearest manifest declaring
80+
* one. That is the version CI's Corepack store already holds, so the fixtures
81+
* resolve it with no network call, and nothing is copied by hand. Reading the
82+
* root `package.json` from here instead would make it a declared input of this
83+
* package's tests (`check:cross-package-test-inputs`), which would put cli's
84+
* whole PR-tier suite on every PR that edits that manifest. The spec carries no
85+
* integrity hash; Corepack resolves the stored version by its number.
86+
*
87+
* ⛔ The blind spot this leaves, stated so it is not read as covered: the pin
88+
* is an UNDECLARED input. A PR that bumps only the root pin does not re-run
89+
* this file in CI, exactly as it re-runs none of this suite's other `pnpm -v`
90+
* and `pnpm pack` callers, which resolve pnpm through the same walk. A pnpm
91+
* whose no-projects sentence changed would first go red on a later PR that
92+
* re-runs cli's tests.
93+
*/
94+
function derivePackageManagerPin(): string {
95+
const answer = execFileSync('pnpm', ['--version'], {
96+
cwd: PACKAGE_ROOT,
97+
encoding: 'utf8',
98+
stdio: ['ignore', 'pipe', 'pipe'],
99+
env: childEnv(),
100+
}).trim();
101+
if (!/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(answer)) {
102+
throw new Error(
103+
`\`pnpm --version\` in ${PACKAGE_ROOT} answered ${JSON.stringify(answer)}, not a version; ` +
104+
'without one these fixtures carry no pin and Corepack would fetch a pnpm from the registry',
105+
);
106+
}
107+
return `pnpm@${answer}`;
108+
}
109+
62110
/** oclif + tsx cold start, with every command module loaded; ~2-10 s when healthy. */
63111
const RUN_TIMEOUT_MS = 180_000;
64112

@@ -143,27 +191,46 @@ describe('the watch opt-out is reachable', () => {
143191
});
144192
});
145193

194+
let fixtureRoot: string;
195+
let packageManagerPin: string;
146196
let projectDir: string;
147197
let workspaceDir: string;
148198
let watchEqualsFalse: Run;
149199
let noWatchAtWorkspace: Run;
150200
let bareAtWorkspace: Run;
151201

152202
beforeAll(async () => {
203+
// Both fixtures sit under one root whose manifest carries the pin (see
204+
// `derivePackageManagerPin`). Above `projectDir`, not in it: a `package.json`
205+
// inside `projectDir` makes it a project, and pnpm then answers
206+
// `No projects matched the filters` instead of the `No projects found`
207+
// this file pins. Corepack walks up to the nearest manifest declaring a
208+
// `packageManager`; pnpm's no-projects answer reads `projectDir` alone.
209+
packageManagerPin = derivePackageManagerPin();
210+
fixtureRoot = mkdtempSync(join(tmpdir(), 'os-dev-no-watch-'));
211+
writeFileSync(
212+
join(fixtureRoot, 'package.json'),
213+
`${JSON.stringify({ private: true, packageManager: packageManagerPin })}\n`,
214+
);
215+
153216
// A project directory: a config file, and no pnpm workspace above it.
154-
projectDir = mkdtempSync(join(tmpdir(), 'os-dev-no-watch-project-'));
217+
projectDir = join(fixtureRoot, 'project');
218+
mkdirSync(projectDir);
155219
writeFileSync(join(projectDir, 'objectstack.config.ts'), 'export default {};\n');
156220

157221
// A workspace root whose own `dev` script announces that it ran — so the
158222
// `--no-watch` refusal below is measured against a fixture that DOES start
159-
// something when asked (`bareAtWorkspace` is that control).
160-
workspaceDir = mkdtempSync(join(tmpdir(), 'os-dev-no-watch-workspace-'));
223+
// something when asked (`bareAtWorkspace` is that control). Its own manifest
224+
// carries the pin too: the nearest one is what Corepack reads first.
225+
workspaceDir = join(fixtureRoot, 'workspace');
226+
mkdirSync(workspaceDir);
161227
writeFileSync(join(workspaceDir, 'pnpm-workspace.yaml'), 'packages: []\n');
162228
writeFileSync(
163229
join(workspaceDir, 'package.json'),
164230
`${JSON.stringify({
165231
name: 'os-dev-no-watch-fixture',
166232
private: true,
233+
packageManager: packageManagerPin,
167234
scripts: { dev: `node -e "console.log('${DEV_SCRIPT_MARK}')"` },
168235
})}\n`,
169236
);
@@ -176,8 +243,7 @@ beforeAll(async () => {
176243
}, RUN_TIMEOUT_MS);
177244

178245
afterAll(() => {
179-
rmSync(projectDir, { recursive: true, force: true });
180-
rmSync(workspaceDir, { recursive: true, force: true });
246+
if (fixtureRoot) rmSync(fixtureRoot, { recursive: true, force: true });
181247
});
182248

183249
describe('a PACKAGE that selects no workspace project fails', () => {

‎packages/cli/test/package-owned-command-readers.test.ts‎

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,23 +30,66 @@
3030

3131
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
3232
import { execFile } from 'node:child_process';
33-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
33+
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3434
import { tmpdir } from 'node:os';
35-
import { join } from 'node:path';
35+
import { delimiter, join } from 'node:path';
3636
import { FILE_REFERENCES_MIGRATION_ID } from '@objectstack/spec/system';
3737

3838
import { CLI, TSX, childEnv } from './helpers/serve-process.js';
3939
import { linkSpec } from './helpers/define-stack-fixture.js';
4040

4141
interface Run { code: number; stdout: string; stderr: string }
4242

43+
/**
44+
* The version a stub `pnpm` answers with. `os doctor` runs `pnpm -v` in its own
45+
* cwd, and every cwd here is a `tmpdir()` fixture with no `packageManager` in
46+
* reach: under Corepack (CI's `setup-pnpm`) a real `pnpm` there resolves
47+
* `pnpm@latest` from the public registry — measured: two registry requests per
48+
* run of this file, one per doctor child. Doctor degrades a failed probe to a
49+
* row, so a refused registry never reddened this file; it reached the network
50+
* all the same. Which pnpm answers is not what these pins judge, so a stub on
51+
* `PATH` answers instead, and `doctor` asserts below that the stub is what it
52+
* reached.
53+
*/
54+
const STUB_PNPM_VERSION = '0.0.0-package-owned-readers-stub';
55+
56+
/** A `pnpm` that answers `-v` and refuses anything else, loudly. */
57+
function writeStubPnpm(binDir: string): void {
58+
mkdirSync(binDir, { recursive: true });
59+
const bin = join(binDir, 'pnpm');
60+
writeFileSync(
61+
bin,
62+
[
63+
'#!/bin/sh',
64+
'case "$1" in',
65+
` -v|--version) echo ${STUB_PNPM_VERSION}; exit 0 ;;`,
66+
'esac',
67+
'echo "stub pnpm: unexpected invocation: $*" >&2',
68+
'exit 97',
69+
'',
70+
].join('\n'),
71+
);
72+
chmodSync(bin, 0o755);
73+
}
74+
75+
let stubBinDir = '';
76+
4377
function runCli(args: string[], cwd: string): Promise<Run> {
4478
return new Promise((resolvePromise) => {
4579
execFile(
4680
TSX,
4781
[CLI, ...args],
4882
// Artifact-less on purpose: the config is the only input these commands read.
49-
{ cwd, maxBuffer: 16 * 1024 * 1024, env: childEnv({ NO_COLOR: '1', OS_ARTIFACT_PATH: join(cwd, 'dist', 'objectstack.json') }) },
83+
// The stub `pnpm` leads `PATH` for every child; only `doctor` calls it.
84+
{
85+
cwd,
86+
maxBuffer: 16 * 1024 * 1024,
87+
env: childEnv({
88+
NO_COLOR: '1',
89+
OS_ARTIFACT_PATH: join(cwd, 'dist', 'objectstack.json'),
90+
PATH: `${stubBinDir}${delimiter}${process.env.PATH ?? ''}`,
91+
}),
92+
},
5093
(err, stdout, stderr) => {
5194
const code = err ? (typeof (err as { code?: unknown }).code === 'number' ? (err as { code: number }).code : 1) : 0;
5295
resolvePromise({ code, stdout: String(stdout), stderr: String(stderr) });
@@ -97,6 +140,8 @@ let r: Record<string, Run> = {};
97140

98141
beforeAll(async () => {
99142
root = mkdtempSync(join(tmpdir(), 'os-package-owned-command-readers-'));
143+
stubBinDir = join(root, 'bin');
144+
writeStubPnpm(stubBinDir);
100145
for (const [name, source] of Object.entries(CONFIGS)) {
101146
const dir = join(root, name);
102147
mkdirSync(dir, { recursive: true });
@@ -137,6 +182,12 @@ describe('#22288 — os doctor runs its metadata checks over every package', ()
137182
expect(unused.join('\n'), out).toMatch(/"pco_note"/);
138183
expect(unused.join('\n'), out).toMatch(/"pco_ticket"/);
139184
});
185+
186+
it(`${shape === 'two' ? 'two packages' : 'control, one package'}: doctor's pnpm probe reached the stub, not the registry`, () => {
187+
const run = r[`doctor:${shape}`];
188+
const out = run.stdout + run.stderr;
189+
expect(out, out).toContain(STUB_PNPM_VERSION);
190+
});
140191
}
141192
});
142193

0 commit comments

Comments
 (0)