From bd3f09e9696dd5432d0cea8fda5ee4300bd03345 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 8 May 2026 06:39:44 +0100 Subject: [PATCH] fix(doctor): avoid duplicate gateway runtime warnings (#79203) --- CHANGELOG.md | 1 + src/commands/doctor-gateway-services.test.ts | 56 ++++++++++++++++++-- src/commands/doctor-gateway-services.ts | 11 ++-- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 698d118e078..53e69400247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -185,6 +185,7 @@ Docs: https://docs.openclaw.ai - Auto-reply/media: resolve `scp` from `PATH` when staging sandbox media so nonstandard OpenSSH installs can copy remote attachments. - Agents/PI: route PI-native OpenAI-compatible default streams through OpenClaw boundary-aware transports so local-compatible model runs keep API-key injection and transport policy. - Gateway/media: require authenticated owner or admin context for managed outgoing image bytes instead of trusting requester-session headers. +- Doctor/gateway: avoid duplicate Node runtime warnings when the daemon install plan already selected a supported Node runtime. - Gateway/watch: leave `OPENCLAW_TRACE_SYNC_IO` disabled by default in `pnpm gateway:watch:raw` so watch mode avoids noisy Node sync-I/O stack traces unless explicitly requested. - Codex app-server: close stdio stdin before force-killing the managed app-server, matching Codex single-client shutdown behavior and avoiding unsettled CLI exits after successful runs. - CLI/Codex: dispose registered agent harnesses during short-lived CLI shutdown so successful Codex-backed `agent --local` runs do not leave app-server child processes alive. diff --git a/src/commands/doctor-gateway-services.test.ts b/src/commands/doctor-gateway-services.test.ts index 1077dec26b8..1f3b71f3e31 100644 --- a/src/commands/doctor-gateway-services.test.ts +++ b/src/commands/doctor-gateway-services.test.ts @@ -38,6 +38,9 @@ const mocks = vi.hoisted(() => ({ resolveIsNixMode: vi.fn(() => false), findExtraGatewayServices: vi.fn().mockResolvedValue([]), renderGatewayServiceCleanupHints: vi.fn().mockReturnValue([]), + needsNodeRuntimeMigration: vi.fn(() => false), + renderSystemNodeWarning: vi.fn().mockReturnValue(undefined), + resolveSystemNodeInfo: vi.fn().mockResolvedValue(null), isSystemdUnitActive: vi.fn().mockResolvedValue(false), uninstallLegacySystemdUnits: vi.fn().mockResolvedValue([]), note: vi.fn(), @@ -62,13 +65,13 @@ vi.mock("../daemon/inspect.js", () => ({ })); vi.mock("../daemon/runtime-paths.js", () => ({ - renderSystemNodeWarning: vi.fn().mockReturnValue(undefined), - resolveSystemNodeInfo: vi.fn().mockResolvedValue(null), + renderSystemNodeWarning: mocks.renderSystemNodeWarning, + resolveSystemNodeInfo: mocks.resolveSystemNodeInfo, })); vi.mock("../daemon/service-audit.js", () => ({ auditGatewayServiceConfig: mocks.auditGatewayServiceConfig, - needsNodeRuntimeMigration: vi.fn(() => false), + needsNodeRuntimeMigration: mocks.needsNodeRuntimeMigration, readEmbeddedGatewayToken: readEmbeddedGatewayTokenForTest, SERVICE_AUDIT_CODES: { gatewayCommandMissing: testServiceAuditCodes.gatewayCommandMissing, @@ -246,6 +249,9 @@ describe("maybeRepairGatewayServiceConfig", () => { vi.clearAllMocks(); fsMocks.realpath.mockImplementation(async (value: string) => value); mocks.resolveGatewayPort.mockReturnValue(18789); + mocks.needsNodeRuntimeMigration.mockReturnValue(false); + mocks.renderSystemNodeWarning.mockReturnValue(undefined); + mocks.resolveSystemNodeInfo.mockResolvedValue(null); mocks.isSystemdUnitActive.mockResolvedValue(false); mocks.resolveGatewayAuthTokenForService.mockImplementation(async (cfg: OpenClawConfig, env) => { const configToken = @@ -303,6 +309,50 @@ describe("maybeRepairGatewayServiceConfig", () => { expect(mocks.install).toHaveBeenCalledTimes(1); }); + it("does not duplicate gateway runtime warnings already emitted by the node install plan", async () => { + const nvmNode = "/home/orin/.nvm/versions/node/v22.22.2/bin/node"; + mocks.readCommand.mockResolvedValue({ + programArguments: [nvmNode, "/usr/local/bin/openclaw", "gateway", "--port", "18789"], + environment: {}, + }); + mocks.buildGatewayInstallPlan.mockImplementation(async ({ warn }) => { + warn?.( + "System Node 20.20.2 at /usr/bin/node is below the required Node 22.16+. Using /home/orin/.nvm/versions/node/v22.22.2/bin/node for the daemon.", + "Gateway runtime", + ); + return { + programArguments: [nvmNode, "/usr/local/bin/openclaw", "gateway", "--port", "18789"], + workingDirectory: "/tmp", + environment: {}, + }; + }); + mocks.auditGatewayServiceConfig.mockResolvedValue({ + ok: true, + issues: [{ code: "runtime", message: "runtime migration", level: "recommended" }], + }); + mocks.needsNodeRuntimeMigration.mockReturnValue(true); + mocks.resolveSystemNodeInfo.mockResolvedValue({ + path: "/usr/bin/node", + version: "20.20.2", + supported: false, + }); + mocks.renderSystemNodeWarning.mockReturnValue("duplicate doctor runtime warning"); + + await runRepair({ gateway: {} }); + + const runtimeNotes = mocks.note.mock.calls.filter(([, title]) => title === "Gateway runtime"); + const runtimeMessages = runtimeNotes.map(([message]) => message); + expect(runtimeMessages).not.toContain("duplicate doctor runtime warning"); + expect(runtimeMessages).not.toEqual( + expect.arrayContaining([expect.stringContaining("not found")]), + ); + expect(runtimeMessages).toEqual( + expect.arrayContaining([ + expect.stringContaining("Using /home/orin/.nvm/versions/node/v22.22.2/bin/node"), + ]), + ); + }); + it("passes planned managed env keys into service audit for legacy inline secret detection", async () => { mocks.readCommand.mockResolvedValue({ programArguments: gatewayProgramArguments, diff --git a/src/commands/doctor-gateway-services.ts b/src/commands/doctor-gateway-services.ts index 23e8e78eefb..822c537d826 100644 --- a/src/commands/doctor-gateway-services.ts +++ b/src/commands/doctor-gateway-services.ts @@ -422,15 +422,16 @@ export async function maybeRepairGatewayServiceConfig( ? await resolveSystemNodeInfo({ env: process.env }) : null; const systemNodePath = systemNodeInfo?.supported ? systemNodeInfo.path : null; - if (needsNodeRuntime && !systemNodePath) { + if (needsNodeRuntime && !systemNodePath && runtimeChoice !== "node") { const warning = renderSystemNodeWarning(systemNodeInfo); if (warning) { note(warning, "Gateway runtime"); + } else { + note( + "System Node 22 LTS (22.16+) or Node 24 not found. Install via Homebrew/apt/choco and rerun doctor to migrate off Bun/version managers.", + "Gateway runtime", + ); } - note( - "System Node 22 LTS (22.16+) or Node 24 not found. Install via Homebrew/apt/choco and rerun doctor to migrate off Bun/version managers.", - "Gateway runtime", - ); } const expectedRuntimePlan =