mirror of https://github.com/openclaw/openclaw.git
test: add diagnostic and port format helper coverage
This commit is contained in:
parent
1a319b7847
commit
f95c09b6f2
|
|
@ -0,0 +1,121 @@
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
emitDiagnosticEvent,
|
||||||
|
isDiagnosticsEnabled,
|
||||||
|
onDiagnosticEvent,
|
||||||
|
resetDiagnosticEventsForTest,
|
||||||
|
} from "./diagnostic-events.js";
|
||||||
|
|
||||||
|
describe("diagnostic-events", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetDiagnosticEventsForTest();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
resetDiagnosticEventsForTest();
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits monotonic seq and timestamps to subscribers", () => {
|
||||||
|
vi.spyOn(Date, "now").mockReturnValueOnce(111).mockReturnValueOnce(222);
|
||||||
|
const events: Array<{ seq: number; ts: number; type: string }> = [];
|
||||||
|
const stop = onDiagnosticEvent((event) => {
|
||||||
|
events.push({ seq: event.seq, ts: event.ts, type: event.type });
|
||||||
|
});
|
||||||
|
|
||||||
|
emitDiagnosticEvent({
|
||||||
|
type: "model.usage",
|
||||||
|
usage: { total: 1 },
|
||||||
|
});
|
||||||
|
emitDiagnosticEvent({
|
||||||
|
type: "session.state",
|
||||||
|
state: "processing",
|
||||||
|
});
|
||||||
|
stop();
|
||||||
|
|
||||||
|
expect(events).toEqual([
|
||||||
|
{ seq: 1, ts: 111, type: "model.usage" },
|
||||||
|
{ seq: 2, ts: 222, type: "session.state" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("isolates listener failures and logs them", () => {
|
||||||
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
const seen: string[] = [];
|
||||||
|
onDiagnosticEvent(() => {
|
||||||
|
throw new Error("boom");
|
||||||
|
});
|
||||||
|
onDiagnosticEvent((event) => {
|
||||||
|
seen.push(event.type);
|
||||||
|
});
|
||||||
|
|
||||||
|
emitDiagnosticEvent({
|
||||||
|
type: "message.queued",
|
||||||
|
source: "telegram",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(seen).toEqual(["message.queued"]);
|
||||||
|
expect(errorSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("listener error type=message.queued seq=1: Error: boom"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports unsubscribe and full reset", () => {
|
||||||
|
const seen: string[] = [];
|
||||||
|
const stop = onDiagnosticEvent((event) => {
|
||||||
|
seen.push(event.type);
|
||||||
|
});
|
||||||
|
|
||||||
|
emitDiagnosticEvent({
|
||||||
|
type: "webhook.received",
|
||||||
|
channel: "telegram",
|
||||||
|
});
|
||||||
|
stop();
|
||||||
|
emitDiagnosticEvent({
|
||||||
|
type: "webhook.processed",
|
||||||
|
channel: "telegram",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(seen).toEqual(["webhook.received"]);
|
||||||
|
|
||||||
|
resetDiagnosticEventsForTest();
|
||||||
|
emitDiagnosticEvent({
|
||||||
|
type: "webhook.error",
|
||||||
|
channel: "telegram",
|
||||||
|
error: "failed",
|
||||||
|
});
|
||||||
|
expect(seen).toEqual(["webhook.received"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops recursive emissions after the guard threshold", () => {
|
||||||
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
let calls = 0;
|
||||||
|
onDiagnosticEvent(() => {
|
||||||
|
calls += 1;
|
||||||
|
emitDiagnosticEvent({
|
||||||
|
type: "queue.lane.enqueue",
|
||||||
|
lane: "main",
|
||||||
|
queueSize: calls,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
emitDiagnosticEvent({
|
||||||
|
type: "queue.lane.enqueue",
|
||||||
|
lane: "main",
|
||||||
|
queueSize: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(calls).toBe(101);
|
||||||
|
expect(errorSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining(
|
||||||
|
"recursion guard tripped at depth=101, dropping type=queue.lane.enqueue",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires an explicit true diagnostics flag", () => {
|
||||||
|
expect(isDiagnosticsEnabled()).toBe(false);
|
||||||
|
expect(isDiagnosticsEnabled({ diagnostics: { enabled: false } } as never)).toBe(false);
|
||||||
|
expect(isDiagnosticsEnabled({ diagnostics: { enabled: true } } as never)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -2,11 +2,6 @@ import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { withTempDir } from "../test-utils/temp-dir.js";
|
import { withTempDir } from "../test-utils/temp-dir.js";
|
||||||
import {
|
|
||||||
emitDiagnosticEvent,
|
|
||||||
onDiagnosticEvent,
|
|
||||||
resetDiagnosticEventsForTest,
|
|
||||||
} from "./diagnostic-events.js";
|
|
||||||
import { readSessionStoreJson5 } from "./state-migrations.fs.js";
|
import { readSessionStoreJson5 } from "./state-migrations.fs.js";
|
||||||
|
|
||||||
describe("infra store", () => {
|
describe("infra store", () => {
|
||||||
|
|
@ -38,52 +33,4 @@ describe("infra store", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("diagnostic-events", () => {
|
|
||||||
it("emits monotonic seq", async () => {
|
|
||||||
resetDiagnosticEventsForTest();
|
|
||||||
const seqs: number[] = [];
|
|
||||||
const stop = onDiagnosticEvent((evt) => seqs.push(evt.seq));
|
|
||||||
|
|
||||||
emitDiagnosticEvent({
|
|
||||||
type: "model.usage",
|
|
||||||
usage: { total: 1 },
|
|
||||||
});
|
|
||||||
emitDiagnosticEvent({
|
|
||||||
type: "model.usage",
|
|
||||||
usage: { total: 2 },
|
|
||||||
});
|
|
||||||
|
|
||||||
stop();
|
|
||||||
|
|
||||||
expect(seqs).toEqual([1, 2]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("emits message-flow events", async () => {
|
|
||||||
resetDiagnosticEventsForTest();
|
|
||||||
const types: string[] = [];
|
|
||||||
const stop = onDiagnosticEvent((evt) => types.push(evt.type));
|
|
||||||
|
|
||||||
emitDiagnosticEvent({
|
|
||||||
type: "webhook.received",
|
|
||||||
channel: "telegram",
|
|
||||||
updateType: "telegram-post",
|
|
||||||
});
|
|
||||||
emitDiagnosticEvent({
|
|
||||||
type: "message.queued",
|
|
||||||
channel: "telegram",
|
|
||||||
source: "telegram",
|
|
||||||
queueDepth: 1,
|
|
||||||
});
|
|
||||||
emitDiagnosticEvent({
|
|
||||||
type: "session.state",
|
|
||||||
state: "processing",
|
|
||||||
reason: "run_started",
|
|
||||||
});
|
|
||||||
|
|
||||||
stop();
|
|
||||||
|
|
||||||
expect(types).toEqual(["webhook.received", "message.queued", "session.state"]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
buildPortHints,
|
||||||
|
classifyPortListener,
|
||||||
|
formatPortDiagnostics,
|
||||||
|
formatPortListener,
|
||||||
|
} from "./ports-format.js";
|
||||||
|
|
||||||
|
describe("ports-format", () => {
|
||||||
|
it("classifies listeners across gateway, ssh, and unknown command lines", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
listener: { commandLine: "ssh -N -L 18789:127.0.0.1:18789 user@host" },
|
||||||
|
expected: "ssh",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
listener: { command: "ssh" },
|
||||||
|
expected: "ssh",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
listener: { commandLine: "node /Users/me/Projects/openclaw/dist/entry.js gateway" },
|
||||||
|
expected: "gateway",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
listener: { commandLine: "python -m http.server 18789" },
|
||||||
|
expected: "unknown",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const testCase of cases) {
|
||||||
|
expect(
|
||||||
|
classifyPortListener(testCase.listener, 18789),
|
||||||
|
JSON.stringify(testCase.listener),
|
||||||
|
).toBe(testCase.expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds ordered hints for mixed listener kinds and multiplicity", () => {
|
||||||
|
expect(
|
||||||
|
buildPortHints(
|
||||||
|
[
|
||||||
|
{ commandLine: "node dist/index.js openclaw gateway" },
|
||||||
|
{ commandLine: "ssh -N -L 18789:127.0.0.1:18789" },
|
||||||
|
{ commandLine: "python -m http.server 18789" },
|
||||||
|
],
|
||||||
|
18789,
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
expect.stringContaining("Gateway already running locally."),
|
||||||
|
"SSH tunnel already bound to this port. Close the tunnel or use a different local port in -L.",
|
||||||
|
"Another process is listening on this port.",
|
||||||
|
expect.stringContaining("Multiple listeners detected"),
|
||||||
|
]);
|
||||||
|
expect(buildPortHints([], 18789)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats listeners with pid, user, command, and address fallbacks", () => {
|
||||||
|
expect(
|
||||||
|
formatPortListener({ pid: 123, user: "alice", commandLine: "ssh -N", address: "::1" }),
|
||||||
|
).toBe("pid 123 alice: ssh -N (::1)");
|
||||||
|
expect(formatPortListener({ command: "ssh", address: "127.0.0.1:18789" })).toBe(
|
||||||
|
"pid ?: ssh (127.0.0.1:18789)",
|
||||||
|
);
|
||||||
|
expect(formatPortListener({})).toBe("pid ?: unknown");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats free and busy port diagnostics", () => {
|
||||||
|
expect(
|
||||||
|
formatPortDiagnostics({
|
||||||
|
port: 18789,
|
||||||
|
status: "free",
|
||||||
|
listeners: [],
|
||||||
|
hints: [],
|
||||||
|
}),
|
||||||
|
).toEqual(["Port 18789 is free."]);
|
||||||
|
|
||||||
|
const lines = formatPortDiagnostics({
|
||||||
|
port: 18789,
|
||||||
|
status: "busy",
|
||||||
|
listeners: [{ pid: 123, user: "alice", 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),
|
||||||
|
});
|
||||||
|
expect(lines[0]).toContain("Port 18789 is already in use");
|
||||||
|
expect(lines).toContain("- pid 123 alice: ssh -N -L 18789:127.0.0.1:18789");
|
||||||
|
expect(lines.some((line) => line.includes("SSH tunnel"))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -7,16 +7,8 @@ const runCommandWithTimeoutMock = vi.hoisted(() => vi.fn());
|
||||||
vi.mock("../process/exec.js", () => ({
|
vi.mock("../process/exec.js", () => ({
|
||||||
runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args),
|
runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args),
|
||||||
}));
|
}));
|
||||||
import { formatPortListener } from "./ports-format.js";
|
|
||||||
import { inspectPortUsage } from "./ports-inspect.js";
|
import { inspectPortUsage } from "./ports-inspect.js";
|
||||||
import {
|
import { ensurePortAvailable, handlePortError, PortInUseError } from "./ports.js";
|
||||||
buildPortHints,
|
|
||||||
classifyPortListener,
|
|
||||||
ensurePortAvailable,
|
|
||||||
formatPortDiagnostics,
|
|
||||||
handlePortError,
|
|
||||||
PortInUseError,
|
|
||||||
} from "./ports.js";
|
|
||||||
|
|
||||||
const describeUnix = process.platform === "win32" ? describe.skip : describe;
|
const describeUnix = process.platform === "win32" ? describe.skip : describe;
|
||||||
|
|
||||||
|
|
@ -62,78 +54,6 @@ describe("ports helpers", () => {
|
||||||
const messages = runtime.error.mock.calls.map((call) => stripAnsi(String(call[0] ?? "")));
|
const messages = runtime.error.mock.calls.map((call) => stripAnsi(String(call[0] ?? "")));
|
||||||
expect(messages.join("\n")).toContain("another OpenClaw instance is already running");
|
expect(messages.join("\n")).toContain("another OpenClaw instance is already running");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("classifies port listeners across gateway, ssh, and unknown cases", () => {
|
|
||||||
const cases = [
|
|
||||||
{
|
|
||||||
listener: { commandLine: "ssh -N -L 18789:127.0.0.1:18789 user@host" },
|
|
||||||
expected: "ssh",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
listener: { command: "ssh" },
|
|
||||||
expected: "ssh",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
listener: { commandLine: "node /Users/me/Projects/openclaw/dist/entry.js gateway" },
|
|
||||||
expected: "gateway",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
listener: { commandLine: "python -m http.server 18789" },
|
|
||||||
expected: "unknown",
|
|
||||||
},
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
for (const testCase of cases) {
|
|
||||||
expect(
|
|
||||||
classifyPortListener(testCase.listener, 18789),
|
|
||||||
JSON.stringify(testCase.listener),
|
|
||||||
).toBe(testCase.expected);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("builds ordered hints for mixed listener kinds and multiple listeners", () => {
|
|
||||||
expect(
|
|
||||||
buildPortHints(
|
|
||||||
[
|
|
||||||
{ commandLine: "node dist/index.js openclaw gateway" },
|
|
||||||
{ commandLine: "ssh -N -L 18789:127.0.0.1:18789" },
|
|
||||||
{ commandLine: "python -m http.server 18789" },
|
|
||||||
],
|
|
||||||
18789,
|
|
||||||
),
|
|
||||||
).toEqual([
|
|
||||||
expect.stringContaining("Gateway already running locally."),
|
|
||||||
"SSH tunnel already bound to this port. Close the tunnel or use a different local port in -L.",
|
|
||||||
"Another process is listening on this port.",
|
|
||||||
expect.stringContaining("Multiple listeners detected"),
|
|
||||||
]);
|
|
||||||
expect(buildPortHints([], 18789)).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("formats port listeners and diagnostics for free and busy ports", () => {
|
|
||||||
expect(formatPortListener({ command: "ssh", address: "127.0.0.1:18789" })).toBe(
|
|
||||||
"pid ?: ssh (127.0.0.1:18789)",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
formatPortDiagnostics({
|
|
||||||
port: 18789,
|
|
||||||
status: "free",
|
|
||||||
listeners: [],
|
|
||||||
hints: [],
|
|
||||||
}),
|
|
||||||
).toEqual(["Port 18789 is free."]);
|
|
||||||
|
|
||||||
const lines = formatPortDiagnostics({
|
|
||||||
port: 18789,
|
|
||||||
status: "busy",
|
|
||||||
listeners: [{ pid: 123, user: "alice", 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),
|
|
||||||
});
|
|
||||||
expect(lines[0]).toContain("Port 18789 is already in use");
|
|
||||||
expect(lines).toContain("- pid 123 alice: ssh -N -L 18789:127.0.0.1:18789");
|
|
||||||
expect(lines.some((line) => line.includes("SSH tunnel"))).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describeUnix("inspectPortUsage", () => {
|
describeUnix("inspectPortUsage", () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue