/** * @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ĩ */ 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"), ""); } 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); }); });