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("