115 lines
4.0 KiB
TypeScript
115 lines
4.0 KiB
TypeScript
import net from "node:net";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { stripAnsi } from "../terminal/ansi.js";
|
|
|
|
const runCommandWithTimeoutMock = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock("../process/exec.js", () => ({
|
|
runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args),
|
|
}));
|
|
import { inspectPortUsage } from "./ports-inspect.js";
|
|
import {
|
|
buildPortHints,
|
|
classifyPortListener,
|
|
ensurePortAvailable,
|
|
formatPortDiagnostics,
|
|
handlePortError,
|
|
PortInUseError,
|
|
} from "./ports.js";
|
|
|
|
const describeUnix = process.platform === "win32" ? describe.skip : describe;
|
|
|
|
describe("ports helpers", () => {
|
|
it("ensurePortAvailable rejects when port busy", async () => {
|
|
const server = net.createServer();
|
|
await new Promise<void>((resolve) => server.listen(0, () => resolve()));
|
|
const port = (server.address() as net.AddressInfo).port;
|
|
await expect(ensurePortAvailable(port)).rejects.toBeInstanceOf(PortInUseError);
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
});
|
|
|
|
it("handlePortError exits nicely on EADDRINUSE", async () => {
|
|
const runtime = {
|
|
error: vi.fn(),
|
|
log: vi.fn(),
|
|
exit: vi.fn() as unknown as (code: number) => never,
|
|
};
|
|
// Avoid slow OS port inspection; this test only cares about messaging + exit behavior.
|
|
await handlePortError(new PortInUseError(1234, "details"), 1234, "context", runtime).catch(
|
|
() => {},
|
|
);
|
|
const messages = runtime.error.mock.calls.map((call) => stripAnsi(String(call[0] ?? "")));
|
|
expect(messages.join("\n")).toContain("context failed: port 1234 is already in use.");
|
|
expect(messages.join("\n")).toContain("Resolve by stopping the process");
|
|
expect(runtime.exit).toHaveBeenCalledWith(1);
|
|
});
|
|
|
|
it("prints an OpenClaw-specific hint when port details look like another OpenClaw instance", async () => {
|
|
const runtime = {
|
|
error: vi.fn(),
|
|
log: vi.fn(),
|
|
exit: vi.fn() as unknown as (code: number) => never,
|
|
};
|
|
|
|
await handlePortError(
|
|
new PortInUseError(18789, "node dist/index.js openclaw gateway"),
|
|
18789,
|
|
"gateway start",
|
|
runtime,
|
|
).catch(() => {});
|
|
|
|
const messages = runtime.error.mock.calls.map((call) => stripAnsi(String(call[0] ?? "")));
|
|
expect(messages.join("\n")).toContain("another OpenClaw instance is already running");
|
|
});
|
|
|
|
it("classifies ssh and gateway listeners", () => {
|
|
expect(
|
|
classifyPortListener({ commandLine: "ssh -N -L 18789:127.0.0.1:18789 user@host" }, 18789),
|
|
).toBe("ssh");
|
|
expect(
|
|
classifyPortListener(
|
|
{
|
|
commandLine: "node /Users/me/Projects/openclaw/dist/entry.js gateway",
|
|
},
|
|
18789,
|
|
),
|
|
).toBe("gateway");
|
|
});
|
|
|
|
it("formats port diagnostics with hints", () => {
|
|
const diagnostics = {
|
|
port: 18789,
|
|
status: "busy" as const,
|
|
listeners: [{ pid: 123, commandLine: "ssh -N -L 18789:127.0.0.1:18789" }],
|
|
hints: buildPortHints([{ pid: 123, commandLine: "ssh -N -L 18789:127.0.0.1:18789" }], 18789),
|
|
};
|
|
const lines = formatPortDiagnostics(diagnostics);
|
|
expect(lines[0]).toContain("Port 18789 is already in use");
|
|
expect(lines.some((line) => line.includes("SSH tunnel"))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describeUnix("inspectPortUsage", () => {
|
|
beforeEach(() => {
|
|
runCommandWithTimeoutMock.mockReset();
|
|
});
|
|
|
|
it("reports busy when lsof is missing but loopback listener exists", async () => {
|
|
const server = net.createServer();
|
|
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
const port = (server.address() as net.AddressInfo).port;
|
|
|
|
runCommandWithTimeoutMock.mockRejectedValueOnce(
|
|
Object.assign(new Error("spawn lsof ENOENT"), { code: "ENOENT" }),
|
|
);
|
|
|
|
try {
|
|
const result = await inspectPortUsage(port);
|
|
expect(result.status).toBe("busy");
|
|
expect(result.errors?.some((err) => err.includes("ENOENT"))).toBe(true);
|
|
} finally {
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
}
|
|
});
|
|
});
|