Skip to content

Commit 08ce17c

Browse files
fix(gateway): surface unreachable status diagnostics (#74691)
Summary: - The PR adds a `no_gateway_reachable` gateway probe warning, passes discovery count into warning construction, adds focused coverage, and updates the changelog. - Reproducibility: yes. for the missing diagnostic: current main can reach an all-unreachable, zero-discovery ... -count input. No for the underlying #49012 freeze itself; that remains a separate root-cause investigation. ClawSweeper fixups: - Included follow-up commit: fix(gateway): surface unreachable status diagnostics Validation: - ClawSweeper review passed for head 50fb29c. - Required merge gates passed before the squash merge. Prepared head SHA: 50fb29c Review: #74691 (comment) Co-authored-by: Vincent Koc <vincentkoc@ieee.org> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
1 parent b9eb31b commit 08ce17c

5 files changed

Lines changed: 90 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
1616
- Status: show the `openai-codex` OAuth profile for `openai/gpt-*` sessions running through the native Codex runtime instead of reporting auth as unknown. (#76197) Thanks @mbelinky.
1717
- Plugins/externalization: keep diagnostics ClawHub packages and persisted bundled-plugin relocation on npm-first install metadata for launch, and omit Discord from the core package now that its external package is published. Thanks @vincentkoc.
1818
- Plugins/Codex: allow the official npm Codex plugin to install without the unsafe-install override, keep `/codex` command ownership, and cover the real npm Docker live path through managed `.openclaw/npm` dependencies plus uninstall failure proof.
19+
- Gateway/status: add concrete service, config, listener-owner, and log collection next steps when gateway probes fail and Bonjour finds no local gateway, so frozen or port-conflict reports include the data needed for root-cause triage. Refs #49012. Thanks @vincentkoc.
1920

2021
## 2026.5.2
2122

src/commands/gateway-status.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,46 @@ describe("gateway-status command", () => {
312312
expect(targets[0]?.summary).toBeTruthy();
313313
});
314314

315+
it("includes diagnostic next steps when no gateway is reachable or discoverable", async () => {
316+
const { runtime, runtimeLogs, runtimeErrors } = createRuntimeCapture();
317+
const defaultProbeGateway = probeGateway.getMockImplementation();
318+
try {
319+
probeGateway.mockImplementation(async (opts: { url: string }) => ({
320+
ok: false,
321+
url: opts.url,
322+
connectLatencyMs: null,
323+
error: "connection refused",
324+
close: null,
325+
auth: {
326+
role: null,
327+
scopes: [],
328+
capability: "unknown",
329+
},
330+
health: null,
331+
status: null,
332+
presence: null,
333+
configSnapshot: null,
334+
}));
335+
336+
await expect(runGatewayStatus(runtime, { timeout: "1000", json: true })).rejects.toThrow(
337+
"__exit__:1",
338+
);
339+
} finally {
340+
probeGateway.mockReset();
341+
if (defaultProbeGateway) {
342+
probeGateway.mockImplementation(defaultProbeGateway);
343+
}
344+
}
345+
346+
expect(runtimeErrors).toHaveLength(0);
347+
const parsed = JSON.parse(runtimeLogs.join("\n")) as {
348+
warnings?: Array<{ code?: string; message?: string }>;
349+
};
350+
const warning = parsed.warnings?.find((entry) => entry.code === "no_gateway_reachable");
351+
expect(warning?.message).toContain("openclaw gateway status --deep --require-rpc");
352+
expect(warning?.message).toContain("ss -ltnp");
353+
});
354+
315355
it("omits discovery wsUrl when only TXT hints are present", async () => {
316356
const { runtime, runtimeLogs, runtimeErrors } = createRuntimeCapture();
317357
discoverGatewayBeacons.mockResolvedValueOnce([

src/commands/gateway-status.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export async function gatewayStatusCommand(
121121
sshTarget: probePass.sshTarget,
122122
sshTunnelStarted: probePass.sshTunnelStarted,
123123
sshTunnelError: probePass.sshTunnelError,
124+
discoveryCount: probePass.discovery.length,
124125
localTlsLoadError:
125126
localTlsRuntime && !localTlsRuntime.enabled && localTlsRuntime.required
126127
? (localTlsRuntime.error ?? "gateway tls is enabled but local TLS runtime could not load")

src/commands/gateway-status/output.test.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import type { GatewayProbeResult } from "../../gateway/probe.js";
33
import type { RuntimeEnv } from "../../runtime.js";
44
import type { GatewayStatusProbedTarget } from "./probe-run.js";
55

6-
const writeRuntimeJson = vi.fn();
6+
const mocks = vi.hoisted(() => ({
7+
writeRuntimeJson: vi.fn(),
8+
}));
79

810
vi.mock("../../runtime.js", () => ({
9-
writeRuntimeJson: (...args: unknown[]) => writeRuntimeJson(...args),
11+
writeRuntimeJson: (...args: unknown[]) => mocks.writeRuntimeJson(...args),
1012
}));
1113

1214
vi.mock("../../terminal/theme.js", async () => {
@@ -18,7 +20,8 @@ vi.mock("../../terminal/theme.js", async () => {
1820
};
1921
});
2022

21-
const { writeGatewayStatusJson, writeGatewayStatusText } = await import("./output.js");
23+
const { buildGatewayStatusWarnings, writeGatewayStatusJson, writeGatewayStatusText } =
24+
await import("./output.js");
2225

2326
function createRuntimeCapture(): RuntimeEnv {
2427
return {
@@ -77,7 +80,35 @@ function createTarget(id: string, probe: GatewayProbeResult): GatewayStatusProbe
7780

7881
describe("gateway status output", () => {
7982
beforeEach(() => {
80-
writeRuntimeJson.mockReset();
83+
mocks.writeRuntimeJson.mockReset();
84+
});
85+
86+
it("warns with diagnostic next steps when no probes or Bonjour discovery find a gateway", () => {
87+
const warnings = buildGatewayStatusWarnings({
88+
probed: [
89+
createTarget(
90+
"localLoopback",
91+
createProbe("unknown", {
92+
ok: false,
93+
connectLatencyMs: null,
94+
error: "connection refused",
95+
}),
96+
),
97+
],
98+
sshTarget: null,
99+
sshTunnelStarted: false,
100+
sshTunnelError: null,
101+
discoveryCount: 0,
102+
});
103+
104+
expect(warnings).toContainEqual(
105+
expect.objectContaining({
106+
code: "no_gateway_reachable",
107+
message: expect.stringContaining("openclaw gateway status --deep --require-rpc"),
108+
targetIds: ["localLoopback"],
109+
}),
110+
);
111+
expect(warnings.at(0)?.message).toContain("lsof -nP -iTCP:<port>");
81112
});
82113

83114
it("derives summary capability from reachable probes only in json output", () => {
@@ -114,7 +145,7 @@ describe("gateway status output", () => {
114145
primaryTargetId: "reachable-read",
115146
});
116147

117-
expect(writeRuntimeJson).toHaveBeenCalledWith(
148+
expect(mocks.writeRuntimeJson).toHaveBeenCalledWith(
118149
runtime,
119150
expect.objectContaining({
120151
ok: true,
@@ -188,7 +219,7 @@ describe("gateway status output", () => {
188219
primaryTargetId: "detail-timeout",
189220
});
190221

191-
expect(writeRuntimeJson).toHaveBeenCalledWith(
222+
expect(mocks.writeRuntimeJson).toHaveBeenCalledWith(
192223
runtime,
193224
expect.objectContaining({
194225
ok: true,

src/commands/gateway-status/output.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ export type GatewayStatusWarning = {
1818
targetIds?: string[];
1919
};
2020

21+
const noReachableGatewayDiagnostic =
22+
"No gateway answered any probe and Bonjour discovery returned no local gateways. Run `openclaw gateway status --deep --require-rpc` to inspect service state, config paths, listener owners, and logs; include `ss -ltnp` or `lsof -nP -iTCP:<port> -sTCP:LISTEN` for the configured port when filing a report.";
23+
2124
export function pickPrimaryProbedTarget(probed: GatewayStatusProbedTarget[]) {
2225
const reachable = probed.filter((entry) => isProbeReachable(entry.probe));
2326
return (
@@ -35,6 +38,7 @@ export function buildGatewayStatusWarnings(params: {
3538
sshTunnelStarted: boolean;
3639
sshTunnelError: string | null;
3740
localTlsLoadError?: string | null;
41+
discoveryCount?: number;
3842
}): GatewayStatusWarning[] {
3943
const reachable = params.probed.filter((entry) => isProbeReachable(entry.probe));
4044
const degradedScopeLimited = params.probed.filter((entry) =>
@@ -59,6 +63,13 @@ export function buildGatewayStatusWarnings(params: {
5963
targetIds: ["localLoopback"],
6064
});
6165
}
66+
if (reachable.length === 0 && params.discoveryCount === 0) {
67+
warnings.push({
68+
code: "no_gateway_reachable",
69+
message: noReachableGatewayDiagnostic,
70+
targetIds: params.probed.map((entry) => entry.target.id),
71+
});
72+
}
6273
if (reachable.length > 1) {
6374
warnings.push({
6475
code: "multiple_gateways",

0 commit comments

Comments
 (0)