diff --git a/src/commands/doctor-gateway-daemon-flow.ts b/src/commands/doctor-gateway-daemon-flow.ts index ab8ecd63c1e1..4a299cd40aae 100644 --- a/src/commands/doctor-gateway-daemon-flow.ts +++ b/src/commands/doctor-gateway-daemon-flow.ts @@ -59,14 +59,14 @@ type LaunchAgentBootstrapDoctorOutcome = function noteGatewayRuntime( serviceRuntime: GatewayServiceRuntime | undefined, env: Record, -): boolean { +): void { const summary = formatGatewayRuntimeSummary(serviceRuntime); const hints = buildGatewayRuntimeHints(serviceRuntime, { platform: process.platform, env, }); if (!summary && hints.length === 0) { - return false; + return; } const lines: string[] = []; @@ -75,7 +75,6 @@ function noteGatewayRuntime( } lines.push(...hints); note(lines.join("\n"), "Gateway"); - return true; } async function maybeRepairLaunchAgentBootstrap(params: { @@ -208,6 +207,8 @@ export async function maybeRepairGatewayDaemon(params: { const serviceRepairPolicy = resolveServiceRepairPolicy(); const serviceRepairExternal = isServiceRepairExternallyManaged(serviceRepairPolicy); const service = resolveGatewayService(); + const isLocalDarwinGateway = + process.platform === "darwin" && params.cfg.gateway?.mode !== "remote"; // systemd can throw in containers/WSL; treat as "not loaded" and fall back to hints. let loaded; try { @@ -225,8 +226,7 @@ export async function maybeRepairGatewayDaemon(params: { ...command.environment, } satisfies NodeJS.ProcessEnv) : process.env; - const shouldReadRuntime = - loaded || (process.platform === "darwin" && params.cfg.gateway?.mode !== "remote"); + const shouldReadRuntime = loaded || isLocalDarwinGateway; if (shouldReadRuntime) { serviceRuntime = await service.readRuntime(serviceEnv).catch(() => undefined); } @@ -237,7 +237,7 @@ export async function maybeRepairGatewayDaemon(params: { } } - if (process.platform === "darwin" && params.cfg.gateway?.mode !== "remote") { + if (isLocalDarwinGateway) { const gatewayRepair = serviceRuntime?.missingGuiSession ? ({ status: "gui-session-unavailable", detail: serviceRuntime.detail ?? "" } as const) : await maybeRepairLaunchAgentBootstrap({ @@ -296,7 +296,7 @@ export async function maybeRepairGatewayDaemon(params: { if (!loaded) { if ( - process.platform === "darwin" && + isLocalDarwinGateway && (serviceRuntime?.missingGuiSession || serviceRuntime?.missingSupervision || serviceRuntime?.cachedLabel) diff --git a/src/daemon/launchd.test.ts b/src/daemon/launchd.test.ts index 03c0d73a07f1..fb98147d9b08 100644 --- a/src/daemon/launchd.test.ts +++ b/src/daemon/launchd.test.ts @@ -428,10 +428,13 @@ describe("launchd runtime state", () => { expect(runtime.detail).toBe("Could not find service"); }); - it("marks installed LaunchAgents unavailable when the GUI domain is missing", async () => { + it.each([ + "Bootstrap failed: 125: Domain does not support specified action", + "Could not find domain for user gui: 999999", + ])("marks installed LaunchAgents unavailable when launchd reports %s", async (detail) => { const env = createDefaultLaunchdEnv(); state.files.set(resolveLaunchAgentPlistPath(env), ""); - state.printError = "Bootstrap failed: 125: Domain does not support specified action"; + state.printError = detail; state.printFailuresRemaining = 1; const runtime = await readLaunchAgentRuntime(env); @@ -439,7 +442,7 @@ describe("launchd runtime state", () => { expect(runtime.status).toBe("unknown"); expect(runtime.missingSupervision).toBe(true); expect(runtime.missingGuiSession).toBe(true); - expect(runtime.detail).toContain("Domain does not support specified action"); + expect(runtime.detail).toBe(detail); }); it("marks a missing unit when launchd has no job and no plist exists", async () => { @@ -793,8 +796,11 @@ describe("launchd bootstrap repair", () => { expect(launchctlCommandNames()).not.toContain("kickstart"); }); - it("classifies headless GUI bootstrap failures separately from generic not-loaded repair", async () => { - state.bootstrapError = "Bootstrap failed: 125: Domain does not support specified action"; + it.each([ + "Bootstrap failed: 125: Domain does not support specified action", + "Could not find domain for user gui: 999999", + ])("classifies %s separately from generic not-loaded repair", async (detail) => { + state.bootstrapError = detail; const env = createDefaultLaunchdEnv(); const repair = await repairLaunchAgentBootstrap({ env }); @@ -802,7 +808,7 @@ describe("launchd bootstrap repair", () => { expect(repair).toEqual({ ok: false, status: "gui-session-unavailable", - detail: "Bootstrap failed: 125: Domain does not support specified action", + detail, domain: typeof process.getuid === "function" ? `gui/${process.getuid()}` : "gui/501", }); expect(launchctlCommandNames()).not.toContain("kickstart"); diff --git a/src/daemon/launchd.ts b/src/daemon/launchd.ts index 97bf0f4c7cba..a1ebaeb727f4 100644 --- a/src/daemon/launchd.ts +++ b/src/daemon/launchd.ts @@ -707,6 +707,7 @@ function isUnsupportedGuiDomain(detail: string): boolean { const normalized = normalizeLowercaseStringOrEmpty(detail); return ( normalized.includes("domain does not support specified action") || + normalized.includes("could not find domain for user gui") || normalized.includes("bootstrap failed: 125") ); }