fix: verify gateway restart health after daemon restart
This commit is contained in:
131
src/cli/daemon-cli/lifecycle.test.ts
Normal file
131
src/cli/daemon-cli/lifecycle.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
type RestartHealthSnapshot = {
|
||||
healthy: boolean;
|
||||
staleGatewayPids: number[];
|
||||
runtime: { status?: string };
|
||||
portUsage: { port: number; status: string; listeners: []; hints: []; errors?: string[] };
|
||||
};
|
||||
|
||||
type RestartPostCheckContext = {
|
||||
json: boolean;
|
||||
stdout: NodeJS.WritableStream;
|
||||
warnings: string[];
|
||||
fail: (message: string, hints?: string[]) => void;
|
||||
};
|
||||
|
||||
type RestartParams = {
|
||||
opts?: { json?: boolean };
|
||||
postRestartCheck?: (ctx: RestartPostCheckContext) => Promise<void>;
|
||||
};
|
||||
|
||||
const service = {
|
||||
readCommand: vi.fn(),
|
||||
restart: vi.fn(),
|
||||
};
|
||||
|
||||
const runServiceRestart = vi.fn();
|
||||
const waitForGatewayHealthyRestart = vi.fn();
|
||||
const terminateStaleGatewayPids = vi.fn();
|
||||
const renderRestartDiagnostics = vi.fn(() => ["diag: unhealthy runtime"]);
|
||||
const resolveGatewayPort = vi.fn(() => 18789);
|
||||
const loadConfig = vi.fn(() => ({}));
|
||||
|
||||
vi.mock("../../config/config.js", () => ({
|
||||
loadConfig: () => loadConfig(),
|
||||
resolveGatewayPort,
|
||||
}));
|
||||
|
||||
vi.mock("../../daemon/service.js", () => ({
|
||||
resolveGatewayService: () => service,
|
||||
}));
|
||||
|
||||
vi.mock("./restart-health.js", () => ({
|
||||
waitForGatewayHealthyRestart,
|
||||
terminateStaleGatewayPids,
|
||||
renderRestartDiagnostics,
|
||||
}));
|
||||
|
||||
vi.mock("./lifecycle-core.js", () => ({
|
||||
runServiceRestart,
|
||||
runServiceStart: vi.fn(),
|
||||
runServiceStop: vi.fn(),
|
||||
runServiceUninstall: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("runDaemonRestart health checks", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
service.readCommand.mockReset();
|
||||
service.restart.mockReset();
|
||||
runServiceRestart.mockReset();
|
||||
waitForGatewayHealthyRestart.mockReset();
|
||||
terminateStaleGatewayPids.mockReset();
|
||||
renderRestartDiagnostics.mockClear();
|
||||
resolveGatewayPort.mockClear();
|
||||
loadConfig.mockClear();
|
||||
|
||||
service.readCommand.mockResolvedValue({
|
||||
programArguments: ["openclaw", "gateway", "--port", "18789"],
|
||||
environment: {},
|
||||
});
|
||||
|
||||
runServiceRestart.mockImplementation(async (params: RestartParams) => {
|
||||
const fail = (message: string, hints?: string[]) => {
|
||||
const err = new Error(message) as Error & { hints?: string[] };
|
||||
err.hints = hints;
|
||||
throw err;
|
||||
};
|
||||
await params.postRestartCheck?.({
|
||||
json: Boolean(params.opts?.json),
|
||||
stdout: process.stdout,
|
||||
warnings: [],
|
||||
fail,
|
||||
});
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
it("kills stale gateway pids and retries restart", async () => {
|
||||
const unhealthy: RestartHealthSnapshot = {
|
||||
healthy: false,
|
||||
staleGatewayPids: [1993],
|
||||
runtime: { status: "stopped" },
|
||||
portUsage: { port: 18789, status: "busy", listeners: [], hints: [] },
|
||||
};
|
||||
const healthy: RestartHealthSnapshot = {
|
||||
healthy: true,
|
||||
staleGatewayPids: [],
|
||||
runtime: { status: "running" },
|
||||
portUsage: { port: 18789, status: "busy", listeners: [], hints: [] },
|
||||
};
|
||||
waitForGatewayHealthyRestart.mockResolvedValueOnce(unhealthy).mockResolvedValueOnce(healthy);
|
||||
terminateStaleGatewayPids.mockResolvedValue([1993]);
|
||||
|
||||
const { runDaemonRestart } = await import("./lifecycle.js");
|
||||
const result = await runDaemonRestart({ json: true });
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(terminateStaleGatewayPids).toHaveBeenCalledWith([1993]);
|
||||
expect(service.restart).toHaveBeenCalledTimes(1);
|
||||
expect(waitForGatewayHealthyRestart).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("fails restart when gateway remains unhealthy", async () => {
|
||||
const unhealthy: RestartHealthSnapshot = {
|
||||
healthy: false,
|
||||
staleGatewayPids: [],
|
||||
runtime: { status: "stopped" },
|
||||
portUsage: { port: 18789, status: "free", listeners: [], hints: [] },
|
||||
};
|
||||
waitForGatewayHealthyRestart.mockResolvedValue(unhealthy);
|
||||
|
||||
const { runDaemonRestart } = await import("./lifecycle.js");
|
||||
|
||||
await expect(runDaemonRestart({ json: true })).rejects.toMatchObject({
|
||||
message: "Gateway restart failed health checks.",
|
||||
});
|
||||
expect(terminateStaleGatewayPids).not.toHaveBeenCalled();
|
||||
expect(renderRestartDiagnostics).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user