From ff8bc72c81148c0f60c4facd0639e5f24fc5186e Mon Sep 17 00:00:00 2001 From: YBoy <231405196+YB0y@users.noreply.github.com> Date: Mon, 11 May 2026 08:29:14 -0600 Subject: [PATCH] fix: consolidate gateway doctor service notes (#78688) Fixes #80287. Co-authored-by: YB0y --- CHANGELOG.md | 1 + src/commands/doctor-gateway-services.test.ts | 96 ++++++++++++++++++++ src/commands/doctor-gateway-services.ts | 48 ++++++---- 3 files changed, 127 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e18080f2dc..0427bad5abf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,7 @@ Docs: https://docs.openclaw.ai - Channels: cache selected channel registry lookups against the active fallback snapshot so pinned-empty registries refresh native command and alias routing after active registry swaps. (#80333) Thanks @samzong. - Gateway: scope `sessions.resolve` sessionId and label store loads to the requested agent so large unrelated agent stores are not parsed for scoped lookups. Fixes #51264. (#79474) Thanks @samzong. - Gateway: share serialized streaming event envelopes across eligible WebSocket and node subscribers while preserving per-client sequence numbers. (#80299) Thanks @samzong. +- Gateway: consolidate duplicate `openclaw doctor` service config panels while preserving the declined-repair `--force` hint. Fixes #80287. (#78688) Thanks @YB0y. - Browser: report Chrome MCP existing-session page readiness in browser status without letting status probes exceed the client timeout. Fixes #80268. (#80280) Thanks @ai-hpc. - WhatsApp: route opening-phase Baileys 428 connectionClosed through the WhatsApp reconnect policy and keep post-open 428 closes retryable, so transient setup socket closes retry with WhatsApp diagnostics instead of escaping as a bare `channel exited` error. Fixes #75736; mitigates #77443. Thanks @dataCenter430. - Agents: disable Pi's default filesystem resource discovery for embedded runs while keeping OpenClaw inline extension factories active, avoiding Windows event-loop stalls during first WhatsApp-triggered agent startup. Fixes #77443. Thanks @dataCenter430. diff --git a/src/commands/doctor-gateway-services.test.ts b/src/commands/doctor-gateway-services.test.ts index fa0e3131d14..5ac0654a72f 100644 --- a/src/commands/doctor-gateway-services.test.ts +++ b/src/commands/doctor-gateway-services.test.ts @@ -998,6 +998,102 @@ describe("maybeRepairGatewayServiceConfig", () => { } }); }); + + it("does not duplicate Gateway service config panels for a source-checkout entrypoint with audit findings", async () => { + await withEnvAsync({}, async () => { + const root = await fs.mkdtemp( + path.join(os.tmpdir(), "openclaw-doctor-service-config-dedup-"), + ); + try { + await fs.mkdir(path.join(root, ".git"), { recursive: true }); + await fs.mkdir(path.join(root, "src"), { recursive: true }); + await fs.mkdir(path.join(root, "extensions"), { recursive: true }); + await fs.mkdir(path.join(root, "dist"), { recursive: true }); + await fs.writeFile( + path.join(root, "package.json"), + JSON.stringify({ name: "openclaw", version: "0.0.0-test" }), + "utf8", + ); + const sourceCheckoutEntrypoint = path.join(root, "dist", "index.js"); + await fs.writeFile(sourceCheckoutEntrypoint, "export {};\n", "utf8"); + const installEntrypoint = "/usr/local/lib/node_modules/openclaw/dist/index.js"; + setupGatewayEntrypointRepairScenario({ + currentEntrypoint: sourceCheckoutEntrypoint, + installEntrypoint, + installWorkingDirectory: "/tmp", + }); + + await runRepair({ gateway: {} }); + + const gatewayServiceConfigNotes = mocks.note.mock.calls.filter( + ([, title]) => title === "Gateway service config", + ); + expect(gatewayServiceConfigNotes).toHaveLength(1); + const consolidated = gatewayServiceConfigNotes[0]?.[0] ?? ""; + expect(consolidated).toContain( + "Gateway service entrypoint does not match the current install.", + ); + expect(consolidated).not.toContain("resolves to a source checkout"); + const forceMatches = consolidated.match(/openclaw gateway install --force/g) ?? []; + expect(forceMatches).toHaveLength(0); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); + }); + + it("keeps the gateway install force hint when a source-checkout warning is suppressed and repair is declined", async () => { + await withEnvAsync({}, async () => { + const root = await fs.mkdtemp( + path.join(os.tmpdir(), "openclaw-doctor-service-config-force-hint-"), + ); + try { + await fs.mkdir(path.join(root, ".git"), { recursive: true }); + await fs.mkdir(path.join(root, "src"), { recursive: true }); + await fs.mkdir(path.join(root, "extensions"), { recursive: true }); + await fs.mkdir(path.join(root, "dist"), { recursive: true }); + await fs.writeFile( + path.join(root, "package.json"), + JSON.stringify({ name: "openclaw", version: "0.0.0-test" }), + "utf8", + ); + const sourceCheckoutEntrypoint = path.join(root, "dist", "index.js"); + await fs.writeFile(sourceCheckoutEntrypoint, "export {};\n", "utf8"); + const installEntrypoint = "/usr/local/lib/node_modules/openclaw/dist/index.js"; + setupGatewayEntrypointRepairScenario({ + currentEntrypoint: sourceCheckoutEntrypoint, + installEntrypoint, + installWorkingDirectory: "/tmp", + }); + + const declinePrompts = { + ...makeDoctorPrompts(), + confirmAutoFix: vi.fn().mockResolvedValue(false), + confirmAggressiveAutoFix: vi.fn().mockResolvedValue(false), + confirmRuntimeRepair: vi.fn().mockResolvedValue(false), + }; + await maybeRepairGatewayServiceConfig( + { gateway: {} }, + "local", + makeDoctorIo(), + declinePrompts, + ); + + const gatewayServiceConfigNotes = mocks.note.mock.calls.filter( + ([, title]) => title === "Gateway service config", + ); + expect(gatewayServiceConfigNotes).toHaveLength(2); + const auditNote = gatewayServiceConfigNotes[0]?.[0] ?? ""; + expect(auditNote).toContain( + "Gateway service entrypoint does not match the current install.", + ); + expect(auditNote).not.toContain("resolves to a source checkout"); + expect(gatewayServiceConfigNotes[1]?.[0]).toContain("openclaw gateway install --force"); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); + }); }); describe("maybeScanExtraGatewayServices", () => { diff --git a/src/commands/doctor-gateway-services.ts b/src/commands/doctor-gateway-services.ts index b59dcc7439a..ad7bcfff17f 100644 --- a/src/commands/doctor-gateway-services.ts +++ b/src/commands/doctor-gateway-services.ts @@ -381,15 +381,12 @@ export async function maybeRepairGatewayServiceConfig( note(`Gateway service invokes ${OPENCLAW_WRAPPER_ENV_KEY}: ${serviceWrapperPath}`, "Gateway"); } const serviceLayout = await summarizeGatewayServiceLayout(command); - if (serviceLayout?.entrypointSourceCheckout) { - note( - [ + const sourceCheckoutWarning = serviceLayout?.entrypointSourceCheckout + ? [ `Gateway service entrypoint resolves to a source checkout: ${serviceLayout.packageRootReal ?? serviceLayout.packageRoot ?? serviceLayout.entrypointReal ?? serviceLayout.entrypoint}.`, "Run `openclaw doctor --fix` from the intended package install, or reinstall the gateway service with `openclaw gateway install --force`.", - ].join("\n"), - "Gateway service config", - ); - } + ].join("\n") + : null; const tokenRefConfigured = Boolean( resolveSecretInputRef({ @@ -483,21 +480,34 @@ export async function maybeRepairGatewayServiceConfig( issues: audit.issues, }); + const hasEntrypointMismatch = audit.issues.some( + (issue) => issue.code === SERVICE_AUDIT_CODES.gatewayEntrypointMismatch, + ); + const showSourceCheckoutWarning = sourceCheckoutWarning !== null && !hasEntrypointMismatch; + if (audit.issues.length === 0) { + if (sourceCheckoutWarning !== null && !hasEntrypointMismatch) { + note(sourceCheckoutWarning, "Gateway service config"); + } return; } const serviceRepairPolicy = resolveServiceRepairPolicy(); const serviceRepairExternal = isServiceRepairExternallyManaged(serviceRepairPolicy); - note( - audit.issues - .map((issue) => - issue.detail ? `- ${issue.message} (${issue.detail})` : `- ${issue.message}`, - ) - .join("\n"), - "Gateway service config", + const consolidatedLines: string[] = []; + let emittedSourceCheckoutWarning = false; + if (sourceCheckoutWarning !== null && showSourceCheckoutWarning) { + consolidatedLines.push(sourceCheckoutWarning); + consolidatedLines.push(""); + emittedSourceCheckoutWarning = true; + } + consolidatedLines.push( + ...audit.issues.map((issue) => + issue.detail ? `- ${issue.message} (${issue.detail})` : `- ${issue.message}`, + ), ); + note(consolidatedLines.join("\n"), "Gateway service config"); const aggressiveIssues = audit.issues.filter((issue) => issue.level === "aggressive"); const needsAggressive = aggressiveIssues.length > 0; @@ -555,10 +565,12 @@ export async function maybeRepairGatewayServiceConfig( requiresInteractiveConfirmation: true, }); if (!repair) { - note( - "Run `openclaw gateway install --force` when you want to replace the gateway service definition.", - "Gateway service config", - ); + if (!emittedSourceCheckoutWarning) { + note( + "Run `openclaw gateway install --force` when you want to replace the gateway service definition.", + "Gateway service config", + ); + } return; } const serviceEmbeddedToken = readEmbeddedGatewayToken(command);