/** * Frozen-install test — locks in the [E00-S01-T13] clean-clone frozen * lockfile install for the workspace. * * Acceptance criteria covered (each test fails without the committed config): * - "a clean clone installs with a frozen lockfile" → a fresh clone of the * committed tree (no node_modules, no untracked files) runs * `pnpm install --frozen-lockfile` — through the pinned pnpm 11.23.0 * (`corepack pnpm`, the same path CI and developers use) — and exits 0 * with a populated `node_modules/` virtual store. * - "the frozen install completes without resolving new versions" → pnpm * reports the lockfile as up to date and skips the resolution step, the * committed `pnpm-lock.yaml` is byte-identical after the install (a frozen * install never rewrites the lockfile), and the installed virtual store * matches the lockfile exactly: every package resolved in the lockfile * `packages:` section is present in `node_modules/.pnpm/@` * and no extra package versions are installed (so the install produced * exactly the locked dependency tree, nothing resolved beyond it). * * Run: `node --test tests/frozen-install.test.mjs` * (node:test — built into Node >= 18; no dependencies, lockfile untouched.) */ import test, { before, after } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync, readdirSync, existsSync, mkdtempSync, rmSync } from 'node:fs'; import { spawnSync } from 'node:child_process'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const read = (relPath) => readFileSync(path.join(REPO_ROOT, relPath), 'utf8'); /** The exact TypeScript version the workspace is pinned to ([E00-S01-T09]). */ const PINNED_TYPESCRIPT = '6.0.3'; /** Relative path of the pnpm virtual store inside a clone (pnpm 11 default). */ const VIRTUAL_STORE = 'node_modules/.pnpm'; // --------------------------------------------------------------------------- // Lockfile → virtual-store helpers (no dependencies, lockfile untouched) // --------------------------------------------------------------------------- /** * Extracts the package entry keys from the `packages:` section of a pnpm 9.x * lockfile, e.g. `typescript@6.0.3` or `@scope/name@1.2.3(peer@2.0.0)`. * pnpm 11 quotes scoped package keys in the lockfile YAML (e.g. * `'@types/node@24.13.3':`), so surrounding single quotes are stripped to * yield the plain `@` key the virtual-store mapping expects. */ function lockedPackageKeys(lockfileText) { const packagesSection = lockfileText.slice( lockfileText.indexOf('packages:'), lockfileText.indexOf('snapshots:'), ); return [...packagesSection.matchAll(/^ (\S+):$/gm)].map((m) => { const key = m[1]; return key.length >= 2 && key.startsWith("'") && key.endsWith("'") ? key.slice(1, -1) : key; }); } /** * Maps a lockfile package key to the directory name pnpm gives it inside the * virtual store: scoped names get their `/` rewritten to `+` (`@scope/name@1.2.3` * → `@scope+name@1.2.3`), peer-dependency suffixes are dropped (`(peer@2.0.0)` * → `_peer@2.0.0` appended to the dir name). */ function virtualStoreDirName(packageKey) { const withoutPeers = packageKey.replace(/\([^)]*\)$/, ''); const at = withoutPeers.lastIndexOf('@'); assert.ok(at > 0, `lockfile package key "${packageKey}" must be @`); const name = withoutPeers.slice(0, at); const version = withoutPeers.slice(at + 1); const dirName = name.startsWith('@') ? `${name.replace('/', '+')}@${version}` : `${name}@${version}`; return { dirName, name, version }; } /** * Returns the locked package keys whose virtual-store directory is missing * from the given `.pnpm` directory listing. A locked entry matches a directory * either exactly (`typescript@6.0.3`) or by the `_` peer-suffix prefix pnpm * appends (`typescript@6.0.3_peer@2.0.0`). */ function missingLockedDirs(packageKeys, pnpmDirEntries) { const missing = []; for (const key of packageKeys) { const { dirName } = virtualStoreDirName(key); const found = pnpmDirEntries.some( (entry) => entry === dirName || entry.startsWith(`${dirName}_`), ); if (!found) missing.push(key); } return missing; } // --------------------------------------------------------------------------- // Clean-clone fixture: one clone + one frozen install, shared by all tests // --------------------------------------------------------------------------- /** Temp dir state shared across the tests in this file. */ let cloneDir; let install; before(() => { // A clean clone: only the committed tree, cloned into a temp dir. This is a // fresh copy — no node_modules, no .pnpm store, no untracked files — exactly // what `git clone` produces for a new developer. cloneDir = mkdtempSync(path.join(os.tmpdir(), 'eppp-frozen-install-')); const cloned = spawnSync('git', ['clone', '--quiet', '--no-hardlinks', REPO_ROOT, cloneDir], { encoding: 'utf8', timeout: 60_000, }); assert.equal( cloned.status, 0, `git clone of the committed tree failed: ${(cloned.stderr || cloned.stdout || '').trim()}`, ); assert.ok( existsSync(path.join(cloneDir, 'pnpm-lock.yaml')), 'the clean clone must contain the committed pnpm-lock.yaml', ); // Frozen install through the pinned pnpm (corepack), exactly like CI. install = spawnSync('corepack', ['pnpm', 'install', '--frozen-lockfile'], { cwd: cloneDir, encoding: 'utf8', timeout: 300_000, env: { ...process.env, COREPACK_ENABLE_DOWNLOAD_PROMPT: '0', npm_config_update_notifier: 'false' }, }); if (install.status !== 0) { throw new Error( `pnpm install --frozen-lockfile failed in the clean clone:\n` + `${(install.stdout || '')}\n${(install.stderr || '')}`.trim(), ); } }); after(() => { // Guarded: the clone may not exist if the before hook failed early. if (cloneDir) rmSync(cloneDir, { recursive: true, force: true }); }); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- test('a clean clone installs with a frozen lockfile (pnpm install --frozen-lockfile)', () => { assert.equal( install.status, 0, `pnpm install --frozen-lockfile must exit 0 in a clean clone:\n` + `${(install.stdout || '')}\n${(install.stderr || '')}`.trim(), ); // The install must have actually installed something (node_modules with the // pnpm virtual store), not silently no-op'd. assert.ok( existsSync(path.join(cloneDir, VIRTUAL_STORE)), 'the frozen install must populate node_modules/.pnpm in the clean clone', ); }); test('the frozen install completes without resolving new versions', () => { // pnpm only reports the lockfile as up to date (and skips resolution) when // the manifests are fully satisfied by the committed lockfile — with // --frozen-lockfile an out-of-date lockfile fails instead of resolving. assert.match( install.stdout, /Lockfile is up to date, resolution step is skipped/, 'pnpm must skip the resolution step (the committed lockfile fully satisfies the manifests)', ); // A frozen install never rewrites the lockfile: it must be byte-identical // to the committed one before and after the install. const committedLockfile = read('pnpm-lock.yaml'); const clonedLockfile = readFileSync(path.join(cloneDir, 'pnpm-lock.yaml'), 'utf8'); assert.equal( clonedLockfile, committedLockfile, 'pnpm-lock.yaml must be byte-identical after the frozen install (no re-resolution, no lockfile drift)', ); }); test('the installed dependency tree matches the lockfile exactly (no new versions)', () => { const lockedKeys = lockedPackageKeys(read('pnpm-lock.yaml')); assert.ok(lockedKeys.length > 0, 'the lockfile packages section must resolve at least one package'); const pnpmDir = path.join(cloneDir, VIRTUAL_STORE); const installedDirs = readdirSync(pnpmDir, { withFileTypes: true }) .filter((entry) => entry.isDirectory() && entry.name !== 'node_modules') .map((entry) => entry.name); // Every package the lockfile resolves must be present in the virtual store // at its locked version — nothing from the lockfile is missing. const missing = missingLockedDirs(lockedKeys, installedDirs); assert.deepEqual( missing, [], `the installed tree is missing locked packages: ${missing.join(', ')}`, ); // And nothing beyond the locked set may be installed: the virtual store // (minus pnpm's internal node_modules dir) contains exactly the locked // package directories, so no extra version was resolved. assert.equal( installedDirs.length, lockedKeys.length, `the virtual store must contain exactly the ${lockedKeys.length} locked packages ` + `(${installedDirs.join(', ')}), one dir per lockfile packages: entry`, ); }); test('the clean clone resolves the exact locked TypeScript version (6.0.3)', () => { const result = spawnSync('corepack', ['pnpm', 'exec', 'tsc', '--version'], { cwd: cloneDir, encoding: 'utf8', timeout: 120_000, }); assert.equal( result.status, 0, `"corepack pnpm exec tsc --version" failed in the clean clone: ${(result.stderr || result.stdout || '').trim()}`, ); assert.equal( result.stdout.trim(), `Version ${PINNED_TYPESCRIPT}`, `the clean clone must resolve TypeScript ${PINNED_TYPESCRIPT} (got "${result.stdout.trim()}")`, ); }); test('the lockfile→virtual-store helpers flag missing packages (non-vacuous probe)', () => { const keys = ['typescript@6.0.3', '@scope/core@1.2.3', 'with-peer@2.0.0(peer@1.0.0)']; // Sanity: the dir-name mapping is what pnpm actually lays out. assert.equal(virtualStoreDirName('typescript@6.0.3').dirName, 'typescript@6.0.3'); assert.equal(virtualStoreDirName('@scope/core@1.2.3').dirName, '@scope+core@1.2.3'); assert.equal(virtualStoreDirName('with-peer@2.0.0(peer@1.0.0)').dirName, 'with-peer@2.0.0'); // A complete store must not be flagged… const complete = ['typescript@6.0.3', '@scope+core@1.2.3', 'with-peer@2.0.0_peer@1.0.0']; assert.deepEqual(missingLockedDirs(keys, complete), []); // …while a store missing or drifting on any locked version must be. const missingOne = ['@scope+core@1.2.3', 'with-peer@2.0.0_peer@1.0.0']; assert.deepEqual(missingLockedDirs(keys, missingOne), ['typescript@6.0.3']); const drifted = ['typescript@6.0.4', '@scope+core@1.2.3', 'with-peer@2.0.0_peer@1.0.0']; assert.deepEqual(missingLockedDirs(keys, drifted), ['typescript@6.0.3']); // Scoped package keys are quoted by pnpm in the lockfile YAML (e.g. // `'@types/node@24.13.3':`) — the extractor must strip the quotes so the // key keeps the plain @ shape the store lookup expects. const quotedScoped = "packages:\n\n '@scope/core@1.2.3':\n resolution: {integrity: x}\n\nsnapshots:\n"; assert.deepEqual(lockedPackageKeys(quotedScoped), ['@scope/core@1.2.3']); });