Skip to content

Commit 12e2a73

Browse files
committed
test(vscode): inject an unreleased toolchain into the E2E fixtures
RSTACK_E2E_TOOLCHAIN_OVERRIDES="name=<package dir>" packs each package into <fixture>/.toolchain/ and adds a file: override to every fixture's pnpm-workspace.yaml, so rstack-ecosystem-ci can run the suites against unreleased builds. Setup fails if the lockfile still holds a registry version; the yaml is not restored (disposable checkouts).
1 parent 6cf439b commit 12e2a73

3 files changed

Lines changed: 83 additions & 19 deletions

File tree

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ packages/vscode/e2e/fixtures/*/pnpm-lock.yaml
157157
packages/vscode/e2e/lint/fixtures/pnpm-lock.yaml
158158
packages/vscode/e2e/lint/fixtures/*/pnpm-lock.yaml
159159
packages/vscode/e2e/rstest/fixtures/*/pnpm-lock.yaml
160+
packages/vscode/e2e/**/.toolchain/
160161

161162
# Build-time copy of the workspace root LICENSE (see rslib.config.mts)
162163
packages/vscode/LICENSE

‎packages/vscode/AGENTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,4 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten
7070
- Prefer running the E2E slice that covers the change over the full chain: `pnpm test:e2e <slice ...>` (or the `test:e2e:<slice>` aliases). Slices are declared in the `SLICES` table in `e2e/run.mjs` (name, fixtures, entry) — the package.json scripts are thin forwards and carry no slice knowledge. `RSTACK_LINT_E2E_SUITES=<name,...>` filters lint suites.
7171
- Run E2E locally as `VSCODE_CLI=1 pnpm test:e2e <slice ...>`. Without it, the launched VS Code overwrites the extension host's `PATH` with a login-shell snapshot; on a machine whose login-shell `node` is below the runtime floor, the User Node preflight (correctly) refuses and every fmt test times out waiting for a server. CI is unaffected — its PATH `node` is new enough either way. The heavier alternative, `--force-disable-user-env` in `e2e/runTest.ts`, was deliberately not taken: it would change env fidelity for every slice.
7272
- Linux E2E runs only in rstack-ecosystem-ci, which relies on `e2e/run.mjs` re-execing itself under `xvfb-run -a` when `DISPLAY` is unset (the whole chain runs against one X server, so `pnpm test:e2e` is the same command everywhere; `xvfb` is a prerequisite on Linux). This repo's CI runs E2E on Windows and macOS only, so the Linux path is not exercised here.
73+
- `RSTACK_E2E_TOOLCHAIN_OVERRIDES="<name>=<package dir>,…"` packs each package into `<fixture>/.toolchain/` and adds a `file:` override to every fixture's `pnpm-workspace.yaml`, so rstack-ecosystem-ci can run the suites against unreleased builds; setup fails if the lockfile still holds a registry version. The yaml is not restored — the env is for disposable checkouts; locally, `git checkout -- packages/vscode/e2e`. Rationale in the `setupFixtures.mjs` header.

‎packages/vscode/e2e/setupFixtures.mjs‎

Lines changed: 81 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// The fixtures install **exact published npm versions** of
44
// `@rslint/core` / `@rstest/core` / `rstack` — the extension resolves all three
55
// from the project, so a fixture that linked this repo's own node_modules would
6-
// test nothing. `rstack@0.7.2` itself pins `@rslint/core@0.9.0` exactly, so the
6+
// test nothing. `rstack@0.8.2` itself pins `@rslint/core@0.9.5` exactly, so the
77
// Rstack fixture pins its lint core transitively. Each fixture is its own
88
// independent install. Exact toolchain pins make installs reproducible
99
// without committed lockfiles, and Renovate bumps those pins.
@@ -22,8 +22,21 @@
2222
//
2323
// Idempotent: pnpm is a no-op when the fixture is already up to date, so
2424
// `test:e2e` can always run it.
25+
//
26+
// `RSTACK_E2E_TOOLCHAIN_OVERRIDES="@rstest/core=/abs/rstest/packages/core,…"`
27+
// installs unreleased builds instead, for rstack-ecosystem-ci (#40). Its root
28+
// `pnpm-workspace.yaml` override cannot reach the fixtures, which are their own
29+
// workspace roots, so each package is packed into `<fixture>/.toolchain/` and
30+
// the fixture yaml gets a relative `file:` override: the dependency-recovery
31+
// suites copy a fixture to tmpdir and reinstall with `--frozen-lockfile`, and
32+
// pnpm records `file:` relative to the lockfile. After install the lockfile
33+
// must hold no registry version of an overridden package — the build and the
34+
// pin can share a version number, so the number proves nothing. The yaml is
35+
// changed in place and never restored: the env is meant for disposable
36+
// checkouts (ecosystem CI); locally, `git checkout -- packages/vscode/e2e`
37+
// undoes it.
2538
import { spawnSync } from 'node:child_process';
26-
import { existsSync } from 'node:fs';
39+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
2740
import path from 'node:path';
2841
import { fileURLToPath } from 'node:url';
2942

@@ -69,19 +82,71 @@ export const FIXTURE_NAMES = Object.keys(FIXTURES);
6982

7083
const pnpmCommand = 'pnpm';
7184

85+
/** `name=<package dir>` entries, comma-separated; unset → none. */
86+
const overrides = (process.env.RSTACK_E2E_TOOLCHAIN_OVERRIDES ?? '')
87+
.split(',')
88+
.filter((entry) => entry.trim())
89+
.map((entry) => {
90+
const [pkg, dir] = entry.split(/=(.*)/).map((part) => part.trim());
91+
return { pkg, dir, file: `${pkg.replace(/^@/, '').replace('/', '-')}.tgz` };
92+
});
93+
94+
/**
95+
* @param {string[]} args
96+
* @param {string} cwd
97+
*/
98+
const pnpm = (args, cwd) => {
99+
const result = spawnSync(pnpmCommand, args, {
100+
cwd,
101+
stdio: 'inherit',
102+
env: process.env,
103+
// On Windows, pnpm is a .cmd shim, and Node refuses to spawn batch
104+
// files without a shell (CVE-2024-27980 hardening) — EINVAL otherwise.
105+
shell: process.platform === 'win32',
106+
});
107+
if (result.error) {
108+
throw result.error;
109+
}
110+
if (result.status !== 0) {
111+
throw new Error(
112+
`pnpm ${args[0]} failed in ${cwd} (exit code ${String(result.status)})`,
113+
);
114+
}
115+
};
116+
117+
/** @param {string} cwd */
118+
const injectOverrides = (cwd) => {
119+
const yamlPath = path.join(cwd, 'pnpm-workspace.yaml');
120+
// A second run in the same checkout must not append a duplicate key.
121+
if (readFileSync(yamlPath, 'utf8').includes('.toolchain/')) {
122+
return;
123+
}
124+
mkdirSync(path.join(cwd, '.toolchain'), { recursive: true });
125+
const lines = ['# RSTACK_E2E_TOOLCHAIN_OVERRIDES: .toolchain/', 'overrides:'];
126+
for (const { pkg, dir, file } of overrides) {
127+
pnpm(['pack', '--out', path.join(cwd, '.toolchain', file)], dir);
128+
lines.push(` '${pkg}': 'file:.toolchain/${file}'`);
129+
}
130+
// A fresh build can depend on just-published packages.
131+
lines.push('minimumReleaseAge: 0', '');
132+
appendFileSync(yamlPath, `\n${lines.join('\n')}`);
133+
};
134+
72135
/** @param {string} name */
73136
const install = (name) => {
74137
const cwd = FIXTURES[name];
75138
if (!existsSync(path.join(cwd, 'package.json'))) {
76139
throw new Error(`E2E fixture ${name} has no package.json at ${cwd}`);
77140
}
141+
if (overrides.length > 0) {
142+
injectOverrides(cwd);
143+
}
78144
console.log(`[e2e] installing fixture: ${name}`);
79145
// Keep pnpm's default isolated layout. In the rstack fixture the tool cores
80146
// are transitive dependencies beside rstack in the virtual store, matching
81147
// the layout users get rather than masking resolution bugs with public
82148
// hoisting.
83-
const result = spawnSync(
84-
pnpmCommand,
149+
pnpm(
85150
[
86151
'install',
87152
// Fixtures pin exact toolchain versions rather than committing lockfiles;
@@ -98,22 +163,19 @@ const install = (name) => {
98163
// build scripts as-is.
99164
'--config.dangerouslyAllowAllBuilds=true',
100165
],
101-
{
102-
cwd,
103-
stdio: 'inherit',
104-
env: process.env,
105-
// On Windows, pnpm is a .cmd shim, and Node refuses to spawn batch
106-
// files without a shell (CVE-2024-27980 hardening) — EINVAL otherwise.
107-
shell: process.platform === 'win32',
108-
},
166+
cwd,
109167
);
110-
if (result.error) {
111-
throw result.error;
112-
}
113-
if (result.status !== 0) {
114-
throw new Error(
115-
`pnpm install failed for the ${name} fixture (exit code ${String(result.status)})`,
116-
);
168+
// A registry resolution is `name@1.2.3`, the tarball `name@file:…`; the
169+
// lockfile also covers transitive ones (the rstack fixture's @rstest/core).
170+
for (const { pkg } of overrides) {
171+
const lockfile = readFileSync(path.join(cwd, 'pnpm-lock.yaml'), 'utf8');
172+
const escaped = pkg.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
173+
if (new RegExp(`${escaped}@\\d`).test(lockfile)) {
174+
throw new Error(
175+
`\`${pkg}\` still resolves to a registry version in the ${name} fixture`,
176+
);
177+
}
178+
console.log(`[e2e] ${name}: no registry ${pkg} in pnpm-lock.yaml`);
117179
}
118180
};
119181

0 commit comments

Comments
 (0)