From f94b164190c299472fd022d3539a0ee49936488b Mon Sep 17 00:00:00 2001 From: Hermes Date: Wed, 1 Jul 2026 07:37:45 -0700 Subject: [PATCH] DC-020: fix last broken require in server.js (./state-manager -> ./src/managers/state-manager) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../__tests__/app-startup-smoke.test.js | 53 +++++++++++++++---- dashcaddy-api/server.js | 2 +- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/dashcaddy-api/__tests__/app-startup-smoke.test.js b/dashcaddy-api/__tests__/app-startup-smoke.test.js index d1b1400..c343a25 100644 --- a/dashcaddy-api/__tests__/app-startup-smoke.test.js +++ b/dashcaddy-api/__tests__/app-startup-smoke.test.js @@ -5,25 +5,58 @@ * 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). The - * full Jest suite (1036 tests) passed anyway because NO test ever executed - * `require()` on the real app module — every "app" test read src/app.js as a - * string or rebuilt a minimal Express app with copied handlers. + * 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: it executes the real production require graph and - * asserts it resolves without throwing. Any future deletion of a required module, - * or any broken relative require, will fail here instead of crashing the container - * on the next deploy. + * 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(__dirname, '..', 'src', 'app'))).not.toThrow(); + expect(() => require(path.join(ROOT, 'src', 'app'))).not.toThrow(); }); it('createApp is exported as a function', () => { - const mod = require(path.join(__dirname, '..', 'src', 'app')); + 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([]); + }); }); diff --git a/dashcaddy-api/server.js b/dashcaddy-api/server.js index f1b3107..f4d6a5f 100644 --- a/dashcaddy-api/server.js +++ b/dashcaddy-api/server.js @@ -70,7 +70,7 @@ process.on('uncaughtException', (error) => { const portLockManager = require('./src/managers/port-lock-manager'); // Create servicesStateManager early — needed by workflow engine init - const StateManager = require('./state-manager'); + const StateManager = require('./src/managers/state-manager'); const servicesStateManager = new StateManager(SERVICES_FILE); // Optional modules