/** * Node engine test — locks in the [E00-S01-T08] Node engine restriction to * 24.x for the workspace. * * Acceptance criteria covered (each test fails without the committed config): * - "Node engine is restricted to 24.x in the package manifest" → root * `package.json` declares `engines.node` exactly `>=24.0.0 <25.0.0`, and a * minimal semver range check proves the range admits 24.x versions * (24.0.0 … 24.99.99) while excluding every other major line. * - "an unsupported Node version is rejected" → the same range check proves * 18.x/22.x/23.x/25.x/26.x are outside the range, the committed * `pnpm-workspace.yaml` sets `engineStrict: true` so pnpm hard-fails the * install on those versions (ERR_PNPM_UNSUPPORTED_ENGINE) instead of * warning, and the current runtime is asserted to satisfy the range (so * `pnpm test` on an unsupported Node fails loudly). * * Run: `node --test tests/node-engine.test.mjs` * (node:test — built into Node >= 18; no dependencies, lockfile untouched.) */ import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; 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 engines.node value committed in the root package.json (24.x). */ const NODE_ENGINE_RANGE = '>=24.0.0 <25.0.0'; /** Versions inside the 24.x line that must satisfy the range. */ const SUPPORTED_EXAMPLES = ['24.0.0', '24.1.0', '24.20.0', '24.99.99']; /** Versions outside the 24.x line that must be rejected by the range. */ const UNSUPPORTED_EXAMPLES = ['18.0.0', '20.0.0', '22.0.0', '22.23.2', '23.0.0', '25.0.0', '26.0.0']; // --------------------------------------------------------------------------- // Minimal semver range checker (no dependencies, lockfile untouched) // --------------------------------------------------------------------------- /** Parses "v?MAJOR.MINOR.PATCH" (pre-release/build suffixes ignored). */ function parseVersion(version) { const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(String(version).trim()); assert.ok(match, `"${version}" is not a MAJOR.MINOR.PATCH semver version`); return [Number(match[1]), Number(match[2]), Number(match[3])]; } /** Compares two [major, minor, patch] tuples: -1, 0 or 1. */ function compareVersions(a, b) { for (let i = 0; i < 3; i += 1) { if (a[i] < b[i]) return -1; if (a[i] > b[i]) return 1; } return 0; } /** * Checks a version against a whitespace-separated comparator range such as * ">=24.0.0 <25.0.0" (AND semantics). Throws on any comparator form this * workspace does not use, so an intentional range change must update this * checker in the same change. */ function satisfies(version, range) { const comparators = range.trim().split(/\s+/); assert.ok(comparators.length > 0, `engines range must not be empty: "${range}"`); const v = parseVersion(version); return comparators.every((token) => { const match = /^(>=|<=|>|<|=)?v?(\d+)\.(\d+)\.(\d+)$/.exec(token); assert.ok(match, `unsupported comparator in engines range "${range}": "${token}"`); const [, operator = '=', major, minor, patch] = match; const c = compareVersions(v, [Number(major), Number(minor), Number(patch)]); switch (operator) { case '>=': return c >= 0; case '<=': return c <= 0; case '>': return c > 0; case '<': return c < 0; default: return c === 0; // '=' (exact) } }); } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- test('root package.json declares engines.node restricted to 24.x', () => { const manifest = JSON.parse(read('package.json')); assert.equal( manifest.engines?.node, NODE_ENGINE_RANGE, 'root package.json must declare engines.node exactly ">=24.0.0 <25.0.0" (24.x)', ); for (const version of SUPPORTED_EXAMPLES) { assert.ok(satisfies(version, NODE_ENGINE_RANGE), `${version} must satisfy ${NODE_ENGINE_RANGE}`); } }); test('the declared engines range rejects unsupported Node versions', () => { for (const version of UNSUPPORTED_EXAMPLES) { assert.equal( satisfies(version, NODE_ENGINE_RANGE), false, `${version} must be outside the 24.x range (${NODE_ENGINE_RANGE})`, ); } }); test('the current Node runtime satisfies the declared engines range', () => { assert.ok( satisfies(process.versions.node, NODE_ENGINE_RANGE), `runtime Node ${process.versions.node} is not within the supported 24.x line — ` + 'install a Node 24.x release (install is rejected via engine-strict otherwise)', ); }); test('the committed pnpm-workspace.yaml enforces engines so an unsupported Node is rejected', () => { const workspace = read('pnpm-workspace.yaml'); assert.ok( workspace.includes('engineStrict: true'), 'pnpm-workspace.yaml must set engineStrict: true so pnpm rejects an unsupported Node version at install', ); }); test('committed CI workflow installs on Node 24', () => { const workflow = read('.gitea/workflows/ci.yml'); assert.ok( workflow.includes("node-version: '24'"), 'CI must run on Node 24 (the supported engines line)', ); });