screentinker/server/test/preflight-deps.test.js
ScreenTinker 1bb24e7604 Choose the SQLite driver at runtime, and ship the FFmpeg licence with the binaries
TWO CHANGES, together because they touch the same packager hunks.

1. THE DRIVER.

The BrightSign package used to be MANUFACTURED. scripts/build-server-zip.sh dropped
better-sqlite3 from package.json and then installed db/sqlite-compat.js into
node_modules under that name, so every require resolved to the façade. It worked —
and it shipped a database layer that no test had ever executed. That is the same
shape as the TELEMETRY_COLLECTOR TDZ crash that took production down while 1676
tests and four CI jobs were green: a build-time rewrite cannot be tested by the
build that performs it.

db/sqlite-driver.js now decides at runtime: the native driver when it loads, the
node:sqlite façade otherwise. One artifact, one code path, and — the point — both
branches reachable from a test. ST_SQLITE_DRIVER=node runs the entire suite the way
a player runs it, and a new CI job does exactly that on Node 24 with --omit=optional
so the fallback is reached the same way it is on hardware, not by an env var alone.

better-sqlite3 becomes an optionalDependency, so a host with no compiler installs
cleanly and falls back rather than failing. preflight-deps stops trying to rebuild a
native module on a host that has no toolchain and a working built-in driver — on a
player that was a five-minute node-gyp failure ending in a server that never started.
Asking for the native driver BY NAME (ST_SQLITE_DRIVER=better-sqlite3) still fails
loudly, because a production box that has lost its native module is broken and should
say so rather than quietly running something else.

⚠️ NODE 24 IN PRACTICE. node:sqlite is unflagged only from 23.4; on the 22.x line it
needs --experimental-sqlite and on 20.x it does not exist. So the code probes rather
than comparing versions, the player package pins engines >=24, and the built-in cases
skip on the Node 20 CI job rather than failing there.

Verified on Node 24, both drivers, full suite:
  better-sqlite3   1762 pass / 0 fail
  node:sqlite      1762 pass / 0 fail
and the built payload resolves node:sqlite with no better-sqlite3 present at all.

2. THE LICENCE.

The ffprobe/ffmpeg binaries added in the previous commit are LGPL 2.1 and statically
linked, so the licence text has to travel WITH them — a link on a website is not the
copy the licence asks to accompany the work. The packager now copies
COPYING.LGPLv2.1 and a build README into bin/, and refuses to build if the licence is
missing. legal/third-party.html gains an LGPL section with the written offer required
by section 6 for static linking, and the exact configure line.

It also drops Sharp, which that page still listed although #263 removed it, and names
what actually does the image work now (jimp, @jsquash/webp, @jsquash/avif).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014kfhrUPit5MCqxeTQyqr56
2026-08-18 20:57:05 -05:00

93 lines
4.7 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');
});