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 {