|
@@ -0,0 +1,509 @@
|
|
|
|
|
+package cli
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "bytes"
|
|
|
|
|
+ "os"
|
|
|
|
|
+ "path/filepath"
|
|
|
|
|
+ "strconv"
|
|
|
|
|
+ "strings"
|
|
|
|
|
+ "testing"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// runCLI is a test helper that invokes Execute-equivalent with a temp config,
|
|
|
|
|
+// captured stdout/stderr, and an empty stdin (non-tty).
|
|
|
|
|
+func runCLI(t *testing.T, configPath string, args ...string) (int, string, string, error) {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ full := append([]string{"--config", configPath}, args...)
|
|
|
|
|
+ var stdout, stderr bytes.Buffer
|
|
|
|
|
+ stdin := bytes.NewReader(nil)
|
|
|
|
|
+ code, err := ExecuteWithStreams(full, stdin, &stdout, &stderr)
|
|
|
|
|
+ return code, stdout.String(), stderr.String(), err
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// setupTestConfig yields a config path and ensures CC_SWITCH_CONFIG is not set.
|
|
|
|
|
+func setupTestConfig(t *testing.T) string {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ t.Setenv("CC_SWITCH_CONFIG", "")
|
|
|
|
|
+ t.Setenv("XDG_CONFIG_HOME", "")
|
|
|
|
|
+ dir := t.TempDir()
|
|
|
|
|
+ return filepath.Join(dir, "config.yaml")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestListEmpty(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ code, stdout, _, err := runCLI(t, cfg, "list")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("unexpected: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if code != 0 {
|
|
|
|
|
+ t.Errorf("code = %d", code)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.Contains(stdout, "no providers configured") {
|
|
|
|
|
+ t.Errorf("stdout: %q", stdout)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestAddAndList(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+
|
|
|
|
|
+ _, _, _, err := runCLI(t, cfg,
|
|
|
|
|
+ "add", "foo",
|
|
|
|
|
+ "--env", "ANTHROPIC_API_KEY=sk-ant-12345678",
|
|
|
|
|
+ "--env", "ANTHROPIC_BASE_URL=https://api.anthropic.com",
|
|
|
|
|
+ "--description", "official")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("add: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ _, stdout, _, err := runCLI(t, cfg, "list")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.Contains(stdout, "foo") {
|
|
|
|
|
+ t.Errorf("missing name: %q", stdout)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.Contains(stdout, "official") {
|
|
|
|
|
+ t.Errorf("missing description: %q", stdout)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ _, stdout, _, err = runCLI(t, cfg, "list", "-V")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if strings.Contains(stdout, "sk-ant-12345678") {
|
|
|
|
|
+ t.Errorf("raw secret leaked in -V: %q", stdout)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.Contains(stdout, "sk-a***") {
|
|
|
|
|
+ t.Errorf("expected masked value: %q", stdout)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestAddEnvRefNotMasked(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ if _, _, _, err := runCLI(t, cfg,
|
|
|
|
|
+ "add", "byref",
|
|
|
|
|
+ "--env", "ANTHROPIC_API_KEY=env:MY_KEY",
|
|
|
|
|
+ ); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ _, stdout, _, err := runCLI(t, cfg, "list", "-V")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.Contains(stdout, "env:MY_KEY") {
|
|
|
|
|
+ t.Errorf("env ref should be shown literal: %q", stdout)
|
|
|
|
|
+ }
|
|
|
|
|
+ if strings.Contains(stdout, "env:***") {
|
|
|
|
|
+ t.Errorf("env ref should NOT be masked: %q", stdout)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestAddDuplicate(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ _, _, _, err := runCLI(t, cfg, "add", "foo", "--env", "K=v")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ code, _, _, err := runCLI(t, cfg, "add", "foo", "--env", "K=v")
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ t.Fatal("expected duplicate error")
|
|
|
|
|
+ }
|
|
|
|
|
+ if code == 0 {
|
|
|
|
|
+ t.Error("duplicate should non-zero exit")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestAddInvalidName(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ _, _, _, err := runCLI(t, cfg, "add", "bad name", "--env", "K=v")
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ t.Fatal("expected invalid name error")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestAddRequiresNonInteractiveInputs(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ _, _, _, err := runCLI(t, cfg, "add", "foo")
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ t.Fatal("expected error (non-tty with no --env or template)")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestAddFromTemplateNonInteractive(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ _, _, _, err := runCLI(t, cfg,
|
|
|
|
|
+ "add", "or",
|
|
|
|
|
+ "--from-template", "openrouter",
|
|
|
|
|
+ "--non-interactive",
|
|
|
|
|
+ "--env", "ANTHROPIC_AUTH_TOKEN=sk-or-xxxxxx",
|
|
|
|
|
+ "--env", "ANTHROPIC_MODEL=anthropic/claude-opus-4",
|
|
|
|
|
+ )
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("unexpected: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ // Template should have filled ANTHROPIC_BASE_URL with its default.
|
|
|
|
|
+ _, stdout, _, err := runCLI(t, cfg, "list", "-V")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.Contains(stdout, "ANTHROPIC_BASE_URL") {
|
|
|
|
|
+ t.Errorf("template key missing: %q", stdout)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.Contains(stdout, "https://openrouter.ai/api/v1") {
|
|
|
|
|
+ // OR's default gets masked too (it's shorter than 4... no it isn't — check masked form)
|
|
|
|
|
+ // "https://openrouter.ai/api/v1" is 29 chars; masked as "http***"
|
|
|
|
|
+ if !strings.Contains(stdout, "http***") {
|
|
|
|
|
+ t.Errorf("expected base URL (possibly masked): %q", stdout)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestAddFromTemplateUnknown(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ _, _, stderr, err := runCLI(t, cfg,
|
|
|
|
|
+ "add", "foo", "--from-template", "nonexistent", "--non-interactive")
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ t.Fatal("expected unknown-template error")
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.Contains(stderr, "templates list") && !strings.Contains(err.Error(), "templates list") {
|
|
|
|
|
+ t.Errorf("expected hint, stderr=%q err=%v", stderr, err)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestTemplatesList(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ _, stdout, _, err := runCLI(t, cfg, "templates", "list")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, name := range []string{"anthropic-official", "openrouter", "deepseek", "moonshot", "zhipu", "custom-base"} {
|
|
|
|
|
+ if !strings.Contains(stdout, name) {
|
|
|
|
|
+ t.Errorf("missing template %q in output: %q", name, stdout)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestTemplatesShow(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ _, stdout, _, err := runCLI(t, cfg, "templates", "show", "openrouter")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.Contains(stdout, "ANTHROPIC_BASE_URL") {
|
|
|
|
|
+ t.Errorf("missing env key in show: %q", stdout)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.Contains(stdout, "openrouter.ai") {
|
|
|
|
|
+ t.Errorf("missing default in show: %q", stdout)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestTemplatesShowMissing(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ _, _, _, err := runCLI(t, cfg, "templates", "show", "nonexistent")
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ t.Fatal("expected missing-template error")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestEditAddAndRemoveKey(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ if _, _, _, err := runCLI(t, cfg, "add", "foo",
|
|
|
|
|
+ "--env", "ANTHROPIC_API_KEY=sk-xxxxxx",
|
|
|
|
|
+ "--env", "ANTHROPIC_BASE_URL=https://one"); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if _, _, _, err := runCLI(t, cfg, "edit", "foo",
|
|
|
|
|
+ "--env", "ANTHROPIC_MODEL=claude-opus",
|
|
|
|
|
+ "--remove-env", "ANTHROPIC_BASE_URL",
|
|
|
|
|
+ "--description", "new desc"); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ _, stdout, _, err := runCLI(t, cfg, "list", "-V")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.Contains(stdout, "ANTHROPIC_MODEL") {
|
|
|
|
|
+ t.Errorf("model not added: %q", stdout)
|
|
|
|
|
+ }
|
|
|
|
|
+ if strings.Contains(stdout, "ANTHROPIC_BASE_URL") {
|
|
|
|
|
+ t.Errorf("base URL not removed: %q", stdout)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.Contains(stdout, "new desc") {
|
|
|
|
|
+ t.Errorf("description not updated: %q", stdout)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestRemoveClearsDefault(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ _, _, _, _ = runCLI(t, cfg, "add", "foo", "--env", "K=v")
|
|
|
|
|
+ _, _, _, _ = runCLI(t, cfg, "config", "set", "default", "foo")
|
|
|
|
|
+ _, stdout, _, err := runCLI(t, cfg, "config", "get", "default")
|
|
|
|
|
+ if err != nil || strings.TrimSpace(stdout) != "foo" {
|
|
|
|
|
+ t.Fatalf("default not set: stdout=%q err=%v", stdout, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if _, _, _, err := runCLI(t, cfg, "remove", "foo"); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ _, stdout, _, _ = runCLI(t, cfg, "config", "get", "default")
|
|
|
|
|
+ if strings.TrimSpace(stdout) != "" {
|
|
|
|
|
+ t.Errorf("default not cleared: %q", stdout)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestConfigSetInvalidKey(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ _, _, _, err := runCLI(t, cfg, "config", "set", "bogus", "x")
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ t.Fatal("expected error for unknown config key")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestConfigSetDefaultMissingProvider(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ _, _, _, err := runCLI(t, cfg, "config", "set", "default", "nope")
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ t.Fatal("expected error")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestUseRejectsUnknownProvider(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ _, _, _, _ = runCLI(t, cfg, "add", "foo", "--env", "K=v")
|
|
|
|
|
+ _, _, _, err := runCLI(t, cfg, "use", "bar")
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ t.Fatal("expected unknown provider error")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestUseNonTTYNoDefault(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ _, _, _, _ = runCLI(t, cfg, "add", "foo", "--env", "K=v")
|
|
|
|
|
+ _, _, _, err := runCLI(t, cfg, "use")
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ t.Fatal("expected non-tty-no-default error")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestUseLaunchesWithFakeClaude(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ dir := t.TempDir()
|
|
|
|
|
+ // Fake claude: print FOO to a file, exit 0.
|
|
|
|
|
+ fakeClaude := filepath.Join(dir, "claude")
|
|
|
|
|
+ outFile := filepath.Join(dir, "out")
|
|
|
|
|
+ script := "#!/bin/sh\nprintf '%s' \"$FOO\" > " + outFile + "\nexit 0\n"
|
|
|
|
|
+ if err := os.WriteFile(fakeClaude, []byte(script), 0o755); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if _, _, _, err := runCLI(t, cfg, "add", "ff", "--env", "FOO=bar"); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if _, _, _, err := runCLI(t, cfg, "config", "set", "claude-path", fakeClaude); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ code, _, _, err := runCLI(t, cfg, "use", "ff")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("use: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if code != 0 {
|
|
|
|
|
+ t.Errorf("exit code = %d", code)
|
|
|
|
|
+ }
|
|
|
|
|
+ b, err := os.ReadFile(outFile)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if string(b) != "bar" {
|
|
|
|
|
+ t.Errorf("env not injected: %q", string(b))
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestUseEnvRefMissingDoesNotLaunch(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ dir := t.TempDir()
|
|
|
|
|
+ fakeClaude := filepath.Join(dir, "claude")
|
|
|
|
|
+ sentinel := filepath.Join(dir, "ran")
|
|
|
|
|
+ script := "#!/bin/sh\ntouch " + sentinel + "\nexit 0\n"
|
|
|
|
|
+ if err := os.WriteFile(fakeClaude, []byte(script), 0o755); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if _, _, _, err := runCLI(t, cfg, "add", "needs-ref",
|
|
|
|
|
+ "--env", "ANTHROPIC_API_KEY=env:MY_MISSING_KEY"); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if _, _, _, err := runCLI(t, cfg, "config", "set", "claude-path", fakeClaude); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ // Ensure MY_MISSING_KEY is NOT set.
|
|
|
|
|
+ t.Setenv("MY_MISSING_KEY", "")
|
|
|
|
|
+ os.Unsetenv("MY_MISSING_KEY")
|
|
|
|
|
+
|
|
|
|
|
+ _, _, _, err := runCLI(t, cfg, "use", "needs-ref")
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ t.Fatal("expected error for missing ref")
|
|
|
|
|
+ }
|
|
|
|
|
+ if _, statErr := os.Stat(sentinel); statErr == nil {
|
|
|
|
|
+ t.Error("claude should not have been launched")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestUseExitCodePassthrough(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ dir := t.TempDir()
|
|
|
|
|
+ fakeClaude := filepath.Join(dir, "claude")
|
|
|
|
|
+ script := "#!/bin/sh\nexit 42\n"
|
|
|
|
|
+ if err := os.WriteFile(fakeClaude, []byte(script), 0o755); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ _, _, _, _ = runCLI(t, cfg, "add", "x", "--env", "FOO=b")
|
|
|
|
|
+ _, _, _, _ = runCLI(t, cfg, "config", "set", "claude-path", fakeClaude)
|
|
|
|
|
+
|
|
|
|
|
+ code, _, _, _ := runCLI(t, cfg, "use", "x")
|
|
|
|
|
+ if code != 42 {
|
|
|
|
|
+ t.Errorf("exit code = %d, want 42", code)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// writeFakeClaude creates a shell script that prints each listed env var's
|
|
|
|
|
+// value (one "KEY=VAL" per line) to outFile and exits with the given code.
|
|
|
|
|
+func writeFakeClaude(t *testing.T, dir string, keys []string, outFile string, exitCode int) string {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ path := filepath.Join(dir, "claude")
|
|
|
|
|
+ var sb strings.Builder
|
|
|
|
|
+ sb.WriteString("#!/bin/sh\n")
|
|
|
|
|
+ sb.WriteString(": > " + outFile + "\n")
|
|
|
|
|
+ for _, k := range keys {
|
|
|
|
|
+ // Print KEY=VAL if set (use `set` so unset yields no output for that key).
|
|
|
|
|
+ sb.WriteString("if [ \"${" + k + "+x}\" = x ]; then printf '%s=%s\\n' \"" + k + "\" \"${" + k + "}\" >> " + outFile + "; fi\n")
|
|
|
|
|
+ }
|
|
|
|
|
+ sb.WriteString("exit " + strconv.Itoa(exitCode) + "\n")
|
|
|
|
|
+ if err := os.WriteFile(path, []byte(sb.String()), 0o755); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ return path
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestUseCleansParentEnvUnion(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ dir := t.TempDir()
|
|
|
|
|
+ outFile := filepath.Join(dir, "out")
|
|
|
|
|
+ fakeClaude := writeFakeClaude(t, dir, []string{"ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL"}, outFile, 0)
|
|
|
|
|
+
|
|
|
|
|
+ // provider A owns ANTHROPIC_API_KEY, provider B owns ANTHROPIC_BASE_URL.
|
|
|
|
|
+ // Union includes both. We USE A: B's key should be stripped from env.
|
|
|
|
|
+ _, _, _, _ = runCLI(t, cfg, "add", "a", "--env", "ANTHROPIC_API_KEY=sk-new")
|
|
|
|
|
+ _, _, _, _ = runCLI(t, cfg, "add", "b", "--env", "ANTHROPIC_BASE_URL=https://b")
|
|
|
|
|
+ _, _, _, _ = runCLI(t, cfg, "config", "set", "claude-path", fakeClaude)
|
|
|
|
|
+
|
|
|
|
|
+ // Simulate a polluted parent shell.
|
|
|
|
|
+ t.Setenv("ANTHROPIC_API_KEY", "stale-should-be-overridden")
|
|
|
|
|
+ t.Setenv("ANTHROPIC_BASE_URL", "stale-should-be-cleaned")
|
|
|
|
|
+
|
|
|
|
|
+ if _, _, _, err := runCLI(t, cfg, "use", "a"); err != nil {
|
|
|
|
|
+ t.Fatalf("use: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ b, err := os.ReadFile(outFile)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ got := string(b)
|
|
|
|
|
+ if !strings.Contains(got, "ANTHROPIC_API_KEY=sk-new") {
|
|
|
|
|
+ t.Errorf("missing injected key: %q", got)
|
|
|
|
|
+ }
|
|
|
|
|
+ if strings.Contains(got, "stale") {
|
|
|
|
|
+ t.Errorf("stale parent value leaked: %q", got)
|
|
|
|
|
+ }
|
|
|
|
|
+ if strings.Contains(got, "ANTHROPIC_BASE_URL") {
|
|
|
|
|
+ t.Errorf("union key should have been cleaned: %q", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestUseLiteralValueNotShellExpanded(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ dir := t.TempDir()
|
|
|
|
|
+ outFile := filepath.Join(dir, "out")
|
|
|
|
|
+ fakeClaude := writeFakeClaude(t, dir, []string{"WEIRD"}, outFile, 0)
|
|
|
|
|
+
|
|
|
|
|
+ _, _, _, _ = runCLI(t, cfg, "add", "x", "--env", "WEIRD=$HOME/api")
|
|
|
|
|
+ _, _, _, _ = runCLI(t, cfg, "config", "set", "claude-path", fakeClaude)
|
|
|
|
|
+ if _, _, _, err := runCLI(t, cfg, "use", "x"); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ b, _ := os.ReadFile(outFile)
|
|
|
|
|
+ if !strings.Contains(string(b), "WEIRD=$HOME/api") {
|
|
|
|
|
+ t.Errorf("value should be literal: %q", string(b))
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestUseEnvRefResolvesFromParent(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ dir := t.TempDir()
|
|
|
|
|
+ outFile := filepath.Join(dir, "out")
|
|
|
|
|
+ fakeClaude := writeFakeClaude(t, dir, []string{"ANTHROPIC_API_KEY"}, outFile, 0)
|
|
|
|
|
+
|
|
|
|
|
+ _, _, _, _ = runCLI(t, cfg, "add", "ref", "--env", "ANTHROPIC_API_KEY=env:MY_EXTERNAL_KEY")
|
|
|
|
|
+ _, _, _, _ = runCLI(t, cfg, "config", "set", "claude-path", fakeClaude)
|
|
|
|
|
+
|
|
|
|
|
+ t.Setenv("MY_EXTERNAL_KEY", "sk-from-parent")
|
|
|
|
|
+
|
|
|
|
|
+ if _, _, _, err := runCLI(t, cfg, "use", "ref"); err != nil {
|
|
|
|
|
+ t.Fatalf("use: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ b, _ := os.ReadFile(outFile)
|
|
|
|
|
+ if !strings.Contains(string(b), "ANTHROPIC_API_KEY=sk-from-parent") {
|
|
|
|
|
+ t.Errorf("ref not resolved: %q", string(b))
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestUseEnvRefResolvesBeforeCleanup(t *testing.T) {
|
|
|
|
|
+ // Critical edge: provider B says `ANTHROPIC_API_KEY=env:ANTHROPIC_API_KEY`.
|
|
|
|
|
+ // The referenced VAR is ALSO in the union (provider A also defines that
|
|
|
|
|
+ // key). We must snapshot parent env before cleanup, so the ref still
|
|
|
|
|
+ // resolves to the parent shell's value.
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ dir := t.TempDir()
|
|
|
|
|
+ outFile := filepath.Join(dir, "out")
|
|
|
|
|
+ fakeClaude := writeFakeClaude(t, dir, []string{"ANTHROPIC_API_KEY"}, outFile, 0)
|
|
|
|
|
+
|
|
|
|
|
+ _, _, _, _ = runCLI(t, cfg, "add", "a", "--env", "ANTHROPIC_API_KEY=sk-a")
|
|
|
|
|
+ _, _, _, _ = runCLI(t, cfg, "add", "b", "--env", "ANTHROPIC_API_KEY=env:ANTHROPIC_API_KEY")
|
|
|
|
|
+ _, _, _, _ = runCLI(t, cfg, "config", "set", "claude-path", fakeClaude)
|
|
|
|
|
+
|
|
|
|
|
+ t.Setenv("ANTHROPIC_API_KEY", "sk-parent")
|
|
|
|
|
+
|
|
|
|
|
+ if _, _, _, err := runCLI(t, cfg, "use", "b"); err != nil {
|
|
|
|
|
+ t.Fatalf("use: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ b, _ := os.ReadFile(outFile)
|
|
|
|
|
+ if !strings.Contains(string(b), "ANTHROPIC_API_KEY=sk-parent") {
|
|
|
|
|
+ t.Errorf("snapshot-before-cleanup broke: %q", string(b))
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestPermissionWarning(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ if _, _, _, err := runCLI(t, cfg, "add", "x", "--env", "K=v"); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := os.Chmod(cfg, 0o644); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ _, _, stderr, err := runCLI(t, cfg, "list")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.Contains(stderr, "chmod 600") {
|
|
|
|
|
+ t.Errorf("expected permission warning; stderr=%q", stderr)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestVersionCmd(t *testing.T) {
|
|
|
|
|
+ cfg := setupTestConfig(t)
|
|
|
|
|
+ _, stdout, _, err := runCLI(t, cfg, "version")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.HasPrefix(stdout, "cc-switch ") {
|
|
|
|
|
+ t.Errorf("unexpected: %q", stdout)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|