test(deps-installer): isolate the heavy deepRecursive test in its own process (#12388)

test/install/deepRecursive.ts resolves @teambit/bit's enormous circular and
peer-dependency graph. Measured in a CI-faithful container (Linux, Node
22.13.0, amd64, default ~4 GB heap) it peaks at ~3.6 GB — it fits the default
heap on its own, but not with the memory the other deps-installer test files
leave behind in the same jest process (the --experimental-vm-modules module
registry is not reclaimed between files). That overflow is the
"FATAL ERROR: Reached heap limit" the CI suite hit.

Run deepRecursive in a dedicated jest process (.test:heavy) so it gets the
whole default heap to itself, and run the rest (.test:rest) in a separate
process with it excluded via a negative-lookahead path pattern. The two runs
cover every test file exactly once.

This makes the earlier OOM workarounds unnecessary, so they are reverted:
- the 5-way sharding of the suite (deepRecursive was the sole culprit; the
  remaining files are a subset of what historically ran in one process), and
- the workerIdleMemoryLimit in the with-registry jest preset.

No global heap bump: every run stays within Node's default ~4 GB, matching
the budget pnpm has in production.
This commit is contained in:
Zoltan Kochan
2026-06-13 22:21:25 +02:00
committed by GitHub
parent 94c13cc068
commit 5b402ea22b
3 changed files with 16 additions and 14 deletions

View File

@@ -333,7 +333,6 @@ const yamlTextFormat = createFormat<string>({
},
})
const depsInstallerTestShardCount = 5
const registryMockPortForCore = 7769
async function updateManifest (workspaceDir: string, manifest: ProjectManifest, dir: string, nextTag: string): Promise<ProjectManifest> {
@@ -369,8 +368,19 @@ async function updateManifest (workspaceDir: string, manifest: ProjectManifest,
if (manifest.name === '@pnpm/installing.deps-installer') {
// @pnpm/installing.deps-installer tests currently works only with port 7769 due to the usage of
// the next package: pkg-with-tarball-dep-from-registry
scripts['.test'] = Array.from({ length: depsInstallerTestShardCount }, (_, index) => `pn .test:shard --shard=${index + 1}/${depsInstallerTestShardCount}`).join(' && ')
scripts['.test:shard'] = `cross-env PNPM_REGISTRY_MOCK_PORT=${registryMockPortForCore} NODE_OPTIONS="$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" jest`
//
// deepRecursive resolves @teambit/bit's enormous circular/peer graph and
// needs ~3.6 GB on its own — enough to fit Node's default ~4 GB heap, but
// not with the memory the other test files leave behind in the same
// process (Jest's `--experimental-vm-modules` registry isn't reclaimed
// between files). Run it in a dedicated jest process (`.test:heavy`) so it
// gets the whole heap to itself, and run the rest (`.test:rest`) in a
// separate process with it excluded.
const heavyTestPath = 'test/install/deepRecursive.ts'
const testEnv = `cross-env PNPM_REGISTRY_MOCK_PORT=${registryMockPortForCore} NODE_OPTIONS="$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169"`
scripts['.test'] = 'pn .test:heavy && pn .test:rest'
scripts['.test:heavy'] = `${testEnv} jest ${heavyTestPath}`
scripts['.test:rest'] = `${testEnv} jest "^(?!.*deepRecursive)"`
} else {
scripts['.test'] = 'cross-env NODE_OPTIONS="$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" jest'
}

View File

@@ -7,15 +7,6 @@ export default {
// Unfortunately, this means that if two such tests will run at the same time,
// they may break each other.
maxWorkers: 1,
// Recycle the test worker once its heap crosses this limit. Under
// `--experimental-vm-modules` Jest's VM module registry is never released
// between test files, so a long-running process climbs to Node's ~4 GB
// old-space ceiling and dies with an out-of-memory FATAL ERROR. Setting a
// limit is also what keeps `maxWorkers: 1` from collapsing to in-band
// execution (see `shouldRunInBand`): a recyclable worker is spawned instead,
// still serial, so the leaked memory is reclaimed between files without
// reintroducing the dist-tag races `maxWorkers: 1` prevents.
workerIdleMemoryLimit: '1500MB',
// Force Jest to exit after globalTeardown completes. The Verdaccio server
// and lifecycle child-processes spawned during tests may leave ref'd handles
// that prevent the process from exiting on its own.

View File

@@ -53,8 +53,9 @@
"test": "pn compile && pn .test",
"prepublishOnly": "tsgo --build",
"compile": "tsgo --build && pn lint --fix",
".test": "pn .test:shard --shard=1/5 && pn .test:shard --shard=2/5 && pn .test:shard --shard=3/5 && pn .test:shard --shard=4/5 && pn .test:shard --shard=5/5",
".test:shard": "cross-env PNPM_REGISTRY_MOCK_PORT=7769 NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest"
".test": "pn .test:heavy && pn .test:rest",
".test:heavy": "cross-env PNPM_REGISTRY_MOCK_PORT=7769 NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest test/install/deepRecursive.ts",
".test:rest": "cross-env PNPM_REGISTRY_MOCK_PORT=7769 NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest \"^(?!.*deepRecursive)\""
},
"dependencies": {
"@inquirer/prompts": "catalog:",