Files
hermes-agent/tests-js/node-engine-alignment.test.ts
Maciej Lisiewski 7062229090 chore(js): switch the workspaces from npm to pnpm
pnpm 12.15.0 (native executable) installs every JS workspace from one
pnpm-lock.yaml, imported from package-lock.json with the same resolved
versions. website/, the WhatsApp bridge and the photon sidecar stay separate
npm projects.

Configuration moves from .npmrc and the root manifest into
pnpm-workspace.yaml: the 14-day minimumReleaseAge and its excludes,
overrides, allowBuilds (the old allowScripts set, with strictDepBuilds
failing any unlisted build script), engineStrict, sideEffectsCache: false
(in-place rebuilds must not write into the store) and
verifyDepsBeforeRun: false (a `pnpm run` never installs).

The pnpm version is pinned in pm/lock.json and in root devEngines.
pmOnFail: ignore keeps that pin out of pnpm-lock.yaml: a leading pin
document makes osv-scanner read zero packages. scripts/build/node-deps.mjs
enforces the exact version instead, and every lock reader refuses a
multi-document lock. A root preinstall refuses `npm install`; devEngines
onFail is warn so npx (MCP servers, the tsc linter) keeps working inside
the checkout.

User-machine installs run --frozen-lockfile --trust-lockfile (release age is
checked when locking, in CI, not per user). node-deps removes npm-era
node_modules trees before the first pnpm install. Nix builds the store with
fetchPnpmDeps and a hash file that CI checks.
2026-11-06 04:08:24 +01:00

60 lines
2.5 KiB
TypeScript

import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import { load } from 'js-yaml'
// semver ships no declarations in this lock; type the two library calls used here.
const semver: { valid(version: string): string | null; satisfies(version: string, range: string): boolean } = require('semver')
import { test } from 'vitest'
interface Engines { node?: string }
interface Manifest {
engines?: Engines
devEngines?: { packageManager?: { name?: string; version?: string; onFail?: string } }
}
interface Lockfile { packages?: Record<string, Manifest> }
interface PmLock { packages: { node: { version: string }; pnpm: { version: string } } }
function read(relativePath: string): string {
return fs.readFileSync(path.resolve(__dirname, '..', relativePath), 'utf8')
}
// SAFETY: these are repository-owned manifest/lock inputs, checked below.
const root = JSON.parse(read('package.json')) as Manifest
const desktop = JSON.parse(read('apps/desktop/package.json')) as Manifest
const lock = load(read('pnpm-lock.yaml')) as Lockfile
const pins = (JSON.parse(read('pm/lock.json')) as PmLock).packages
function nodeRange(manifest: Manifest): string {
assert.ok(manifest.engines?.node, 'workspace must declare engines.node')
return manifest.engines.node
}
test.each([
['26.0.0', true], ['26.12.1', true], ['27.0.0', true],
['22.22.0', false], ['22.23.1', false], ['24.11.0', false], ['24.18.2', false], ['25.2.1', false], ['26.0.0-rc.1', false],
] as const)('workspace Node policy for %s is %s', (version: string, accepted: boolean): void => {
for (const manifest of [root, desktop]) {
assert.equal(semver.satisfies(version, nodeRange(manifest)), accepted)
}
})
test('exact independently managed Node satisfies all declared lockfile engines', (): void => {
assert.ok(semver.valid(pins.node.version), 'node must be an exact pin')
assert.ok(Object.keys(lock.packages ?? {}).length > 0, 'pnpm-lock.yaml has no packages')
for (const [name, manifest] of Object.entries({ ...lock.packages, root, desktop })) {
const range = manifest.engines?.node
if (range) {
assert.ok(semver.satisfies(pins.node.version, range), `${name}: pinned node ${pins.node.version} violates ${range}`)
}
}
})
test('the managed pnpm is exactly the one the repository requires, and any other fails', (): void => {
assert.ok(semver.valid(pins.pnpm.version), 'pnpm must be an exact pin')
assert.deepEqual(root.devEngines?.packageManager, { name: 'pnpm', version: pins.pnpm.version, onFail: 'warn' })
})