import { describe, it, expect, afterEach } from "vitest"; import http from "node:http"; import { parseModuleNotFoundError, isModuleNotFoundError, detectInstallLayout, suggestedReinstallCommand, buildRecoveryHtml, startRecoveryServer, } from "../recovery-server.js"; describe("parseModuleNotFoundError", () => { it("extracts a bare-module name from ERR_MODULE_NOT_FOUND", () => { const e = Object.assign(new Error("Cannot find module 'fastify'"), { code: "ERR_MODULE_NOT_FOUND", }); expect(parseModuleNotFoundError(e)).toBe("fastify"); }); it("extracts an absolute path from ERR_MODULE_NOT_FOUND", () => { const e = Object.assign( new Error("Cannot find module '/abs/path/foo.cjs' imported from /bar"), { code: "ERR_MODULE_NOT_FOUND" }, ); expect(parseModuleNotFoundError(e)).toBe("/abs/path/foo.cjs"); }); it("handles 'Cannot find package' phrasing", () => { const e = Object.assign(new Error("Cannot find package 'toad-cache'"), { code: "ERR_MODULE_NOT_FOUND", }); expect(parseModuleNotFoundError(e)).toBe("toad-cache"); }); it("handles legacy MODULE_NOT_FOUND", () => { const e = Object.assign(new Error("Cannot find module 'foo'"), { code: "MODULE_NOT_FOUND", }); expect(parseModuleNotFoundError(e)).toBe("foo"); }); it("returns null for non-module errors", () => { expect(parseModuleNotFoundError(new Error("nope"))).toBeNull(); expect(parseModuleNotFoundError(null)).toBeNull(); expect(parseModuleNotFoundError(undefined)).toBeNull(); }); }); describe("isModuleNotFoundError", () => { it("recognizes ERR_MODULE_NOT_FOUND", () => { const e = Object.assign(new Error("Cannot find module 'x'"), { code: "ERR_MODULE_NOT_FOUND" }); expect(isModuleNotFoundError(e)).toBe(true); }); it("recognizes phrase-only matches (no code)", () => { expect(isModuleNotFoundError(new Error("Cannot find module 'x'"))).toBe(true); expect(isModuleNotFoundError(new Error("Cannot find package 'x'"))).toBe(true); }); it("rejects unrelated errors", () => { expect(isModuleNotFoundError(new Error("EADDRINUSE"))).toBe(false); expect(isModuleNotFoundError(null)).toBe(false); }); }); describe("detectInstallLayout", () => { it("detects npm-global layout", () => { expect( detectInstallLayout("/usr/local/lib/node_modules/@blackbelt-technology/pi-agent-dashboard/packages/server/src/cli.ts"), ).toBe("npm-global"); }); it("detects monorepo layout", () => { expect(detectInstallLayout("/Users/x/repo/packages/server/src/cli.ts")).toBe("monorepo"); }); it("returns unknown for unrecognized paths", () => { expect(detectInstallLayout("/tmp/foo.js")).toBe("unknown"); }); }); describe("suggestedReinstallCommand", () => { it("returns npm -g for npm-global", () => { expect(suggestedReinstallCommand("npm-global")).toMatch(/npm install -g/); }); it("returns repo-root install for monorepo", () => { expect(suggestedReinstallCommand("monorepo")).toMatch(/repo root/); }); }); describe("buildRecoveryHtml", () => { it("includes the missing-module identifier and error stack", () => { const html = buildRecoveryHtml({ port: 8000, error: Object.assign(new Error("Cannot find module 'fastify'"), { stack: "STACK_TRACE_HERE" }), missingModule: "fastify", suggestedFix: "npm install -g foo", }); expect(html).toContain("fastify"); expect(html).toContain("STACK_TRACE_HERE"); expect(html).toContain("npm install -g foo"); expect(html).toContain("Recovery Mode"); }); it("escapes HTML in error messages to prevent XSS", () => { const html = buildRecoveryHtml({ port: 8000, error: new Error(""), missingModule: "", }); expect(html).not.toContain("