feat(doctor): catch half-written installs, not just absent ones
Runtime deps checked that node_modules existed, which is true of the broken tree too — it now verifies every declared dependency landed. Dashboard UI quotes the last client-build.log error instead of repeating the build command, and npm and free disk get their own rows: both truncate installs silently.
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* @file Tests the `Runtime deps` and `Dashboard UI` rows of
|
||||
* scripts/plugin-doctor.js — the two checks that have to survive a half-written
|
||||
* install. A `node_modules` directory that exists but is missing packages is
|
||||
* exactly what took a client build down with TS2318 (typescript's lib/*.d.ts
|
||||
* never landed, so every global type was undefined), and when the bundle is
|
||||
* missing the reader needs the build error, not another "run /agenthub-open".
|
||||
* Every case runs against an injected temp CLAUDE_HOME.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const { describe, it, after } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
const TMP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-doctor-runtime-"));
|
||||
process.env.CLAUDE_HOME = TMP_HOME;
|
||||
delete process.env.DASHBOARD_CLIENT_DIST;
|
||||
|
||||
const { diagnose } = require("../../scripts/plugin-doctor");
|
||||
|
||||
const RT = path.join(TMP_HOME, "agenthub", "runtime");
|
||||
|
||||
after(() => {
|
||||
fs.rmSync(TMP_HOME, { recursive: true, force: true });
|
||||
delete process.env.CLAUDE_HOME;
|
||||
});
|
||||
|
||||
/**
|
||||
* Rebuild the runtime dir from scratch, then read back one row.
|
||||
* `installed` are the packages that actually made it into node_modules.
|
||||
*/
|
||||
function rowFor(label, { manifest, installed = [], buildLog, dist = false } = {}) {
|
||||
fs.rmSync(RT, { recursive: true, force: true });
|
||||
fs.mkdirSync(path.join(RT, "node_modules"), { recursive: true });
|
||||
|
||||
if (manifest) fs.writeFileSync(path.join(RT, "package.json"), JSON.stringify(manifest));
|
||||
for (const dep of installed) {
|
||||
fs.mkdirSync(path.join(RT, "node_modules", dep), { recursive: true });
|
||||
fs.writeFileSync(path.join(RT, "node_modules", dep, "package.json"), "{}");
|
||||
}
|
||||
if (buildLog) fs.writeFileSync(path.join(RT, "client-build.log"), buildLog);
|
||||
if (dist) {
|
||||
fs.mkdirSync(path.join(RT, "client-dist"), { recursive: true });
|
||||
fs.writeFileSync(path.join(RT, "client-dist", "index.html"), "<html></html>");
|
||||
}
|
||||
|
||||
const row = diagnose().find((r) => r.label === label);
|
||||
assert.ok(row, `the doctor must always emit a ${label} row`);
|
||||
return row;
|
||||
}
|
||||
|
||||
describe("doctor: Runtime deps", () => {
|
||||
it("fails on a tree that npm reported as installed but that is missing packages", () => {
|
||||
const row = rowFor("Runtime deps", {
|
||||
manifest: { dependencies: { express: "^4", ws: "^8", uuid: "^11" } },
|
||||
installed: ["express"],
|
||||
});
|
||||
|
||||
assert.equal(row.level, "fail", row.detail);
|
||||
assert.match(row.detail, /ws/);
|
||||
assert.match(row.detail, /uuid/);
|
||||
assert.ok(!/express/.test(row.detail), `express is installed and must not be listed`);
|
||||
});
|
||||
|
||||
it("passes when every declared dependency landed", () => {
|
||||
const row = rowFor("Runtime deps", {
|
||||
manifest: { dependencies: { express: "^4", ws: "^8" } },
|
||||
installed: ["express", "ws"],
|
||||
});
|
||||
|
||||
assert.equal(row.level, "ok", row.detail);
|
||||
assert.match(row.detail, /2 packages/);
|
||||
});
|
||||
|
||||
it("does not fail over an absent optional dependency", () => {
|
||||
// better-sqlite3 is optional by design: the server falls back to
|
||||
// node:sqlite. Failing here would send readers reinstalling for nothing.
|
||||
const row = rowFor("Runtime deps", {
|
||||
manifest: { dependencies: { ws: "^8" }, optionalDependencies: { "better-sqlite3": "^12" } },
|
||||
installed: ["ws"],
|
||||
});
|
||||
|
||||
assert.equal(row.level, "ok", row.detail);
|
||||
});
|
||||
|
||||
it("warns rather than fails when there is no manifest to verify against", () => {
|
||||
const row = rowFor("Runtime deps", { installed: ["ws"] });
|
||||
|
||||
assert.equal(row.level, "warn", row.detail);
|
||||
});
|
||||
});
|
||||
|
||||
describe("doctor: Dashboard UI", () => {
|
||||
it("quotes the last build error instead of repeating the build command", () => {
|
||||
const row = rowFor("Dashboard UI", {
|
||||
manifest: { dependencies: {} },
|
||||
buildLog: [
|
||||
"added 321 packages in 7s",
|
||||
"> tsc -b && vite build",
|
||||
"error TS2318: Cannot find global type 'Array'.",
|
||||
"",
|
||||
].join("\n"),
|
||||
});
|
||||
|
||||
assert.equal(row.level, "warn", row.detail);
|
||||
assert.match(row.detail, /TS2318/);
|
||||
assert.match(row.detail, /client-build\.log/);
|
||||
});
|
||||
|
||||
it("says the bundle was never built when no build has run", () => {
|
||||
const row = rowFor("Dashboard UI", { manifest: { dependencies: {} } });
|
||||
|
||||
assert.equal(row.level, "warn", row.detail);
|
||||
assert.match(row.detail, /not built yet/);
|
||||
});
|
||||
|
||||
it("is ok once the bundle exists", () => {
|
||||
const row = rowFor("Dashboard UI", { manifest: { dependencies: {} }, dist: true });
|
||||
|
||||
assert.equal(row.level, "ok", row.detail);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user