The DC-020 require-path sweep fixed every '../src/...' -> './src/...' in server.js, but missed one: line 73 still had . From the production entry point (/app/server.js) this resolves to /app/state-manager.js — a file that does NOT exist (the module lives at src/managers/state-manager.js). Unlike the optional modules below it, this require is bare (not wrapped in try/catch), so a MODULE_NOT_FOUND here throws out of the top-level startup IIFE and crash-loops the container — the exact same failure mode as the deleted license-keygen.js. Fix: ./state-manager -> ./src/managers/state-manager (matches line 146). Also hardens the DC-020 regression guard (app-startup-smoke.test.js): adds a static check that EVERY relative require() in server.js resolves to a real file on disk. server.js cannot be require()'d at test time (its IIFE binds port 3001 + starts interval modules, leaking workers), so the static scan is what catches this class of entry-point path bug. This test would have failed on the original ./state-manager line. 1067/1067 tests pass (was 1066 baseline + 1 new). Zero new ESLint warnings.
63 lines
3.1 KiB
JavaScript
63 lines
3.1 KiB
JavaScript
/**
|
|
* App startup require-graph smoke test (DC-020 regression guard)
|
|
*
|
|
* WHY THIS EXISTS:
|
|
* The `refactor(desloppify)` commit deleted `license-keygen.js` thinking it was
|
|
* stale dev-root noise. It is actually required by `src/managers/license-manager.js`
|
|
* (`require('./license-keygen')`). The deletion put the production `dashcaddy-api`
|
|
* container in a crash-restart loop (MODULE_NOT_FOUND from /app/src/app.js). A second,
|
|
* masked bug had the same effect from the entry point: server.js used `require('./state-manager')`
|
|
* which from /app/server.js resolves to /app/state-manager.js (does not exist) instead of
|
|
* `./src/managers/state-manager`. The full Jest suite passed anyway because NO test ever
|
|
* executed the real production require graph — every "app" test read src/app.js as a
|
|
* string or rebuilt a minimal Express app with copied handlers, and server.js was never
|
|
* loaded at all (requiring it starts the HTTP server + timers, which would leak workers).
|
|
*
|
|
* This test closes that gap two ways:
|
|
* 1. Execute the real src/app.js require graph (catches deleted-module regressions).
|
|
* 2. Statically verify EVERY relative require in server.js resolves to a real file
|
|
* (catches entry-point path bugs like the ./state-manager regression, without starting
|
|
* the server). server.js cannot be require()'d directly because its top-level IIFE
|
|
* binds port 3001 and starts interval-based feature modules.
|
|
*/
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const ROOT = path.resolve(__dirname, '..');
|
|
|
|
describe('app startup require-graph smoke', () => {
|
|
it('src/app.js and its entire require graph load without throwing', () => {
|
|
expect(() => require(path.join(ROOT, 'src', 'app'))).not.toThrow();
|
|
});
|
|
|
|
it('createApp is exported as a function', () => {
|
|
const mod = require(path.join(ROOT, 'src', 'app'));
|
|
expect(typeof mod.createApp).toBe('function');
|
|
});
|
|
|
|
it('every relative require() in server.js resolves to a real module', () => {
|
|
// server.js is the production entry point (Dockerfile CMD ["node","server.js"]).
|
|
// We statically check its require graph because require()-ing it at test time
|
|
// starts the HTTP server and interval-based modules (would leak the worker).
|
|
const serverFile = path.join(ROOT, 'server.js');
|
|
const src = fs.readFileSync(serverFile, 'utf8')
|
|
// strip block + line comments so example requires in docstrings don't trip us up
|
|
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
.replace(/(^|[^:\\])\/\/.*$/gm, '$1');
|
|
|
|
const requireRe = /require\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
const unresolved = [];
|
|
let match;
|
|
while ((match = requireRe.exec(src))) {
|
|
const spec = match[1];
|
|
if (!spec.startsWith('.')) continue; // only relative specs are path-bug-prone
|
|
const base = path.resolve(path.dirname(serverFile), spec);
|
|
const ok = fs.existsSync(base + '.js') ||
|
|
fs.existsSync(base + '.json') ||
|
|
fs.existsSync(path.join(base, 'index.js'));
|
|
if (!ok) unresolved.push(spec);
|
|
}
|
|
expect(unresolved).toEqual([]);
|
|
});
|
|
});
|