package claudecode import ( "context" "os" "os/exec" "strings" "testing" "time" "github.com/chenhg5/cc-connect/config" "github.com/chenhg5/cc-connect/core" ) // These integration tests use real provider credentials from ~/.cc-connect/config.toml. // They verify that provider switching correctly sets up env vars and that agent // sessions can be started with the right configuration. // // Run with: CC_RUN_PROVIDER_INTEGRATION=1 go test ./agent/claudecode -run TestIntegration -v // Skip explicitly with: CC_SKIP_INTEGRATION=1 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.Thinking != "" { cp.Thinking = p.Thinking } for _, m := range p.Models { cp.Models = append(cp.Models, core.ModelOption{Name: m.Model}) } return cp } func findProjectProviders(cfg *config.Config, agentType string) (projName string, providers []core.ProviderConfig, workDir string) { for i := range cfg.Projects { proj := &cfg.Projects[i] if proj.Agent.Type != agentType || len(proj.Agent.Providers) == 0 { continue } for _, p := range proj.Agent.Providers { providers = append(providers, configToCoreProv(p)) } projName = proj.Name if wd, ok := proj.Agent.Options["work_dir"].(string); ok { workDir = wd } return } return } func TestIntegration_ProviderSwitch_EnvVars(t *testing.T) { cfg := skipIfNoConfig(t) name, providers, _ := findProjectProviders(cfg, "claudecode") if name == "" { t.Skip("no claudecode 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) env0 := envSliceToMap(a.providerEnvLocked()) if p0.BaseURL != "" { if got := env0["ANTHROPIC_BASE_URL"]; got != p0.BaseURL { t.Errorf("provider[0] ANTHROPIC_BASE_URL = %q, want %q", got, p0.BaseURL) } } if p0.Model != "" { if got := env0["ANTHROPIC_MODEL"]; got != p0.Model { t.Errorf("provider[0] ANTHROPIC_MODEL = %q, want %q", got, p0.Model) } } a.SetActiveProvider(p1.Name) env1 := envSliceToMap(a.providerEnvLocked()) if p1.BaseURL != "" { if got := env1["ANTHROPIC_BASE_URL"]; got != p1.BaseURL { t.Errorf("provider[1] ANTHROPIC_BASE_URL = %q, want %q", got, p1.BaseURL) } } if p1.Model != "" { if got := env1["ANTHROPIC_MODEL"]; got != p1.Model { t.Errorf("provider[1] ANTHROPIC_MODEL = %q, want %q", got, p1.Model) } } if p0.BaseURL != p1.BaseURL { if env0["ANTHROPIC_BASE_URL"] == env1["ANTHROPIC_BASE_URL"] { t.Error("providers have different base_urls but env didn't change after switch") } } if p0.Model != p1.Model && p0.Model != "" && p1.Model != "" { if env0["ANTHROPIC_MODEL"] == env1["ANTHROPIC_MODEL"] { t.Error("providers have different models but ANTHROPIC_MODEL didn't change after switch") } } } func TestIntegration_ProviderSwitch_SessionStartModel(t *testing.T) { cfg := skipIfNoConfig(t) name, providers, workDir := findProjectProviders(cfg, "claudecode") if name == "" { t.Skip("no claudecode project with providers found") } if len(providers) < 2 { t.Skipf("project %q has only %d provider(s), need at least 2", name, len(providers)) } if workDir == "" { workDir = "/tmp" } cliBin, err := exec.LookPath("claude") if err != nil { t.Skipf("claude CLI not found: %v", err) } for _, prov := range providers[:2] { t.Run(prov.Name, func(t *testing.T) { a := &Agent{ model: "default-model", providers: providers, activeIdx: -1, workDir: workDir, cliBin: cliBin, } 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.(*claudeSession) envMap := envSliceToMap(cs.cmd.Env) if prov.Model != "" { found := false for _, arg := range cs.cmd.Args { if arg == prov.Model { found = true break } } if !found { t.Errorf("--model %q not found in args: %v", prov.Model, cs.cmd.Args) } } if prov.BaseURL != "" { gotURL := envMap["ANTHROPIC_BASE_URL"] if gotURL == "" { t.Error("ANTHROPIC_BASE_URL not set in session env") } else if !strings.Contains(gotURL, "127.0.0.1") && gotURL != prov.BaseURL { t.Errorf("ANTHROPIC_BASE_URL = %q, want %q (or proxy URL)", gotURL, prov.BaseURL) } } if prov.Model != "" { if got := envMap["ANTHROPIC_MODEL"]; got != prov.Model { t.Errorf("ANTHROPIC_MODEL = %q, want %q", got, prov.Model) } } t.Logf("session started OK: pid=%d, model=%s, env_model=%s, env_base_url=%s", cs.cmd.Process.Pid, prov.Model, envMap["ANTHROPIC_MODEL"], envMap["ANTHROPIC_BASE_URL"]) }) } } func TestIntegration_CodexProvider_EnvVars(t *testing.T) { cfg := skipIfNoConfig(t) name, providers, _ := findProjectProviders(cfg, "codex") if name == "" { t.Skip("no codex project with providers found") } for _, prov := range providers { t.Run(prov.Name, func(t *testing.T) { t.Logf("codex provider: name=%s base_url=%s model=%s", prov.Name, prov.BaseURL, prov.Model) if prov.BaseURL == "" { t.Error("codex provider should have a base_url") } if prov.Model == "" { t.Error("codex provider should have a model") } }) } } func TestIntegration_AgentTypeChange_FiltersProviders(t *testing.T) { cfg := skipIfNoConfig(t) if len(cfg.Providers) == 0 { t.Skip("no global providers in config") } globalByName := make(map[string]config.ProviderConfig, len(cfg.Providers)) for _, p := range cfg.Providers { globalByName[p.Name] = p } for i := range cfg.Projects { proj := &cfg.Projects[i] if proj.Agent.Type != "claudecode" || len(proj.Agent.ProviderRefs) == 0 { continue } var compatible, incompatible []string for _, ref := range proj.Agent.ProviderRefs { gp, ok := globalByName[ref] if !ok { continue } if len(gp.AgentTypes) > 0 { hasCodex := false for _, at := range gp.AgentTypes { if at == "codex" { hasCodex = true } } if hasCodex { compatible = append(compatible, ref) } else { incompatible = append(incompatible, ref) } } else { compatible = append(compatible, ref) } } if len(incompatible) == 0 { continue } t.Logf("project %q: switching claudecode→codex: keep=%v remove=%v", proj.Name, compatible, incompatible) t.Logf("provider filtering validated: %d provider(s) would be removed on agent type change", len(incompatible)) return } t.Skip("no project with incompatible providers found for agent type change test") }