Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"ui:preview": "npm run ui:build && wrangler dev --config apps/loopover-ui/dist/server/wrangler.json --ip 127.0.0.1 --port 4173 --local",
"ui:lint": "npm run ui:kit:build && npm --workspace @loopover/ui run lint && npm --workspace @loopover/ui-miner run lint",
"ui:typecheck": "npm run ui:kit:build && npm --workspace @loopover/ui run typecheck && npm --workspace @loopover/ui-miner run typecheck",
"preui:test": "npm run check-node-version",
"ui:test": "npm run ui:kit:build && npm --workspace @loopover/ui run test && npm --workspace @loopover/ui-miner run test && npm --workspace @loopover/miner-extension run test",
"ui:openapi": "tsx scripts/write-ui-openapi.ts",
"ui:openapi:check": "tsx scripts/write-ui-openapi.ts --check",
Expand Down Expand Up @@ -88,6 +89,8 @@
"mcp:release-due": "node scripts/check-mcp-release-due.mjs --json",
"mcp:release-candidate": "node scripts/check-mcp-release-candidate.mjs",
"typecheck": "tsc --noEmit",
"check-node-version": "node scripts/check-node-version.mjs",
"pretest": "npm run check-node-version",
"test": "vitest run",
"test:unit": "vitest run test/unit",
"test:integration": "vitest run test/integration",
Expand All @@ -96,13 +99,16 @@
"test:live-gate-parity": "vitest run test/contract/live-gate-parity.test.ts",
"test:driver-parity": "vitest run test/contract/coding-agent-driver-parity.test.ts",
"test:changed": "vitest run --changed=origin/main",
"pretest:workers": "npm run check-node-version",
"test:workers": "vitest run --config vitest.workers.config.ts",
"pretest:coverage": "npm run check-node-version",
"test:coverage": "vitest run --coverage --pool=forks",
"test:smoke:production": "node scripts/smoke-production.mjs",
"test:smoke:observability": "node scripts/smoke-observability-traces.mjs",
"test:smoke:observability:metrics": "node scripts/smoke-observability-metrics.mjs",
"test:smoke:browser:install": "playwright install chromium",
"test:smoke:browser": "node scripts/smoke-ui-browser.mjs",
"pretest:ci": "npm run check-node-version",
"test:ci": "git diff --check && npm run actionlint && npm run lint:composite-actions && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build --workspace @loopover/engine && npm run build --workspace @loopover/discovery-index && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test:live-gate-parity && npm run test:driver-parity && npm run test --workspace @loopover/engine && npm run test:workers && npm run build:mcp && npm run test:mcp-pack && npm run build:miner && npm run test:miner-pack && npm run test:miner-deployment-docs-audit && npm run rees:test && npm run ui:openapi:check && npm run ui:openapi:settings-parity && npm run ui:version-audit && npm run docs:drift-check && npm run branding-drift:check && npm run manifest:drift-check && npm run engine-parity:drift-check && npm run engines-nvmrc:check && npm run release-manifest:sync:check && npm run command-reference:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build",
"test:release": "npm run test:ci && npm run changelog:check",
"test:release:mcp": "npm run test:ci",
Expand Down
8 changes: 8 additions & 0 deletions scripts/check-node-version.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function checkNodeVersion(options?: {
nodeVersion?: string;
readFile?: () => string;
}): {
ok: boolean;
requiredRange: string | undefined;
nodeVersion?: string;
};
36 changes: 36 additions & 0 deletions scripts/check-node-version.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env node
// Fails fast, with a clear message, when the running Node doesn't satisfy root package.json's
// engines.node -- even when node_modules is already installed. The root .npmrc's engine-strict=true only
// fires during npm install/ci (dependency resolution); a node_modules installed while on the pinned Node,
// followed by simply switching the active `node` (nvm/homebrew default change) with no reinstall, sails
// straight past engine-strict on every later `npm run`. That's exactly the shape of gap that let the
// Node 26 jsdom/localStorage bug (#7592/#7597/#7612) go unnoticed the first two times: a pile of
// confusing downstream test failures instead of one clear "wrong Node version" message up front. Wired as
// a `pretest*` hook (see package.json) on the commands people actually run vitest through.
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import semver from "semver";

const PACKAGE_JSON_URL = new URL("../package.json", import.meta.url);

export function checkNodeVersion({ nodeVersion = process.version, readFile = () => readFileSync(PACKAGE_JSON_URL, "utf8") } = {}) {
const pkg = JSON.parse(readFile());
const requiredRange = pkg.engines?.node;
if (!requiredRange) return { ok: true, requiredRange: undefined };

const ok = semver.satisfies(nodeVersion, requiredRange);
return { ok, requiredRange, nodeVersion };
}

function main() {
const { ok, requiredRange, nodeVersion } = checkNodeVersion();
if (!ok) {
console.error(
`\nRunning Node ${nodeVersion}, but this repo requires ${requiredRange} (see .nvmrc / package.json engines).\n` +
`Switch to the pinned Node version (e.g. \`nvm use\`) before running this command.\n`,
);
process.exit(1);
}
}

if (process.argv[1] === fileURLToPath(import.meta.url)) main();
47 changes: 47 additions & 0 deletions test/unit/check-node-version-script.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { execFileSync } from "node:child_process";
import { describe, expect, it } from "vitest";
import { checkNodeVersion } from "../../scripts/check-node-version.mjs";

describe("check-node-version script", () => {
it("passes when the running Node satisfies the declared engines.node range", () => {
const result = checkNodeVersion({
nodeVersion: "v22.23.1",
readFile: () => JSON.stringify({ engines: { node: ">=22.0.0 <23.0.0" } }),
});

expect(result).toEqual({ ok: true, requiredRange: ">=22.0.0 <23.0.0", nodeVersion: "v22.23.1" });
});

it("fails when the running Node is outside the declared engines.node range (the Node 26 case)", () => {
const result = checkNodeVersion({
nodeVersion: "v26.5.0",
readFile: () => JSON.stringify({ engines: { node: ">=22.0.0 <23.0.0" } }),
});

expect(result).toEqual({ ok: false, requiredRange: ">=22.0.0 <23.0.0", nodeVersion: "v26.5.0" });
});

it("passes trivially when package.json declares no engines.node at all", () => {
const result = checkNodeVersion({
nodeVersion: "v26.5.0",
readFile: () => JSON.stringify({}),
});

expect(result).toEqual({ ok: true, requiredRange: undefined });
});

// Regression guard: proves the process actually running this test suite -- CI's own Node, or whatever a
// contributor is running locally -- genuinely satisfies the real repo's engines.node. If this fails, the
// repo is being tested on the wrong Node right now.
it("the real repo's engines.node is satisfied by whatever Node is running this suite", () => {
const result = checkNodeVersion();

expect(result.ok).toBe(true);
});

it("prints nothing and exits 0 as a subprocess on a satisfying Node", () => {
const output = execFileSync(process.execPath, ["scripts/check-node-version.mjs"], { encoding: "utf8" });

expect(output).toBe("");
});
});
Loading