Files
openclaw/src/cli/gateway-cli/run.supervised-lock.test.ts
2026-04-30 16:30:56 +01:00

164 lines
5.0 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { GatewayLockError } from "../../infra/gateway-lock.js";
import { __testing } from "./run.js";
function createLogger() {
return {
info: vi.fn(),
warn: vi.fn(),
};
}
describe("supervised gateway lock recovery", () => {
it("does not retry gateway lock errors outside a supervisor", async () => {
const err = new GatewayLockError("gateway already running");
const startLoop = vi.fn(async () => {
throw err;
});
await expect(
__testing.runGatewayLoopWithSupervisedLockRecovery({
startLoop,
supervisor: null,
port: 18789,
healthHost: "127.0.0.1",
log: createLogger(),
}),
).rejects.toBe(err);
expect(startLoop).toHaveBeenCalledTimes(1);
});
it("leaves a healthy launchd-supervised gateway in control", async () => {
const startLoop = vi.fn(async () => {
throw new GatewayLockError("gateway already running");
});
const probeHealth = vi.fn(async () => true);
const log = createLogger();
await __testing.runGatewayLoopWithSupervisedLockRecovery({
startLoop,
supervisor: "launchd",
port: 18789,
healthHost: "0.0.0.0",
log,
probeHealth,
});
expect(startLoop).toHaveBeenCalledTimes(1);
expect(probeHealth).toHaveBeenCalledWith({ host: "0.0.0.0", port: 18789 });
expect(log.info).toHaveBeenCalledWith(
"gateway already running under launchd; existing gateway is healthy, leaving it in control",
);
expect(log.warn).not.toHaveBeenCalled();
});
it("uses exit 78 semantics for healthy systemd-supervised lock conflicts", async () => {
const startLoop = vi.fn(async () => {
throw new GatewayLockError("another gateway instance is already listening");
});
const probeHealth = vi.fn(async () => true);
await expect(
__testing.runGatewayLoopWithSupervisedLockRecovery({
startLoop,
supervisor: "systemd",
port: 18789,
healthHost: "127.0.0.1",
log: createLogger(),
probeHealth,
}),
).rejects.toThrow("exiting with code 78 to prevent a systemd Restart=always loop");
expect(startLoop).toHaveBeenCalledTimes(1);
expect(probeHealth).toHaveBeenCalledWith({ host: "127.0.0.1", port: 18789 });
expect(
__testing.resolveGatewayLockErrorExitCode(
new GatewayLockError("gateway already running under systemd; existing gateway is healthy"),
"systemd",
),
).toBe(78);
});
it("bounds supervised retries when the existing gateway stays unhealthy", async () => {
let now = 0;
const startLoop = vi.fn(async () => {
throw new GatewayLockError("gateway already running");
});
const sleep = vi.fn(async (ms: number) => {
now += ms;
});
await expect(
__testing.runGatewayLoopWithSupervisedLockRecovery({
startLoop,
supervisor: "systemd",
port: 18789,
healthHost: "127.0.0.1",
log: createLogger(),
probeHealth: vi.fn(async () => false),
now: () => now,
sleep,
retryMs: 5,
timeoutMs: 12,
}),
).rejects.toThrow(
"gateway already running under systemd; existing gateway did not become healthy after 12ms",
);
expect(startLoop).toHaveBeenCalledTimes(4);
expect(sleep).toHaveBeenNthCalledWith(1, 5);
expect(sleep).toHaveBeenNthCalledWith(2, 5);
expect(sleep).toHaveBeenNthCalledWith(3, 2);
});
it("bounds supervised retries for EADDRINUSE lock errors", async () => {
let now = 0;
const startLoop = vi.fn(async () => {
throw new GatewayLockError(
"another gateway instance is already listening on ws://127.0.0.1:18789",
);
});
const sleep = vi.fn(async (ms: number) => {
now += ms;
});
await expect(
__testing.runGatewayLoopWithSupervisedLockRecovery({
startLoop,
supervisor: "systemd",
port: 18789,
healthHost: "127.0.0.1",
log: createLogger(),
probeHealth: vi.fn(async () => false),
now: () => now,
sleep,
retryMs: 5,
timeoutMs: 12,
}),
).rejects.toThrow(
"gateway already running under systemd; existing gateway did not become healthy after 12ms",
);
expect(startLoop).toHaveBeenCalledTimes(4);
expect(sleep).toHaveBeenNthCalledWith(1, 5);
expect(sleep).toHaveBeenNthCalledWith(2, 5);
expect(sleep).toHaveBeenNthCalledWith(3, 2);
});
it("keeps unmanaged duplicate starts on the existing exit-success path", () => {
expect(
__testing.resolveGatewayLockErrorExitCode(
new GatewayLockError("another gateway instance is already listening"),
null,
),
).toBe(0);
});
it("normalizes wildcard bind hosts for local health probes", () => {
expect(__testing.normalizeGatewayHealthProbeHost("0.0.0.0")).toBe("127.0.0.1");
expect(__testing.normalizeGatewayHealthProbeHost("::")).toBe("127.0.0.1");
expect(__testing.normalizeGatewayHealthProbeHost("127.0.0.1")).toBe("127.0.0.1");
});
});