Files
AgentHub/server/__tests__/runtime-deps.test.js
T
nntrivi2001 aa8ac6cad9 fix: v2.8.10 — the server starts again on plugin installs (js-yaml is a runtime dependency)
v2.8.9 crashed at start on every plugin install with "Cannot find module
'js-yaml'": server/lib/lane-detect.js requires it, but it was only a
devDependency that redoc used to pull in, and v2.8.8 dropped redoc. The
plugin installs with --omit=dev. This box hid it: Debian's global
/usr/share/nodejs has js-yaml. Reproduced with a pruned install and the
global paths off; fixed by moving js-yaml (and argparse in the lock) to
dependencies. New runtime-deps.test.js fails on any server/ require that is
not a dependency.
2026-10-16 14:54:43 +07:00

69 lines
2.6 KiB
JavaScript

/**
* @file runtime-deps.test.js
* @description Every package the server code requires is a runtime dependency.
* A plugin install runs `npm install --omit=dev`, so a package listed only in
* devDependencies — or one that arrived only as some other package's
* dependency — is missing there. v2.8.9 crashed at start on every plugin
* install: `js-yaml` (server/lib/lane-detect.js) was a devDependency that
* `redoc` used to pull in, and dropping `redoc` took it away. Machines with
* Debian's global /usr/share/nodejs never saw it, so this checks the manifest
* instead of trusting a local `require` to fail.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it } = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const { builtinModules } = require("node:module");
const ROOT = path.join(__dirname, "..", "..");
const pkg = require(path.join(ROOT, "package.json"));
// Required only behind a runtime check, never on a plain Node server.
const GUARDED = new Set(["electron"]);
function sourceFiles(dir) {
const out = [];
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, e.name);
if (e.isDirectory()) {
if (!["node_modules", "__tests__", "vendor"].includes(e.name)) out.push(...sourceFiles(p));
} else if (/\.(c?js|mjs)$/.test(e.name)) out.push(p);
}
return out;
}
/** Bare package names `file` requires, comments left out. */
function requiredPackages(file) {
const code = fs
.readFileSync(file, "utf8")
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/(^|[^:"'`\\])\/\/.*$/gm, "$1");
const names = new Set();
for (const m of code.matchAll(/\brequire\(\s*["']([^"'./][^"']*)["']\s*\)/g)) {
const spec = m[1];
if (spec.startsWith("node:")) continue;
const name = spec.startsWith("@") ? spec.split("/").slice(0, 2).join("/") : spec.split("/")[0];
if (!builtinModules.includes(name)) names.add(name);
}
return names;
}
describe("runtime dependencies", () => {
it("every package server/ requires is in dependencies or optionalDependencies", () => {
const runtime = new Set([
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.optionalDependencies || {}),
]);
const missing = [];
for (const file of sourceFiles(path.join(ROOT, "server"))) {
for (const name of requiredPackages(file)) {
if (!runtime.has(name) && !GUARDED.has(name))
missing.push(`${path.relative(ROOT, file)}: ${name}`);
}
}
assert.deepEqual(missing, []);
});
});