package provider import ( "errors" "reflect" "sort" "strings" "testing" "github.com/kotoyuuko/cc-switch-cli/internal/config" ) func TestUnionEnvKeys(t *testing.T) { in := map[string]config.Provider{ "a": {Env: map[string]string{"X": "1", "Y": "2"}}, "b": {Env: map[string]string{"Y": "3", "Z": "4"}}, } got := UnionEnvKeys(in) want := []string{"X", "Y", "Z"} if !reflect.DeepEqual(got, want) { t.Errorf("UnionEnvKeys = %v, want %v", got, want) } } func TestResolveEnvRefs_Literal(t *testing.T) { p := config.Provider{Env: map[string]string{"A": "plain", "B": "$HOME/api"}} got, err := ResolveEnvRefs(p, map[string]string{"HOME": "/root"}) if err != nil { t.Fatal(err) } if got["A"] != "plain" { t.Errorf("A: %q", got["A"]) } if got["B"] != "$HOME/api" { t.Errorf("B shouldn't be shell-expanded: %q", got["B"]) } } func TestResolveEnvRefs_Ref(t *testing.T) { p := config.Provider{Env: map[string]string{ "ANTHROPIC_API_KEY": "env:MY_KEY", "ANTHROPIC_BASE_URL": "https://x", }} snap := map[string]string{"MY_KEY": "sk-ok"} got, err := ResolveEnvRefs(p, snap) if err != nil { t.Fatal(err) } if got["ANTHROPIC_API_KEY"] != "sk-ok" { t.Errorf("ref not resolved: %q", got["ANTHROPIC_API_KEY"]) } if got["ANTHROPIC_BASE_URL"] != "https://x" { t.Errorf("literal changed: %q", got["ANTHROPIC_BASE_URL"]) } } func TestResolveEnvRefs_Missing(t *testing.T) { p := config.Provider{Env: map[string]string{"K": "env:MISSING"}} _, err := ResolveEnvRefs(p, map[string]string{}) if err == nil { t.Fatal("expected error") } var ref *EnvRefError if !errors.As(err, &ref) { t.Fatalf("wrong error type: %T %v", err, err) } if ref.Key != "K" || ref.Var != "MISSING" { t.Errorf("unexpected: %#v", ref) } if !strings.Contains(err.Error(), "MISSING") { t.Errorf("message should name missing var: %q", err.Error()) } } func TestResolveEnvRefs_NoChain(t *testing.T) { // `env:A` → snap has A="env:B", B="plain". We should get "env:B" literally. p := config.Provider{Env: map[string]string{"K": "env:A"}} snap := map[string]string{"A": "env:B", "B": "plain"} got, err := ResolveEnvRefs(p, snap) if err != nil { t.Fatal(err) } if got["K"] != "env:B" { t.Errorf("chain should NOT be followed; got %q", got["K"]) } } func TestBuildChildEnv_CleansUnion(t *testing.T) { parent := []string{"HOME=/root", "ANTHROPIC_API_KEY=old", "PATH=/usr/bin"} union := []string{"ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL"} resolved := map[string]string{} // selected provider has NONE of those got := BuildChildEnv(parent, union, resolved) for _, kv := range got { if strings.HasPrefix(kv, "ANTHROPIC_API_KEY=") { t.Errorf("old API key leaked: %q", kv) } } // HOME/PATH should survive. want := map[string]bool{"HOME=/root": true, "PATH=/usr/bin": true} for _, kv := range got { delete(want, kv) } if len(want) != 0 { t.Errorf("unrelated vars dropped: %v", want) } } func TestBuildChildEnv_InjectOverrides(t *testing.T) { parent := []string{"HOME=/root", "ANTHROPIC_MODEL=x"} union := []string{} // empty — model not in anyone's provider env resolved := map[string]string{"ANTHROPIC_MODEL": "y"} got := BuildChildEnv(parent, union, resolved) // HOME present once, model = y exactly once var home, model int for _, kv := range got { if kv == "HOME=/root" { home++ } if strings.HasPrefix(kv, "ANTHROPIC_MODEL=") { if kv != "ANTHROPIC_MODEL=y" { t.Errorf("wrong model value: %q", kv) } model++ } } if home != 1 { t.Errorf("HOME count = %d", home) } if model != 1 { t.Errorf("model count = %d", model) } } func TestBuildChildEnv_DeterministicOrder(t *testing.T) { parent := []string{} union := []string{} resolved := map[string]string{"B": "2", "A": "1", "C": "3"} got := BuildChildEnv(parent, union, resolved) // The injected tail should be sorted. sorted := make([]string, len(got)) copy(sorted, got) sort.Strings(sorted) if !reflect.DeepEqual(got, sorted) { t.Errorf("injected env not sorted: %v", got) } } func TestSnapshotEnv(t *testing.T) { got := SnapshotEnv([]string{"A=1", "B=2=3", "MALFORMED", "A=override"}) if got["A"] != "override" { t.Errorf("last-wins: %q", got["A"]) } if got["B"] != "2=3" { t.Errorf("first = only: %q", got["B"]) } if _, ok := got["MALFORMED"]; ok { t.Error("malformed entry should be dropped") } }