mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-19 08:33:56 -06:00
Some checks are pending
CI / Unit tests (node --test) (push) Waiting to run
CI / Unit tests (node:sqlite fallback, Node 24) (push) Waiting to run
CI / OpenAPI spec lint (push) Waiting to run
CI / Android unit tests (Kotlin schedule evaluator vectors) (push) Waiting to run
CI / Licence gate + SBOM (production deps) (push) Waiting to run
CI / Boot smoke + version check (push) Waiting to run
1.9.38 made better-sqlite3 optional so a compiler-less host installs cleanly and falls back to node:sqlite. That fallback is Node 24 in practice — absent on 20.x, flagged off on 22.x — and this project's declared floor is 22.9. On those runtimes a missing native driver is not a slower server, it is no server. Found by running the suite the way CI does rather than the way it is convenient to: `npm ci` on a machine that declines to run install scripts reported success, silently left no better-sqlite3 on disk, and 291 tests failed with "server did not boot" and nothing anywhere explaining why. npm drops an optional dependency in that situation where it would have installed a required one. missingDeps() now treats optionalDependencies as required whenever node:sqlite is unavailable, so preflight repairs it as it would any other missing package. The decision is split into requiredDeps(pkg, builtinDriver) so both kinds of host can be tested from either runtime, rather than only the one the suite happens to run on. Full suite 1782 pass / 0 fail on the native driver and on the fallback. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014kfhrUPit5MCqxeTQyqr56
123 lines
6.3 KiB
JavaScript
123 lines
6.3 KiB
JavaScript
'use strict';
|
|
|
|
/*
|
|
* The boot-time dependency check.
|
|
*
|
|
* It exists for the moments nobody is at their best: a rollback that restores an older
|
|
* package.json but not its packages, and a Node upgrade that leaves the native database module
|
|
* compiled against the wrong ABI. Both present as "server will not start", with an error naming a
|
|
* file rather than the action needed.
|
|
*/
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const preflight = require('../lib/preflight-deps');
|
|
|
|
test('a healthy install reports nothing missing and nothing broken', () => {
|
|
assert.deepEqual(preflight.missingDeps(), [], 'this tree should be complete');
|
|
assert.equal(preflight.nativeModuleBroken(), null, 'and the native module should load');
|
|
});
|
|
|
|
test('THE NATIVE CHECK CONSTRUCTS A DATABASE, it does not merely require the module', () => {
|
|
/*
|
|
* better-sqlite3's entry point is plain JavaScript that loads the compiled binding lazily, so
|
|
* `require()` SUCCEEDS under a Node whose ABI the binary was never built for. The first version
|
|
* of this check stopped at require and therefore reported a genuinely broken install — verified
|
|
* against a real Node 18 / Node 20 mismatch — as healthy.
|
|
*
|
|
* Pinned as source because the failure is invisible: the check keeps passing, on every machine
|
|
* where nothing is wrong, right up until the one where something is.
|
|
*/
|
|
const src = fs.readFileSync(path.join(__dirname, '..', 'lib', 'preflight-deps.js'), 'utf8');
|
|
const fn = src.slice(src.indexOf('function nativeModuleBroken'), src.indexOf('function run('));
|
|
assert.match(fn, /new Database\(':memory:'\)/,
|
|
'nativeModuleBroken must open a database, or an ABI mismatch goes undetected');
|
|
assert.ok(!/^\s*require\('better-sqlite3'\);\s*$/m.test(fn), 'a bare require is not a load test');
|
|
});
|
|
|
|
test('missingDeps agrees with what is actually on disk', () => {
|
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
const declared = Object.keys(pkg.dependencies || {});
|
|
assert.ok(declared.length > 0, 'the server declares dependencies');
|
|
const reported = preflight.missingDeps();
|
|
for (const name of declared) {
|
|
const present = fs.existsSync(path.join(__dirname, '..', 'node_modules', name, 'package.json'));
|
|
assert.equal(present, !reported.includes(name), `${name}: presence and report disagree`);
|
|
}
|
|
});
|
|
|
|
test('the preflight uses only Node builtins', () => {
|
|
/*
|
|
* It runs BEFORE dependencies are installed, so anything it imported could be the very thing
|
|
* that is missing — and the failure would be the one it exists to prevent, with an extra layer
|
|
* of confusion on top.
|
|
*/
|
|
const src = fs.readFileSync(path.join(__dirname, '..', 'lib', 'preflight-deps.js'), 'utf8');
|
|
const requires = [...src.matchAll(/require\('([^']+)'\)/g)].map((m) => m[1]);
|
|
const builtins = new Set(['fs', 'path', 'child_process', 'os', 'crypto', 'util']);
|
|
for (const r of requires) {
|
|
// better-sqlite3 is the thing being TESTED for loadability, not a dependency of this file.
|
|
if (r === 'better-sqlite3') continue;
|
|
/*
|
|
* A `node:`-prefixed specifier can ONLY resolve to a builtin - that is what the prefix is for -
|
|
* so it can never be the missing package this file exists to diagnose.
|
|
*
|
|
* `node:sqlite` arrives here as the fallback-driver probe. Note it is genuinely ABSENT on Node
|
|
* 20 and flagged-off on 22.x, which is exactly why preflight requires it inside a try/catch and
|
|
* treats the throw as an answer rather than an error.
|
|
*/
|
|
if (r.startsWith('node:')) continue;
|
|
assert.ok(builtins.has(r) || r.startsWith('.'), `preflight must not depend on ${r}`);
|
|
}
|
|
});
|
|
|
|
test('it can be turned off for an air-gapped host', () => {
|
|
const src = fs.readFileSync(path.join(__dirname, '..', 'lib', 'preflight-deps.js'), 'utf8');
|
|
assert.match(src, /ST_SKIP_DEP_PREFLIGHT/, 'an operator who manages node_modules must be able to opt out');
|
|
const saved = process.env.ST_SKIP_DEP_PREFLIGHT;
|
|
process.env.ST_SKIP_DEP_PREFLIGHT = '1';
|
|
try { assert.doesNotThrow(() => preflight.preflight(), 'opting out must be a clean no-op'); }
|
|
finally { if (saved === undefined) delete process.env.ST_SKIP_DEP_PREFLIGHT; else process.env.ST_SKIP_DEP_PREFLIGHT = saved; }
|
|
});
|
|
|
|
test('server.js runs the preflight BEFORE requiring anything', () => {
|
|
const src = fs.readFileSync(path.join(__dirname, '..', 'server.js'), 'utf8');
|
|
const preflightAt = src.indexOf("require('./lib/preflight-deps')");
|
|
const firstDep = src.indexOf("require('express')");
|
|
assert.ok(preflightAt > -1, 'server.js must run the preflight');
|
|
assert.ok(preflightAt < firstDep, 'it must come before the first dependency, or it cannot help');
|
|
});
|
|
|
|
/*
|
|
* ⚠️ AN OPTIONAL DEPENDENCY IS NOT OPTIONAL ON A HOST WITH NO FALLBACK.
|
|
*
|
|
* better-sqlite3 became optional so a compiler-less host installs cleanly and drops to the built-in
|
|
* node:sqlite. But the built-in is Node 24 in practice — absent on 20.x, flagged off on 22.x — and
|
|
* this project's declared floor is 22.9. On those runtimes a missing better-sqlite3 is not a slower
|
|
* server, it is no server.
|
|
*
|
|
* This is not theoretical: `npm ci` on a machine that declines to run install scripts reported
|
|
* success, silently left no better-sqlite3 on disk, and every test that spawns the server failed
|
|
* with "server did not boot". Preflight has to notice and repair that.
|
|
*/
|
|
test('an optional dependency counts as required where node:sqlite is unavailable', () => {
|
|
const pkg = { dependencies: { express: '^4' }, optionalDependencies: { 'better-sqlite3': '12.9.0' } };
|
|
|
|
const withBuiltin = preflight.requiredDeps(pkg, true);
|
|
assert.deepEqual(withBuiltin, ['express'],
|
|
'on Node 24 the native driver is genuinely optional — the built-in can serve');
|
|
|
|
const withoutBuiltin = preflight.requiredDeps(pkg, false);
|
|
assert.deepEqual(withoutBuiltin.sort(), ['better-sqlite3', 'express'],
|
|
'on Node 20/22 a missing native driver means the server cannot start, so repair it');
|
|
});
|
|
|
|
test('requiredDeps copes with a package.json missing either section', () => {
|
|
assert.deepEqual(preflight.requiredDeps({}, false), []);
|
|
assert.deepEqual(preflight.requiredDeps({ optionalDependencies: { a: '1' } }, false), ['a']);
|
|
assert.deepEqual(preflight.requiredDeps({ dependencies: { b: '1' } }, false), ['b']);
|
|
});
|