[E00-S01-T13] Clean clone installs with frozen lockfile #380

Merged
kpcto merged 1 commits from feature/166 into main 2026-08-28 23:15:16 +00:00
Showing only changes of commit ca9e0ffaf6 - Show all commits
+244
View File
@@ -0,0 +1,244 @@
/**
* 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/<name>@<version>`
* 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)`.
*/
function lockedPackageKeys(lockfileText) {
const packagesSection = lockfileText.slice(
lockfileText.indexOf('packages:'),
lockfileText.indexOf('snapshots:'),
);
return [...packagesSection.matchAll(/^ (\S+):$/gm)].map((m) => m[1]);
}
/**
* 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 <name>@<version>`);
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']);
});