From 6be6324176a2f7a61a7ad09d6ab0aca837987acf Mon Sep 17 00:00:00 2001 From: fi3ework Date: Mon, 14 Sep 2026 11:28:36 +0800 Subject: [PATCH] test(vscode): keep E2E fixture installs under the release-age gate The dependency-recovery E2E tests failed on main whenever a transitive third-party dependency of a fixture was younger than pnpm's minimumReleaseAge: setupFixtures.mjs resolved the lockfile with minimumReleaseAge=0, and the tests' frozen re-install in a tmpdir copy ran under pnpm 11's default policy, which re-verifies lockfile entries. - Every E2E fixture now has a committed, settings-only pnpm-workspace.yaml that makes it its own workspace root and exempts the Rstack family via minimumReleaseAgeExclude. The file takes effect at install time like the root one, and travels with the tmpdir copy, so both installs share one policy. Nothing is generated at setup. - setupFixtures.mjs drops --ignore-workspace (pnpm ignores the file's settings under it) and --config.minimumReleaseAge=0. - Add @rspack/* to the root exclude list; the fixture files repeat the root list verbatim. - Both recovery tests install through a shared e2e/pnpmInstall.ts helper that includes pnpm's stdout and stderr in the error when the install fails; pnpm 11 prints its errors to stdout. --- .../pnpm-workspace.yaml | 10 ++++++ .../e2e/fixtures/rslint/pnpm-workspace.yaml | 10 ++++++ .../e2e/fixtures/rstack/pnpm-workspace.yaml | 10 ++++++ .../e2e/fixtures/rstest/pnpm-workspace.yaml | 10 ++++++ .../dependency-recovery/pnpm-workspace.yaml | 10 ++++++ .../e2e/lint/fixtures/pnpm-workspace.yaml | 10 ++++++ .../dependency-recovery.test.ts | 17 ++-------- packages/vscode/e2e/pnpmInstall.ts | 34 +++++++++++++++++++ .../fixtures/workspace-1/pnpm-workspace.yaml | 10 ++++++ .../fixtures/workspace-2/pnpm-workspace.yaml | 10 ++++++ .../dependency-recovery.test.ts | 16 ++------- packages/vscode/e2e/setupFixtures.mjs | 24 +++++++------ pnpm-workspace.yaml | 2 ++ 13 files changed, 134 insertions(+), 39 deletions(-) create mode 100644 packages/vscode/e2e/fixtures/fmt-missing-config-dependency/pnpm-workspace.yaml create mode 100644 packages/vscode/e2e/fixtures/rslint/pnpm-workspace.yaml create mode 100644 packages/vscode/e2e/fixtures/rstack/pnpm-workspace.yaml create mode 100644 packages/vscode/e2e/fixtures/rstest/pnpm-workspace.yaml create mode 100644 packages/vscode/e2e/lint/fixtures/dependency-recovery/pnpm-workspace.yaml create mode 100644 packages/vscode/e2e/lint/fixtures/pnpm-workspace.yaml create mode 100644 packages/vscode/e2e/pnpmInstall.ts create mode 100644 packages/vscode/e2e/rstest/fixtures/workspace-1/pnpm-workspace.yaml create mode 100644 packages/vscode/e2e/rstest/fixtures/workspace-2/pnpm-workspace.yaml diff --git a/packages/vscode/e2e/fixtures/fmt-missing-config-dependency/pnpm-workspace.yaml b/packages/vscode/e2e/fixtures/fmt-missing-config-dependency/pnpm-workspace.yaml new file mode 100644 index 0000000..b15641f --- /dev/null +++ b/packages/vscode/e2e/fixtures/fmt-missing-config-dependency/pnpm-workspace.yaml @@ -0,0 +1,10 @@ +# E2E fixture install config; rationale in packages/vscode/e2e/setupFixtures.mjs. +minimumReleaseAgeExclude: + - rstack + - '@rslint/core' + - '@rstest/core' + - '@rsbuild/core' + - '@rslib/core' + - '@rstackjs/*' + - rsbuild-plugin-dts + - '@rspack/*' diff --git a/packages/vscode/e2e/fixtures/rslint/pnpm-workspace.yaml b/packages/vscode/e2e/fixtures/rslint/pnpm-workspace.yaml new file mode 100644 index 0000000..b15641f --- /dev/null +++ b/packages/vscode/e2e/fixtures/rslint/pnpm-workspace.yaml @@ -0,0 +1,10 @@ +# E2E fixture install config; rationale in packages/vscode/e2e/setupFixtures.mjs. +minimumReleaseAgeExclude: + - rstack + - '@rslint/core' + - '@rstest/core' + - '@rsbuild/core' + - '@rslib/core' + - '@rstackjs/*' + - rsbuild-plugin-dts + - '@rspack/*' diff --git a/packages/vscode/e2e/fixtures/rstack/pnpm-workspace.yaml b/packages/vscode/e2e/fixtures/rstack/pnpm-workspace.yaml new file mode 100644 index 0000000..b15641f --- /dev/null +++ b/packages/vscode/e2e/fixtures/rstack/pnpm-workspace.yaml @@ -0,0 +1,10 @@ +# E2E fixture install config; rationale in packages/vscode/e2e/setupFixtures.mjs. +minimumReleaseAgeExclude: + - rstack + - '@rslint/core' + - '@rstest/core' + - '@rsbuild/core' + - '@rslib/core' + - '@rstackjs/*' + - rsbuild-plugin-dts + - '@rspack/*' diff --git a/packages/vscode/e2e/fixtures/rstest/pnpm-workspace.yaml b/packages/vscode/e2e/fixtures/rstest/pnpm-workspace.yaml new file mode 100644 index 0000000..b15641f --- /dev/null +++ b/packages/vscode/e2e/fixtures/rstest/pnpm-workspace.yaml @@ -0,0 +1,10 @@ +# E2E fixture install config; rationale in packages/vscode/e2e/setupFixtures.mjs. +minimumReleaseAgeExclude: + - rstack + - '@rslint/core' + - '@rstest/core' + - '@rsbuild/core' + - '@rslib/core' + - '@rstackjs/*' + - rsbuild-plugin-dts + - '@rspack/*' diff --git a/packages/vscode/e2e/lint/fixtures/dependency-recovery/pnpm-workspace.yaml b/packages/vscode/e2e/lint/fixtures/dependency-recovery/pnpm-workspace.yaml new file mode 100644 index 0000000..b15641f --- /dev/null +++ b/packages/vscode/e2e/lint/fixtures/dependency-recovery/pnpm-workspace.yaml @@ -0,0 +1,10 @@ +# E2E fixture install config; rationale in packages/vscode/e2e/setupFixtures.mjs. +minimumReleaseAgeExclude: + - rstack + - '@rslint/core' + - '@rstest/core' + - '@rsbuild/core' + - '@rslib/core' + - '@rstackjs/*' + - rsbuild-plugin-dts + - '@rspack/*' diff --git a/packages/vscode/e2e/lint/fixtures/pnpm-workspace.yaml b/packages/vscode/e2e/lint/fixtures/pnpm-workspace.yaml new file mode 100644 index 0000000..b15641f --- /dev/null +++ b/packages/vscode/e2e/lint/fixtures/pnpm-workspace.yaml @@ -0,0 +1,10 @@ +# E2E fixture install config; rationale in packages/vscode/e2e/setupFixtures.mjs. +minimumReleaseAgeExclude: + - rstack + - '@rslint/core' + - '@rstest/core' + - '@rsbuild/core' + - '@rslib/core' + - '@rstackjs/*' + - rsbuild-plugin-dts + - '@rspack/*' diff --git a/packages/vscode/e2e/lint/suite-dependency-recovery/dependency-recovery.test.ts b/packages/vscode/e2e/lint/suite-dependency-recovery/dependency-recovery.test.ts index d410c0b..a1467c5 100644 --- a/packages/vscode/e2e/lint/suite-dependency-recovery/dependency-recovery.test.ts +++ b/packages/vscode/e2e/lint/suite-dependency-recovery/dependency-recovery.test.ts @@ -1,15 +1,12 @@ import * as assert from 'node:assert'; -import { execFile as execFileCallback } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; -import { promisify } from 'node:util'; import * as vscode from 'vscode'; +import { pnpmInstallFrozen } from '../../pnpmInstall'; import type { StackState } from '../../../src/types'; import { waitForRslintDiagnostics } from '../utils/diagnostics'; import { extensionExports } from '../utils/extension'; -const execFile = promisify(execFileCallback); - function lintExports(): { getFolderStates(): ReadonlyMap; } { @@ -61,17 +58,7 @@ suite('Rslint dependency polling recovery', function () { const beforeContents = fs.readFileSync(lockfile); const beforeMtime = fs.statSync(lockfile).mtimeMs; - await execFile( - 'pnpm', - ['install', '--frozen-lockfile', '--ignore-scripts'], - { - cwd: root, - timeout: 90_000, - // Match setupFixtures.mjs/run.mjs: Windows needs a shell for pnpm's - // .cmd shim. All arguments are fixed safe tokens; cwd is not interpolated. - shell: process.platform === 'win32', - }, - ); + await pnpmInstallFrozen(root); assert.deepStrictEqual( fs.readFileSync(lockfile), diff --git a/packages/vscode/e2e/pnpmInstall.ts b/packages/vscode/e2e/pnpmInstall.ts new file mode 100644 index 0000000..23757a6 --- /dev/null +++ b/packages/vscode/e2e/pnpmInstall.ts @@ -0,0 +1,34 @@ +import { execFile as execFileCallback } from 'node:child_process'; +import { promisify } from 'node:util'; + +const execFile = promisify(execFileCallback); + +/** + * Runs `pnpm install --frozen-lockfile --ignore-scripts` in `cwd`. pnpm 11 + * prints install errors (e.g. ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION) to + * stdout, while execFile's error message carries only stderr, so a failure + * rethrows with both streams appended. + */ +export async function pnpmInstallFrozen(cwd: string): Promise { + try { + await execFile( + 'pnpm', + ['install', '--frozen-lockfile', '--ignore-scripts'], + { + cwd, + timeout: 90_000, + // Windows needs a shell for pnpm's .cmd shim; the arguments are fixed + // safe tokens and cwd is not interpolated. + shell: process.platform === 'win32', + }, + ); + } catch (error) { + const { stdout = '', stderr = '' } = error as { + stdout?: string; + stderr?: string; + }; + throw new Error(`${(error as Error).message}\n${stdout}${stderr}`, { + cause: error, + }); + } +} diff --git a/packages/vscode/e2e/rstest/fixtures/workspace-1/pnpm-workspace.yaml b/packages/vscode/e2e/rstest/fixtures/workspace-1/pnpm-workspace.yaml new file mode 100644 index 0000000..b15641f --- /dev/null +++ b/packages/vscode/e2e/rstest/fixtures/workspace-1/pnpm-workspace.yaml @@ -0,0 +1,10 @@ +# E2E fixture install config; rationale in packages/vscode/e2e/setupFixtures.mjs. +minimumReleaseAgeExclude: + - rstack + - '@rslint/core' + - '@rstest/core' + - '@rsbuild/core' + - '@rslib/core' + - '@rstackjs/*' + - rsbuild-plugin-dts + - '@rspack/*' diff --git a/packages/vscode/e2e/rstest/fixtures/workspace-2/pnpm-workspace.yaml b/packages/vscode/e2e/rstest/fixtures/workspace-2/pnpm-workspace.yaml new file mode 100644 index 0000000..b15641f --- /dev/null +++ b/packages/vscode/e2e/rstest/fixtures/workspace-2/pnpm-workspace.yaml @@ -0,0 +1,10 @@ +# E2E fixture install config; rationale in packages/vscode/e2e/setupFixtures.mjs. +minimumReleaseAgeExclude: + - rstack + - '@rslint/core' + - '@rstest/core' + - '@rsbuild/core' + - '@rslib/core' + - '@rstackjs/*' + - rsbuild-plugin-dts + - '@rspack/*' diff --git a/packages/vscode/e2e/rstest/suite-dependency-recovery/dependency-recovery.test.ts b/packages/vscode/e2e/rstest/suite-dependency-recovery/dependency-recovery.test.ts index d3e5704..74e80d9 100644 --- a/packages/vscode/e2e/rstest/suite-dependency-recovery/dependency-recovery.test.ts +++ b/packages/vscode/e2e/rstest/suite-dependency-recovery/dependency-recovery.test.ts @@ -1,14 +1,11 @@ import assert from 'node:assert/strict'; -import { execFile as execFileCallback } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; -import { promisify } from 'node:util'; import vscode from 'vscode'; +import { pnpmInstallFrozen } from '../../pnpmInstall'; import type { RstackExtensionExports } from '../../../src/types'; import { getProjectItems, getRstestExports, waitFor } from '../suite/helpers'; -const execFile = promisify(execFileCallback); - suite('Rstest dependency polling recovery', function () { this.timeout(180_000); @@ -30,16 +27,7 @@ suite('Rstest dependency polling recovery', function () { const lockfile = path.join(root, 'pnpm-lock.yaml'); const contents = fs.readFileSync(lockfile); const mtime = fs.statSync(lockfile).mtimeMs; - await execFile( - 'pnpm', - ['install', '--frozen-lockfile', '--ignore-scripts'], - { - cwd: root, - timeout: 90_000, - // Windows needs a shell for pnpm.cmd; arguments are fixed safe tokens. - shell: process.platform === 'win32', - }, - ); + await pnpmInstallFrozen(root); assert.deepEqual( fs.readFileSync(lockfile), contents, diff --git a/packages/vscode/e2e/setupFixtures.mjs b/packages/vscode/e2e/setupFixtures.mjs index fb4cd6d..5bca7dd 100644 --- a/packages/vscode/e2e/setupFixtures.mjs +++ b/packages/vscode/e2e/setupFixtures.mjs @@ -5,9 +5,20 @@ // from the project, so a fixture that linked this repo's own node_modules would // test nothing. `rstack@0.7.2` itself pins `@rslint/core@0.9.0` exactly, so the // Rstack fixture pins its lint core transitively. Each fixture is its own -// independent install; `--ignore-workspace` keeps test setup out of the parent -// workspace. Exact toolchain pins make installs reproducible without committed -// lockfiles, and Renovate bumps those pins. +// independent install. Exact toolchain pins make installs reproducible +// without committed lockfiles, and Renovate bumps those pins. +// +// Each fixture carries a committed, settings-only `pnpm-workspace.yaml`. It +// stops pnpm from walking up into this repo's workspace, and it exempts the +// Rstack family from pnpm's `minimumReleaseAge` gate (fixtures pin releases +// that are often hours old; third-party packages stay gated). The exemption +// has to live in a file rather than `--config.minimumReleaseAge=0` because the +// dependency-recovery tests copy a fixture to the OS tmpdir and re-install +// with `--frozen-lockfile`, and pnpm 11 re-checks release age even when +// frozen, so the policy must travel with the copy. The same file rules out +// `--ignore-workspace`, under which pnpm ignores its settings. Setup-only +// options stay flags below. Keep the list in step with the root +// `pnpm-workspace.yaml`. // // Idempotent: pnpm is a no-op when the fixture is already up to date, so // `test:e2e` can always run it. @@ -65,9 +76,6 @@ const install = (name) => { pnpmCommand, [ 'install', - // A fixture is a standalone project, never a workspace member of this - // repo: the whole point is a plain, published-versions install. - '--ignore-workspace', // Fixtures pin exact toolchain versions rather than committing lockfiles; // Renovate updates the pins. '--no-frozen-lockfile', @@ -76,10 +84,6 @@ const install = (name) => { // `node_modules`, which it refuses to do without a TTY. The directory is // disposable. '--config.confirmModulesPurge=false', - // Fixtures deliberately install pinned published versions of the Rstack - // toolchain, which are often hours old — disable pnpm's - // minimum-release-age supply-chain gate for these sandboxes. - '--config.minimumReleaseAge=0', // pnpm's build-script gate exits non-zero on unapproved postinstalls // (e.g. core-js in the rstest fixture). These sandboxes install real // published packages exactly like a user project would, so run their diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 34cde38..085be5a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -11,6 +11,7 @@ allowBuilds: # The whole point of this repo is tracking the freshest Rstack toolchain # releases, so exempt them from the minimum-release-age supply-chain policy. +# Each E2E fixture's pnpm-workspace.yaml repeats this list; keep them in step. minimumReleaseAgeExclude: - rstack - '@rslint/core' @@ -19,3 +20,4 @@ minimumReleaseAgeExclude: - '@rslib/core' - '@rstackjs/*' - rsbuild-plugin-dts + - '@rspack/*'