diff --git a/apps/macos/Sources/Clawdbot/ConnectionModeResolver.swift b/apps/macos/Sources/Clawdbot/ConnectionModeResolver.swift index 0d5bcbbf9..f0aeb4960 100644 --- a/apps/macos/Sources/Clawdbot/ConnectionModeResolver.swift +++ b/apps/macos/Sources/Clawdbot/ConnectionModeResolver.swift @@ -47,4 +47,3 @@ enum ConnectionModeResolver { return EffectiveConnectionMode(mode: seen ? .local : .unconfigured, source: .onboarding) } } - diff --git a/src/commands/doctor-platform-notes.launchctl-env-overrides.test.ts b/src/commands/doctor-platform-notes.launchctl-env-overrides.test.ts new file mode 100644 index 000000000..df5c4b4e5 --- /dev/null +++ b/src/commands/doctor-platform-notes.launchctl-env-overrides.test.ts @@ -0,0 +1,61 @@ +import type { ClawdbotConfig } from "../config/config.js"; + +import { describe, expect, it, vi } from "vitest"; + +import { noteMacLaunchctlGatewayEnvOverrides } from "./doctor-platform-notes.js"; + +describe("noteMacLaunchctlGatewayEnvOverrides", () => { + it("prints clear unsetenv instructions for token override", async () => { + const noteFn = vi.fn(); + const getenv = vi.fn(async (name: string) => + name === "CLAWDBOT_GATEWAY_TOKEN" ? "launchctl-token" : undefined, + ); + const cfg = { + gateway: { + auth: { + token: "config-token", + }, + }, + } as ClawdbotConfig; + + await noteMacLaunchctlGatewayEnvOverrides(cfg, { platform: "darwin", getenv, noteFn }); + + expect(noteFn).toHaveBeenCalledTimes(1); + expect(getenv).toHaveBeenCalledTimes(2); + + const [message, title] = noteFn.mock.calls[0] ?? []; + expect(title).toBe("Gateway (macOS)"); + expect(message).toContain("launchctl environment overrides detected"); + expect(message).toContain("CLAWDBOT_GATEWAY_TOKEN"); + expect(message).toContain("launchctl unsetenv CLAWDBOT_GATEWAY_TOKEN"); + expect(message).not.toContain("CLAWDBOT_GATEWAY_PASSWORD"); + }); + + it("does nothing when config has no gateway credentials", async () => { + const noteFn = vi.fn(); + const getenv = vi.fn(async () => "launchctl-token"); + const cfg = {} as ClawdbotConfig; + + await noteMacLaunchctlGatewayEnvOverrides(cfg, { platform: "darwin", getenv, noteFn }); + + expect(getenv).not.toHaveBeenCalled(); + expect(noteFn).not.toHaveBeenCalled(); + }); + + it("does nothing on non-darwin platforms", async () => { + const noteFn = vi.fn(); + const getenv = vi.fn(async () => "launchctl-token"); + const cfg = { + gateway: { + auth: { + token: "config-token", + }, + }, + } as ClawdbotConfig; + + await noteMacLaunchctlGatewayEnvOverrides(cfg, { platform: "linux", getenv, noteFn }); + + expect(getenv).not.toHaveBeenCalled(); + expect(noteFn).not.toHaveBeenCalled(); + }); +}); diff --git a/src/commands/doctor-platform-notes.ts b/src/commands/doctor-platform-notes.ts index 1fdcada1a..80634ee30 100644 --- a/src/commands/doctor-platform-notes.ts +++ b/src/commands/doctor-platform-notes.ts @@ -49,12 +49,21 @@ function hasConfigGatewayCreds(cfg: ClawdbotConfig): boolean { return Boolean(localToken || localPassword || remoteToken || remotePassword); } -export async function noteMacLaunchctlGatewayEnvOverrides(cfg: ClawdbotConfig) { - if (process.platform !== "darwin") return; +export async function noteMacLaunchctlGatewayEnvOverrides( + cfg: ClawdbotConfig, + deps?: { + platform?: NodeJS.Platform; + getenv?: (name: string) => Promise; + noteFn?: typeof note; + }, +) { + const platform = deps?.platform ?? process.platform; + if (platform !== "darwin") return; if (!hasConfigGatewayCreds(cfg)) return; - const envToken = await launchctlGetenv("CLAWDBOT_GATEWAY_TOKEN"); - const envPassword = await launchctlGetenv("CLAWDBOT_GATEWAY_PASSWORD"); + const getenv = deps?.getenv ?? launchctlGetenv; + const envToken = await getenv("CLAWDBOT_GATEWAY_TOKEN"); + const envPassword = await getenv("CLAWDBOT_GATEWAY_PASSWORD"); if (!envToken && !envPassword) return; const lines = [ @@ -68,5 +77,5 @@ export async function noteMacLaunchctlGatewayEnvOverrides(cfg: ClawdbotConfig) { envPassword ? " launchctl unsetenv CLAWDBOT_GATEWAY_PASSWORD" : undefined, ].filter((line): line is string => Boolean(line)); - note(lines.join("\n"), "Gateway (macOS)"); + (deps?.noteFn ?? note)(lines.join("\n"), "Gateway (macOS)"); }