diff --git a/bin/cli/runtime/sqliteRuntime.mjs b/bin/cli/runtime/sqliteRuntime.mjs index 506481a353..e8ca615bbf 100644 --- a/bin/cli/runtime/sqliteRuntime.mjs +++ b/bin/cli/runtime/sqliteRuntime.mjs @@ -6,7 +6,9 @@ import { pathToFileURL } from "node:url"; import { validateBinaryMagic, platformBinaryLabel } from "./magicBytes.mjs"; const RUNTIME_DIR = join(homedir(), ".omniroute", "runtime"); -const BETTER_SQLITE3_VERSION = "better-sqlite3@^12.10.1"; +// Exported so the packaging coherence guard (tests/unit/pack-boot-runtime-paths.test.ts) +// can assert this stays on the same major as optionalDependencies.better-sqlite3 (#11242). +export const BETTER_SQLITE3_VERSION = "better-sqlite3@^13.0.2"; let resolvedCached = null; diff --git a/scripts/check/check-pack-boot.mjs b/scripts/check/check-pack-boot.mjs index 673decdd21..2ca4097a0a 100644 --- a/scripts/check/check-pack-boot.mjs +++ b/scripts/check/check-pack-boot.mjs @@ -26,10 +26,16 @@ const MAX_SERVER_OUTPUT_CHARS = 1_000_000; const SQLJS_STARTUP_MARKER = "Pre-initializing sql.js WASM"; const DEFAULT_CLI_SALT = "omniroute-cli-auth-v1"; +// Dependency-based packaging (#11242): the tarball can never contain a node_modules +// path (files[] has "!**/node_modules/**" and check:pack-artifact fails on the +// segment), so sql.js must be required where a clean `npm install` of the declared +// `dependencies` places it — /node_modules/sql.js — NOT under the old +// vendored dist/node_modules location. The runtime resolves the WASM the same way +// (src/lib/db/adapters/sqljsAdapter.ts → /node_modules/sql.js/dist/sql-wasm.wasm). export const REQUIRED_SQLJS_RUNTIME_FILES = Object.freeze([ - "dist/node_modules/sql.js/package.json", - "dist/node_modules/sql.js/dist/sql-wasm.js", - "dist/node_modules/sql.js/dist/sql-wasm.wasm", + "node_modules/sql.js/package.json", + "node_modules/sql.js/dist/sql-wasm.js", + "node_modules/sql.js/dist/sql-wasm.wasm", ]); export const REQUIRED_MACHINE_TOKEN_RUNTIME_FILES = Object.freeze([ diff --git a/tests/unit/check-pack-boot.test.ts b/tests/unit/check-pack-boot.test.ts index 56abe03176..2a7e0edf07 100644 --- a/tests/unit/check-pack-boot.test.ts +++ b/tests/unit/check-pack-boot.test.ts @@ -71,10 +71,13 @@ test("installed package contract requires sql.js metadata, entrypoint, and WASM" [] ); - present.delete(path.join("/pkg", "dist/node_modules/sql.js/dist/sql-wasm.wasm")); + // Dependency-based packaging (#11242): sql.js is a declared dependency, so the + // contract path is the npm-installed /node_modules/sql.js location, + // never the old vendored dist/node_modules one (banned from the tarball). + present.delete(path.join("/pkg", "node_modules/sql.js/dist/sql-wasm.wasm")); assert.deepEqual( findMissingSqlJsRuntimeFiles("/pkg", (file) => present.has(file)), - ["dist/node_modules/sql.js/dist/sql-wasm.wasm"] + ["node_modules/sql.js/dist/sql-wasm.wasm"] ); }); diff --git a/tests/unit/pack-boot-runtime-paths.test.ts b/tests/unit/pack-boot-runtime-paths.test.ts new file mode 100644 index 0000000000..8b9016a245 --- /dev/null +++ b/tests/unit/pack-boot-runtime-paths.test.ts @@ -0,0 +1,78 @@ +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"; + +import { + REQUIRED_MACHINE_TOKEN_RUNTIME_FILES, + REQUIRED_SQLJS_RUNTIME_FILES, +} from "../../scripts/check/check-pack-boot.mjs"; +import { PACK_ARTIFACT_NEVER_ALLOWED_SEGMENTS } from "../../scripts/build/pack-artifact-policy.ts"; +import * as sqliteRuntime from "../../bin/cli/runtime/sqliteRuntime.mjs"; + +// Coherence guard for the v3.8.50 publish blocker (#11242): check:pack-artifact +// FAILS any tarball path containing a node_modules segment (files[] excludes them +// via "!**/node_modules/**"), while check:pack-boot REQUIRED sql.js under the +// vendored dist/node_modules/ location — a path the tarball can never contain, +// so the two gates could never be green at the same time. The npm packaging +// model is now dependency-based: sql.js and node-machine-id are declared +// `dependencies` that a clean install places under /node_modules/, +// and better-sqlite3 is an optionalDependency installed natively per platform. +// These tests pin that contract so neither gate can drift back into conflict. + +const REPO_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", ".."); +const PKG = JSON.parse(readFileSync(path.join(REPO_ROOT, "package.json"), "utf8")) as { + dependencies?: Record; + optionalDependencies?: Record; +}; + +test("pack-boot required runtime files never reference a never-publishable vendored path", () => { + const requiredFiles = [...REQUIRED_SQLJS_RUNTIME_FILES, ...REQUIRED_MACHINE_TOKEN_RUNTIME_FILES]; + assert.ok(requiredFiles.length > 0, "pack-boot must require at least one runtime file"); + for (const requiredPath of requiredFiles) { + for (const segment of PACK_ARTIFACT_NEVER_ALLOWED_SEGMENTS) { + const vendoredPrefix = `dist/${segment}/`; + assert.ok( + !requiredPath.includes(vendoredPrefix), + `"${requiredPath}" lives under ${vendoredPrefix} — check:pack-artifact bans any ` + + `tarball path with a "${segment}" segment, so check:pack-boot must require the ` + + `dependency-installed location (node_modules/) instead (#11242)` + ); + } + } +}); + +test("sql.js and node-machine-id are declared runtime dependencies (npm installs them)", () => { + assert.ok( + PKG.dependencies?.["sql.js"], + "sql.js must stay in dependencies so a clean install provides node_modules/sql.js" + ); + assert.ok( + PKG.dependencies?.["node-machine-id"], + "node-machine-id must stay in dependencies so a clean install provides node_modules/node-machine-id" + ); +}); + +test("the lazy better-sqlite3 runtime install targets the declared optionalDependency major", () => { + const spec = (sqliteRuntime as Record).BETTER_SQLITE3_VERSION; + assert.equal( + typeof spec, + "string", + "bin/cli/runtime/sqliteRuntime.mjs must export BETTER_SQLITE3_VERSION" + ); + const declared = PKG.optionalDependencies?.["better-sqlite3"]; + assert.ok(declared, "package.json must declare better-sqlite3 as an optionalDependency"); + + const majorOf = (versionSpec: string): number => { + const match = versionSpec.match(/(\d+)\./); + assert.ok(match, `"${versionSpec}" must contain a semver major`); + return Number(match[1]); + }; + assert.equal( + majorOf(spec as string), + majorOf(declared), + `lazy runtime install "${spec}" drifted from optionalDependencies.better-sqlite3 ` + + `"${declared}" — the fallback install must track the same major (#11242)` + ); +});