mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 02:45:38 -06:00
refactor(gateway): use typed doctor close diagnostics (#125629)
This commit is contained in:
committed by
GitHub
parent
0d74bc21b0
commit
7f4bc83ce7
@@ -62,6 +62,8 @@ export function resolveQaGatewayStartupRetry(params: {
|
||||
}
|
||||
|
||||
function isRetryableGatewayCallError(details: string): boolean {
|
||||
// The persistent QA client exposes preformatted close errors plus child logs,
|
||||
// not the typed transport errors produced by one-shot gateway calls.
|
||||
return (
|
||||
details.includes("handshake timeout") ||
|
||||
details.includes("gateway closed (1000") ||
|
||||
@@ -256,6 +258,8 @@ export async function waitForGatewayListening(params: {
|
||||
}
|
||||
|
||||
export function isRetryableRpcStartupError(error: unknown) {
|
||||
// Startup errors cross the same low-level client/log boundary; timeout and
|
||||
// token-mismatch retry facts exist only in the formatted diagnostic.
|
||||
const details = formatErrorMessage(error);
|
||||
return (
|
||||
details.includes("gateway timeout after") ||
|
||||
|
||||
@@ -119,6 +119,8 @@ async function waitForConfigRestartSettle(
|
||||
}
|
||||
|
||||
function formatGatewayPrimaryErrorText(error: unknown) {
|
||||
// The persistent QA client flattens low-level closes and appends child logs,
|
||||
// so the public one-shot gateway guards cannot recover a typed close here.
|
||||
const text = formatErrorMessage(error);
|
||||
const gatewayLogsIndex = text.indexOf("\nGateway logs:");
|
||||
return (gatewayLogsIndex >= 0 ? text.slice(0, gatewayLogsIndex) : text).trim();
|
||||
|
||||
@@ -22,6 +22,7 @@ const service = vi.hoisted(() => ({
|
||||
const note = vi.hoisted(() => vi.fn());
|
||||
const sleep = vi.hoisted(() => vi.fn(async () => {}));
|
||||
const healthCommand = vi.hoisted(() => vi.fn(async () => {}));
|
||||
const formatGatewayClosedDiagnostic = vi.hoisted(() => vi.fn((): string | undefined => undefined));
|
||||
const inspectPortConnections = vi.hoisted(() => vi.fn());
|
||||
const inspectPortUsage = vi.hoisted(() => vi.fn());
|
||||
const formatPortDiagnostics = vi.hoisted(() => vi.fn(() => ["Port 18789 is already in use."]));
|
||||
@@ -154,7 +155,7 @@ vi.mock("./gateway-install-token.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./health-format.js", () => ({
|
||||
formatGatewayClosedDiagnostic: vi.fn(() => undefined),
|
||||
formatGatewayClosedDiagnostic,
|
||||
formatHealthCheckFailure: vi.fn(() => "health failed"),
|
||||
}));
|
||||
|
||||
@@ -173,6 +174,8 @@ describe("maybeRepairGatewayDaemon", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
formatGatewayClosedDiagnostic.mockReset();
|
||||
formatGatewayClosedDiagnostic.mockReturnValue(undefined);
|
||||
service.isLoaded.mockResolvedValue(true);
|
||||
service.readRuntime.mockResolvedValue({ status: "running" });
|
||||
service.readCommand.mockResolvedValue(null);
|
||||
@@ -624,6 +627,25 @@ describe("maybeRepairGatewayDaemon", () => {
|
||||
expect(service.restart).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("reports a typed close after restart without depending on error wording", async () => {
|
||||
setPlatform("linux");
|
||||
const error = new Error("transport closed after restart");
|
||||
healthCommand.mockRejectedValueOnce(error);
|
||||
formatGatewayClosedDiagnostic.mockReturnValueOnce(
|
||||
"Gateway connect failed: transport closed after restart",
|
||||
);
|
||||
|
||||
const runtime = await runAutoRepair();
|
||||
|
||||
expect(formatGatewayClosedDiagnostic).toHaveBeenCalledWith(error);
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
"Gateway connect failed: transport closed after restart",
|
||||
"Gateway",
|
||||
);
|
||||
expect(note).toHaveBeenCalledWith("details", "Gateway connection");
|
||||
expect(runtime.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("restarts running service when --yes explicitly approves repairs", async () => {
|
||||
setPlatform("linux");
|
||||
|
||||
|
||||
@@ -527,16 +527,10 @@ export async function maybeRepairGatewayDaemon(params: {
|
||||
try {
|
||||
await healthCommand({ json: false, timeoutMs: 10_000 }, params.runtime);
|
||||
} catch (err) {
|
||||
const message = String(err);
|
||||
if (message.includes("gateway closed")) {
|
||||
const closedDiagnostic = formatGatewayClosedDiagnostic(err);
|
||||
if (closedDiagnostic) {
|
||||
note(closedDiagnostic, "Gateway");
|
||||
note(params.gatewayDetailsMessage, "Gateway connection");
|
||||
} else {
|
||||
note("Gateway not running.", "Gateway");
|
||||
note(params.gatewayDetailsMessage, "Gateway connection");
|
||||
}
|
||||
const closedDiagnostic = formatGatewayClosedDiagnostic(err);
|
||||
if (closedDiagnostic) {
|
||||
note(closedDiagnostic, "Gateway");
|
||||
note(params.gatewayDetailsMessage, "Gateway connection");
|
||||
} else {
|
||||
params.runtime.error(formatHealthCheckFailure(err));
|
||||
}
|
||||
|
||||
@@ -311,11 +311,12 @@ describe("checkGatewayHealth", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("reports the typed close reason instead of claiming the gateway is not running", async () => {
|
||||
it("reports a typed close without depending on gateway error wording", async () => {
|
||||
const error = Object.assign(
|
||||
new Error("gateway closed (1008): \u001B]52;c;YXR0YWNr\u0007protocol version mismatch"),
|
||||
new Error("transport closed: \u001B]52;c;YXR0YWNr\u0007protocol version mismatch"),
|
||||
{
|
||||
kind: "closed",
|
||||
code: 1008,
|
||||
},
|
||||
);
|
||||
callGateway.mockRejectedValueOnce(error);
|
||||
@@ -325,7 +326,7 @@ describe("checkGatewayHealth", () => {
|
||||
await checkGatewayHealth({ runtime: runtime as never, cfg, timeoutMs: 3000 });
|
||||
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
"Gateway connect failed: gateway closed (1008): protocol version mismatch",
|
||||
"Gateway connect failed: transport closed: protocol version mismatch",
|
||||
"Gateway",
|
||||
);
|
||||
expect(note).not.toHaveBeenCalledWith("Gateway not running.", "Gateway");
|
||||
|
||||
@@ -198,15 +198,10 @@ export async function checkGatewayHealth(params: {
|
||||
return { healthOk, authenticated: false };
|
||||
}
|
||||
}
|
||||
const message = String(err);
|
||||
if (message.includes("gateway closed")) {
|
||||
const closedDiagnostic = formatGatewayClosedDiagnostic(err);
|
||||
if (closedDiagnostic) {
|
||||
const gatewayDetails = buildGatewayConnectionDetails({ config: params.cfg });
|
||||
const closedDiagnostic = formatGatewayClosedDiagnostic(err);
|
||||
if (closedDiagnostic) {
|
||||
note(closedDiagnostic, "Gateway");
|
||||
} else {
|
||||
note("Gateway not running.", "Gateway");
|
||||
}
|
||||
note(closedDiagnostic, "Gateway");
|
||||
note(gatewayDetails.message, "Gateway connection");
|
||||
} else {
|
||||
params.runtime.error(formatHealthCheckFailure(err));
|
||||
|
||||
@@ -1,6 +1,31 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { HealthSummary } from "../gateway/health/types.js";
|
||||
import { formatHealthChannelLines } from "./health-format.js";
|
||||
import { formatGatewayClosedDiagnostic, formatHealthChannelLines } from "./health-format.js";
|
||||
|
||||
describe("formatGatewayClosedDiagnostic", () => {
|
||||
it("formats a coded gateway transport close", () => {
|
||||
const error = Object.assign(new Error("gateway closed (1006): no close reason"), {
|
||||
name: "GatewayTransportError",
|
||||
kind: "closed",
|
||||
code: 1006,
|
||||
connectionDetails: {},
|
||||
});
|
||||
|
||||
expect(formatGatewayClosedDiagnostic(error)).toBe(
|
||||
"Gateway connect failed: gateway closed (1006): no close reason",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not equate an uncoded connect-time close with a websocket close", () => {
|
||||
const error = Object.assign(new Error("Gateway not reachable at ws://127.0.0.1:18789"), {
|
||||
name: "GatewayTransportError",
|
||||
kind: "closed",
|
||||
connectionDetails: {},
|
||||
});
|
||||
|
||||
expect(formatGatewayClosedDiagnostic(error)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
const createHealthSummary = (
|
||||
params: Pick<HealthSummary, "channels" | "channelOrder" | "channelLabels">,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { isGatewayTransportError } from "../gateway/call.js";
|
||||
import type { ChannelAccountHealthSummary, HealthSummary } from "../gateway/health/types.js";
|
||||
|
||||
export function formatGatewayClosedDiagnostic(err: unknown): string | undefined {
|
||||
if (!isGatewayTransportError(err) || err.kind !== "closed") {
|
||||
if (!isGatewayTransportError(err) || err.kind !== "closed" || err.code === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return `Gateway connect failed: ${sanitizeTerminalText(err.message.split("\n", 1)[0] ?? "")}`;
|
||||
|
||||
Reference in New Issue
Block a user