From 59fa034fcbcbe1a44b16e864c667077f5b1c35db Mon Sep 17 00:00:00 2001 From: Alix-007 Date: Fri, 17 Jul 2026 00:08:55 +0800 Subject: [PATCH] fix(doctor): bound launchctl environment probes (#109115) --- ...form-notes.launchctl-env-overrides.test.ts | 43 ++++++++++++++++++- src/commands/doctor-platform-notes.ts | 7 ++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/commands/doctor-platform-notes.launchctl-env-overrides.test.ts b/src/commands/doctor-platform-notes.launchctl-env-overrides.test.ts index 63dfa617d7da..8304dad4d567 100644 --- a/src/commands/doctor-platform-notes.launchctl-env-overrides.test.ts +++ b/src/commands/doctor-platform-notes.launchctl-env-overrides.test.ts @@ -1,7 +1,16 @@ // Doctor launchctl environment tests cover macOS gateway platform warnings for env overrides. import fs from "node:fs"; -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; + +const processMocks = vi.hoisted(() => ({ + runExec: vi.fn(), +})); + +vi.mock("../process/exec.js", () => ({ + runExec: processMocks.runExec, +})); + import { collectMacGatewayPlatformWarnings, noteMacLaunchctlGatewayEnvOverrides, @@ -17,6 +26,10 @@ function requireNoteCall(noteFn: { mock: { calls: unknown[][] } }, index = 0): u } describe("noteMacLaunchctlGatewayEnvOverrides", () => { + beforeEach(() => { + processMocks.runExec.mockReset().mockResolvedValue({ stdout: "", stderr: "" }); + }); + it("collects clear unsetenv instructions for token override", async () => { const noteFn = vi.fn(); const getenv = vi.fn(async (name: string) => @@ -118,6 +131,34 @@ describe("noteMacLaunchctlGatewayEnvOverrides", () => { expect(getenv).not.toHaveBeenCalled(); expect(noteFn).not.toHaveBeenCalled(); }); + + it("bounds launchctl getenv calls and ignores timeout failures", async () => { + const noteFn = vi.fn(); + processMocks.runExec.mockRejectedValue(new Error("timed out")); + const cfg = { + gateway: { + auth: { + token: "config-token", + }, + }, + } as OpenClawConfig; + + await noteMacLaunchctlGatewayEnvOverrides(cfg, { platform: "darwin", noteFn }); + + expect(processMocks.runExec).toHaveBeenNthCalledWith( + 1, + "/bin/launchctl", + ["getenv", "OPENCLAW_GATEWAY_TOKEN"], + { logOutput: false, timeoutMs: 5_000 }, + ); + expect(processMocks.runExec).toHaveBeenNthCalledWith( + 2, + "/bin/launchctl", + ["getenv", "OPENCLAW_GATEWAY_PASSWORD"], + { logOutput: false, timeoutMs: 5_000 }, + ); + expect(noteFn).not.toHaveBeenCalled(); + }); }); describe("noteMacStaleOpenClawUpdateLaunchdJobs", () => { diff --git a/src/commands/doctor-platform-notes.ts b/src/commands/doctor-platform-notes.ts index 7d7a995e5d2b..252243dc1b17 100644 --- a/src/commands/doctor-platform-notes.ts +++ b/src/commands/doctor-platform-notes.ts @@ -12,6 +12,8 @@ import { resolveGatewayService, type GatewayService } from "../daemon/service.js import { runExec } from "../process/exec.js"; import { shortenHomePath } from "../utils.js"; +const DOCTOR_LAUNCHCTL_TIMEOUT_MS = 5_000; + function resolveHomeDir(): string { return process.env.HOME ?? os.homedir(); } @@ -104,7 +106,10 @@ export async function noteMacStaleOpenClawUpdateLaunchdJobs(deps?: { async function launchctlGetenv(name: string): Promise { try { - const result = await runExec("/bin/launchctl", ["getenv", name], { logOutput: false }); + const result = await runExec("/bin/launchctl", ["getenv", name], { + logOutput: false, + timeoutMs: DOCTOR_LAUNCHCTL_TIMEOUT_MS, + }); const value = normalizeOptionalString(result.stdout) ?? ""; return value.length > 0 ? value : undefined; } catch {