package codex import ( "context" "encoding/json" "os" "os/exec" "path/filepath" "strings" "testing" "time" "github.com/chenhg5/cc-connect/config" "github.com/chenhg5/cc-connect/core" ) func skipIfNoConfig(t *testing.T) *config.Config { t.Helper() if os.Getenv("CC_SKIP_INTEGRATION") == "1" { t.Skip("CC_SKIP_INTEGRATION=1") } if os.Getenv("CC_RUN_PROVIDER_INTEGRATION") != "1" { t.Skip("set CC_RUN_PROVIDER_INTEGRATION=1 to run provider integration tests") } cfgPath := os.ExpandEnv("$HOME/.cc-connect/config.toml") if _, err := os.Stat(cfgPath); os.IsNotExist(err) { t.Skipf("config not found at %s", cfgPath) } cfg, err := config.Load(cfgPath) if err != nil { t.Fatalf("failed to load config: %v", err) } cfg.ResolveProviderRefs() return cfg } func configToCoreProv(p config.ProviderConfig) core.ProviderConfig { cp := core.ProviderConfig{ Name: p.Name, APIKey: p.APIKey, BaseURL: p.BaseURL, Model: p.Model, Env: p.Env, } if p.Codex != nil { cp.CodexWireAPI = p.Codex.WireAPI cp.CodexHTTPHeaders = p.Codex.HTTPHeaders } for _, m := range p.Models { cp.Models = append(cp.Models, core.ModelOption{Name: m.Model}) } return cp } func envSliceToMap(env []string) map[string]string { out := make(map[string]string, len(env)) for _, entry := range env { key, value, ok := strings.Cut(entry, "=") if !ok { continue } out[key] = value } return out } func findCodexProject(cfg *config.Config) (name string, providers []core.ProviderConfig, workDir, codexHome string) { for i := range cfg.Projects { proj := &cfg.Projects[i] if proj.Agent.Type != "codex" || len(proj.Agent.Providers) == 0 { continue } for _, p := range proj.Agent.Providers { providers = append(providers, configToCoreProv(p)) } name = proj.Name if wd, ok := proj.Agent.Options["work_dir"].(string); ok { workDir = wd } if ch, ok := proj.Agent.Options["codex_home"].(string); ok { codexHome = ch } return } return } func TestIntegration_Codex_ProviderSwitch_EnvVars(t *testing.T) { cfg := skipIfNoConfig(t) name, providers, _, _ := findCodexProject(cfg) if name == "" { t.Skip("no codex project with providers found") } if len(providers) < 2 { t.Skipf("project %q has only %d provider(s), need at least 2", name, len(providers)) } a := &Agent{ providers: providers, activeIdx: -1, } p0, p1 := providers[0], providers[1] t.Logf("provider[0]: name=%s model=%s base_url=%s", p0.Name, p0.Model, p0.BaseURL) t.Logf("provider[1]: name=%s model=%s base_url=%s", p1.Name, p1.Model, p1.BaseURL) a.SetActiveProvider(p0.Name) a.mu.RLock() env0 := envSliceToMap(a.providerEnvLocked()) a.mu.RUnlock() if p0.BaseURL != "" { if got := env0["OPENAI_BASE_URL"]; got != p0.BaseURL { t.Errorf("provider[0] OPENAI_BASE_URL = %q, want %q", got, p0.BaseURL) } } if p0.APIKey != "" { if got := env0["OPENAI_API_KEY"]; got == "" { t.Error("provider[0] OPENAI_API_KEY not set") } } a.SetActiveProvider(p1.Name) a.mu.RLock() env1 := envSliceToMap(a.providerEnvLocked()) a.mu.RUnlock() if p1.BaseURL != "" { if got := env1["OPENAI_BASE_URL"]; got != p1.BaseURL { t.Errorf("provider[1] OPENAI_BASE_URL = %q, want %q", got, p1.BaseURL) } } if p1.APIKey != "" { if got := env1["OPENAI_API_KEY"]; got == "" { t.Error("provider[1] OPENAI_API_KEY not set") } } if p0.BaseURL != p1.BaseURL { if env0["OPENAI_BASE_URL"] == env1["OPENAI_BASE_URL"] { t.Error("providers have different base_urls but OPENAI_BASE_URL didn't change after switch") } } } func TestIntegration_Codex_ProviderSwitch_SessionArgs(t *testing.T) { cfg := skipIfNoConfig(t) name, providers, workDir, codexHome := findCodexProject(cfg) if name == "" { t.Skip("no codex project with providers found") } if workDir == "" { workDir = t.TempDir() } for _, prov := range providers { t.Run(prov.Name, func(t *testing.T) { a := &Agent{ model: "default-model", providers: providers, activeIdx: -1, workDir: workDir, codexHome: codexHome, mode: "suggest", backend: "exec", } a.SetActiveProvider(prov.Name) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() sess, err := a.StartSession(ctx, "") if err != nil { t.Fatalf("StartSession failed: %v", err) } defer sess.Close() cs := sess.(*codexSession) if prov.Model != "" && cs.model != prov.Model { t.Errorf("session model = %q, want %q", cs.model, prov.Model) } if prov.BaseURL != "" && cs.baseURL != prov.BaseURL { t.Errorf("session baseURL = %q, want %q", cs.baseURL, prov.BaseURL) } envMap := envSliceToMap(cs.extraEnv) if prov.APIKey != "" { if got := envMap["OPENAI_API_KEY"]; got == "" { t.Error("OPENAI_API_KEY not set in session extraEnv") } } if prov.BaseURL != "" { if got := envMap["OPENAI_BASE_URL"]; got != prov.BaseURL { t.Errorf("OPENAI_BASE_URL = %q, want %q", got, prov.BaseURL) } } t.Logf("session OK: model=%s baseURL=%s modelProvider=%s", cs.model, cs.baseURL, cs.modelProvider) }) } } func TestIntegration_Codex_ProviderConfig_WrittenCorrectly(t *testing.T) { cfg := skipIfNoConfig(t) name, providers, _, _ := findCodexProject(cfg) if name == "" { t.Skip("no codex project with providers found") } for _, prov := range providers { t.Run(prov.Name, func(t *testing.T) { if prov.BaseURL == "" || prov.APIKey == "" { t.Skip("provider has no base_url or api_key") } home := filepath.Join(t.TempDir(), ".codex") err := ensureCodexProviderConfig(home, prov.Name, prov.BaseURL, prov.CodexWireAPI, prov.CodexHTTPHeaders) if err != nil { t.Fatalf("ensureCodexProviderConfig failed: %v", err) } cfgData, err := os.ReadFile(filepath.Join(home, "config.toml")) if err != nil { t.Fatalf("read config.toml: %v", err) } cfgContent := string(cfgData) if !strings.Contains(cfgContent, `[model_providers.`+prov.Name+`]`) { t.Errorf("config.toml missing provider section for %s", prov.Name) } if !strings.Contains(cfgContent, prov.BaseURL) { t.Errorf("config.toml missing base_url %s", prov.BaseURL) } err = ensureCodexAuth(home, prov.APIKey) if err != nil { t.Fatalf("ensureCodexAuth failed: %v", err) } authData, err := os.ReadFile(filepath.Join(home, "auth.json")) if err != nil { t.Fatalf("read auth.json: %v", err) } var authMap map[string]any if err := json.Unmarshal(authData, &authMap); err != nil { t.Fatalf("parse auth.json: %v", err) } if authMap["OPENAI_API_KEY"] != prov.APIKey { t.Error("OPENAI_API_KEY not written correctly to auth.json") } t.Logf("provider config written OK: name=%s wireAPI=%s", prov.Name, prov.CodexWireAPI) }) } } func TestIntegration_Codex_ProviderSwitch_SendMessage(t *testing.T) { cfg := skipIfNoConfig(t) if _, err := exec.LookPath("codex"); err != nil { t.Skip("codex CLI not in PATH") } name, providers, workDir, codexHome := findCodexProject(cfg) if name == "" { t.Skip("no codex project with providers found") } if codexHome == "" { home, _ := os.UserHomeDir() codexHome = filepath.Join(home, ".codex") } if workDir == "" { workDir = t.TempDir() gitInit := exec.Command("git", "init", "-q") gitInit.Dir = workDir if err := gitInit.Run(); err != nil { t.Fatalf("git init: %v", err) } } for _, prov := range providers { t.Run(prov.Name, func(t *testing.T) { if prov.APIKey == "" || prov.BaseURL == "" { t.Skip("provider missing api_key or base_url") } a := &Agent{ model: "default-model", providers: providers, activeIdx: -1, workDir: workDir, codexHome: codexHome, mode: "full-auto", backend: "exec", } a.SetActiveProvider(prov.Name) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() sess, err := a.StartSession(ctx, "") if err != nil { t.Fatalf("StartSession failed: %v", err) } defer sess.Close() err = sess.Send("reply with exactly 'codex-provider-ok' and nothing else", nil, nil) if err != nil { t.Fatalf("Send failed: %v", err) } timer := time.NewTimer(25 * time.Second) defer timer.Stop() var allText strings.Builder for { select { case ev, ok := <-sess.Events(): if !ok { if allText.Len() > 0 && strings.Contains(allText.String(), "codex-provider-ok") { t.Logf("provider %s: send+receive OK (from text events)", prov.Name) return } t.Fatalf("event channel closed, collected text: %s", allText.String()) return } t.Logf("event: type=%s content_len=%d", ev.Type, len(ev.Content)) if ev.Type == core.EventText || ev.Type == core.EventResult { allText.WriteString(ev.Content) } if ev.Type == core.EventResult { if strings.Contains(allText.String(), "codex-provider-ok") { t.Logf("provider %s: send+receive OK", prov.Name) } else { t.Errorf("expected 'codex-provider-ok' in output, got: %s", allText.String()) } return } if ev.Type == core.EventError { t.Fatalf("agent error: %s", ev.Content) } case <-timer.C: if strings.Contains(allText.String(), "codex-provider-ok") { t.Logf("provider %s: send+receive OK (timeout but got result)", prov.Name) return } t.Fatalf("timeout waiting for codex response, collected: %s", allText.String()) } } }) } }