fix(daemon): detect Codex CLI under ChatGPT.app on macOS (#5250)

Detect the bundled Codex CLI under the relocated ChatGPT.app on macOS, while keeping the legacy Codex.app path so older installs still resolve.

Closes #5205
This commit is contained in:
Kyou
2026-07-12 13:50:54 +08:00
committed by GitHub
parent 7356b56a10
commit c7002e3b30
2 changed files with 85 additions and 1 deletions

View File

@@ -756,12 +756,22 @@ var defaultAgentCommandNames = []string{
"pi", "cursor-agent", "copilot", "kimi", "kiro-cli", "codebuddy", "agy", "traecli",
}
// codexDesktopAppBundlePaths returns candidate macOS app-bundle locations for
// the bundled Codex CLI. OpenAI relocated the Desktop app from Codex.app to
// ChatGPT.app (#5205). Candidates are ordered by install location first
// (system /Applications before user ~/Applications); within each location the
// new ChatGPT.app path is tried before the legacy Codex.app path, so updated
// installs win while older installs still resolve.
var codexDesktopAppBundlePaths = func() []string {
paths := []string{
"/Applications/ChatGPT.app/Contents/Resources/codex",
"/Applications/Codex.app/Contents/Resources/codex",
}
if home, err := os.UserHomeDir(); err == nil {
paths = append(paths, filepath.Join(home, "Applications", "Codex.app", "Contents", "Resources", "codex"))
paths = append(paths,
filepath.Join(home, "Applications", "ChatGPT.app", "Contents", "Resources", "codex"),
filepath.Join(home, "Applications", "Codex.app", "Contents", "Resources", "codex"),
)
}
return paths
}

View File

@@ -660,6 +660,80 @@ func TestLoadConfig_UsesCodexDesktopAppBundleFallback(t *testing.T) {
}
}
// Regression for #5205: after OpenAI moved the Desktop app to ChatGPT.app,
// Multica must resolve the bundled CLI under ChatGPT.app (and prefer it over
// the legacy Codex.app path when both exist).
func TestLoadConfig_UsesChatGPTAppBundleCodexPath(t *testing.T) {
pathDir := t.TempDir()
fakeChatGPT := filepath.Join(pathDir, "ChatGPT.app", "Contents", "Resources", "codex")
fakeLegacy := filepath.Join(pathDir, "Codex.app", "Contents", "Resources", "codex")
for _, p := range []string{fakeChatGPT, fakeLegacy} {
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
if err := os.WriteFile(p, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
t.Fatalf("write fake CLI: %v", err)
}
}
oldBundlePaths := codexDesktopAppBundlePaths
// Prefer ChatGPT first, matching production ordering.
codexDesktopAppBundlePaths = func() []string { return []string{fakeChatGPT, fakeLegacy} }
t.Cleanup(func() { codexDesktopAppBundlePaths = oldBundlePaths })
t.Setenv("PATH", t.TempDir())
t.Setenv("SHELL", filepath.Join(t.TempDir(), "fish"))
t.Setenv("MULTICA_DAEMON_ID", "11111111-1111-1111-1111-111111111111")
pinNonCodexAgentsToMissingPaths(t)
cfg, err := LoadConfig(Overrides{
ServerURL: "http://localhost:0",
WorkspacesRoot: t.TempDir(),
})
if err != nil {
t.Fatalf("LoadConfig: %v", err)
}
got, ok := cfg.Agents["codex"]
if !ok {
t.Fatalf("expected codex agent from ChatGPT.app bundle, got %#v", cfg.Agents)
}
if got.Path != fakeChatGPT {
t.Fatalf("codex path = %q, want ChatGPT.app path %q", got.Path, fakeChatGPT)
}
}
func TestCodexDesktopAppBundlePaths_IncludesChatGPTAndLegacy(t *testing.T) {
paths := codexDesktopAppBundlePaths()
var hasChatGPT, hasLegacy bool
for _, p := range paths {
if strings.Contains(p, "ChatGPT.app") && strings.HasSuffix(filepath.ToSlash(p), "Contents/Resources/codex") {
hasChatGPT = true
}
if strings.Contains(p, "Codex.app") && strings.HasSuffix(filepath.ToSlash(p), "Contents/Resources/codex") {
hasLegacy = true
}
}
if !hasChatGPT {
t.Fatalf("codexDesktopAppBundlePaths missing ChatGPT.app entry: %#v", paths)
}
if !hasLegacy {
t.Fatalf("codexDesktopAppBundlePaths missing legacy Codex.app entry: %#v", paths)
}
// New path must be preferred (listed before legacy).
chatgptIdx, legacyIdx := -1, -1
for i, p := range paths {
if chatgptIdx < 0 && strings.Contains(p, "ChatGPT.app") {
chatgptIdx = i
}
if legacyIdx < 0 && strings.Contains(p, "Codex.app") {
legacyIdx = i
}
}
if chatgptIdx < 0 || legacyIdx < 0 || chatgptIdx > legacyIdx {
t.Fatalf("expected ChatGPT.app before Codex.app, got indices chat=%d legacy=%d paths=%#v", chatgptIdx, legacyIdx, paths)
}
}
func TestLoadConfig_CodexDesktopFallbackDoesNotOverrideExplicitPath(t *testing.T) {
pathDir := t.TempDir()
fakeCodex := filepath.Join(pathDir, "Codex.app", "Contents", "Resources", "codex")