diff --git a/apps/macos/Tests/ClawdbotIPCTests/TalkAudioPlayerTests.swift b/apps/macos/Tests/ClawdbotIPCTests/TalkAudioPlayerTests.swift index a42f5616f..d010725fc 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/TalkAudioPlayerTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/TalkAudioPlayerTests.swift @@ -8,7 +8,7 @@ import Testing let wav = makeWav16Mono(sampleRate: 8000, samples: 80) defer { _ = TalkAudioPlayer.shared.stop() } - _ = try await withTimeout(seconds: 2.0) { + _ = try await withTimeout(seconds: 4.0) { await TalkAudioPlayer.shared.play(data: wav) } @@ -27,7 +27,7 @@ import Testing await Task.yield() _ = await TalkAudioPlayer.shared.play(data: wav) - _ = try await withTimeout(seconds: 2.0) { + _ = try await withTimeout(seconds: 4.0) { await first.value } #expect(true) diff --git a/src/browser/chrome.default-browser.test.ts b/src/browser/chrome.default-browser.test.ts index 04c7631b1..cfa1e2167 100644 --- a/src/browser/chrome.default-browser.test.ts +++ b/src/browser/chrome.default-browser.test.ts @@ -24,10 +24,10 @@ describe("browser default executable detection", () => { it("prefers default Chromium browser on macOS", async () => { vi.mocked(execFileSync).mockImplementation((cmd, args) => { const argsStr = Array.isArray(args) ? args.join(" ") : ""; - if (cmd === "/usr/bin/osascript" && argsStr.includes("id of application")) { - return "com.google.Chrome"; + if (cmd === "/usr/bin/plutil" && argsStr.includes("LSHandlers")) { + return JSON.stringify([{ LSHandlerURLScheme: "http", LSHandlerRoleAll: "com.google.Chrome" }]); } - if (cmd === "/usr/bin/osascript" && argsStr.includes("POSIX path")) { + if (cmd === "/usr/bin/osascript" && argsStr.includes("path to application id")) { return "/Applications/Google Chrome.app"; } if (cmd === "/usr/bin/defaults") { @@ -35,9 +35,11 @@ describe("browser default executable detection", () => { } return ""; }); - vi.mocked(fs.existsSync).mockImplementation((p) => - String(p).includes("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"), - ); + vi.mocked(fs.existsSync).mockImplementation((p) => { + const value = String(p); + if (value.includes("com.apple.launchservices.secure.plist")) return true; + return value.includes("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"); + }); const { resolveBrowserExecutableForPlatform } = await import("./chrome.executables.js"); const exe = resolveBrowserExecutableForPlatform( @@ -52,14 +54,16 @@ describe("browser default executable detection", () => { it("falls back when default browser is non-Chromium on macOS", async () => { vi.mocked(execFileSync).mockImplementation((cmd, args) => { const argsStr = Array.isArray(args) ? args.join(" ") : ""; - if (cmd === "/usr/bin/osascript" && argsStr.includes("id of application")) { - return "com.apple.Safari"; + if (cmd === "/usr/bin/plutil" && argsStr.includes("LSHandlers")) { + return JSON.stringify([{ LSHandlerURLScheme: "http", LSHandlerRoleAll: "com.apple.Safari" }]); } return ""; }); - vi.mocked(fs.existsSync).mockImplementation((p) => - String(p).includes("Google Chrome.app/Contents/MacOS/Google Chrome"), - ); + vi.mocked(fs.existsSync).mockImplementation((p) => { + const value = String(p); + if (value.includes("com.apple.launchservices.secure.plist")) return true; + return value.includes("Google Chrome.app/Contents/MacOS/Google Chrome"); + }); const { resolveBrowserExecutableForPlatform } = await import("./chrome.executables.js"); const exe = resolveBrowserExecutableForPlatform( diff --git a/src/browser/chrome.executables.ts b/src/browser/chrome.executables.ts index de130bcf1..ddd863520 100644 --- a/src/browser/chrome.executables.ts +++ b/src/browser/chrome.executables.ts @@ -95,12 +95,17 @@ function exists(filePath: string) { } } -function execText(command: string, args: string[], timeoutMs = 1200): string | null { +function execText( + command: string, + args: string[], + timeoutMs = 1200, + maxBuffer = 1024 * 1024, +): string | null { try { const output = execFileSync(command, args, { timeout: timeoutMs, encoding: "utf8", - maxBuffer: 1024 * 1024, + maxBuffer, }); return String(output ?? "").trim() || null; } catch { @@ -140,14 +145,12 @@ function detectDefaultChromiumExecutable( } function detectDefaultChromiumExecutableMac(): BrowserExecutable | null { - const bundleId = execText("/usr/bin/osascript", [ - "-e", - 'id of application (path to default application for URL "http://example.com")', - ]); - if (!bundleId || !CHROMIUM_BUNDLE_IDS.has(bundleId.trim())) return null; + const bundleId = detectDefaultBrowserBundleIdMac(); + if (!bundleId || !CHROMIUM_BUNDLE_IDS.has(bundleId)) return null; + const appPathRaw = execText("/usr/bin/osascript", [ "-e", - 'POSIX path of (path to default application for URL "http://example.com")', + `POSIX path of (path to application id "${bundleId}")`, ]); if (!appPathRaw) return null; const appPath = appPathRaw.trim().replace(/\/$/, ""); @@ -162,6 +165,45 @@ function detectDefaultChromiumExecutableMac(): BrowserExecutable | null { return { kind: inferKindFromIdentifier(bundleId), path: exePath }; } +function detectDefaultBrowserBundleIdMac(): string | null { + const plistPath = path.join( + os.homedir(), + "Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist", + ); + if (!exists(plistPath)) return null; + const handlersRaw = execText( + "/usr/bin/plutil", + ["-extract", "LSHandlers", "json", "-o", "-", "--", plistPath], + 2000, + 5 * 1024 * 1024, + ); + if (!handlersRaw) return null; + let handlers: unknown; + try { + handlers = JSON.parse(handlersRaw); + } catch { + return null; + } + if (!Array.isArray(handlers)) return null; + + const resolveScheme = (scheme: string) => { + let candidate: string | null = null; + for (const entry of handlers) { + if (!entry || typeof entry !== "object") continue; + const record = entry as Record; + if (record.LSHandlerURLScheme !== scheme) continue; + const role = + (typeof record.LSHandlerRoleAll === "string" && record.LSHandlerRoleAll) || + (typeof record.LSHandlerRoleViewer === "string" && record.LSHandlerRoleViewer) || + null; + if (role) candidate = role; + } + return candidate; + }; + + return resolveScheme("http") ?? resolveScheme("https"); +} + function detectDefaultChromiumExecutableLinux(): BrowserExecutable | null { const desktopId = execText("xdg-settings", ["get", "default-web-browser"]) ||