package claudecfg import ( "encoding/json" "os" "path/filepath" "testing" "gitea.rutua.cn/kate522/CrossRelay/internal/config" ) const originalSettings = `{ "theme": "dark", "env": { "ANTHROPIC_AUTH_TOKEN": "sk-official", "ANTHROPIC_BASE_URL": "https://official.example.com", "DISABLE_AUTOUPDATER": "1" } } ` func newManager(t *testing.T) (*Manager, string) { t.Helper() dir := t.TempDir() settingsPath := filepath.Join(dir, "settings.json") if err := os.WriteFile(settingsPath, []byte(originalSettings), 0o600); err != nil { t.Fatalf("write settings: %v", err) } return New(settingsPath, filepath.Join(dir, "backups")), settingsPath } func readSettings(t *testing.T, path string) map[string]any { t.Helper() data, err := os.ReadFile(path) if err != nil { t.Fatalf("read settings: %v", err) } var out map[string]any if err := json.Unmarshal(data, &out); err != nil { t.Fatalf("parse settings: %v", err) } return out } func TestApplyPreservesUnknownKeys(t *testing.T) { mgr, path := newManager(t) if _, err := mgr.Apply(ApplyOptions{ BaseURL: "http://127.0.0.1:8787", Token: "crl_test", Models: config.Models{Sonnet: "ds-chat", Haiku: "ds-haiku", Opus: "ds-opus", Default: "ds-chat"}, }); err != nil { t.Fatalf("Apply: %v", err) } settings := readSettings(t, path) if settings["theme"] != "dark" { t.Errorf("theme lost: %v", settings["theme"]) } env := settings["env"].(map[string]any) if env["ANTHROPIC_BASE_URL"] != "http://127.0.0.1:8787" { t.Errorf("base url = %v", env["ANTHROPIC_BASE_URL"]) } if env["ANTHROPIC_AUTH_TOKEN"] != "crl_test" { t.Errorf("relay token = %v", env["ANTHROPIC_AUTH_TOKEN"]) } if env["DISABLE_AUTOUPDATER"] != "1" { t.Errorf("unrelated env key lost: %v", env) } if env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] != "ds-haiku" { t.Errorf("haiku model = %v", env["ANTHROPIC_DEFAULT_HAIKU_MODEL"]) } if env["ANTHROPIC_MODEL"] != "ds-chat" { t.Errorf("main model = %v", env["ANTHROPIC_MODEL"]) } } func TestApplyCreatesBackupWithOriginalContent(t *testing.T) { mgr, _ := newManager(t) if _, err := mgr.Apply(ApplyOptions{BaseURL: "http://127.0.0.1:1", Token: "crl_x", Models: config.Models{Default: "m"}}); err != nil { t.Fatalf("Apply: %v", err) } backups, err := mgr.Backups() if err != nil { t.Fatalf("Backups: %v", err) } if len(backups) != 1 { t.Fatalf("backups = %d, want 1", len(backups)) } data, err := os.ReadFile(filepath.Join(mgr.backupDir, backups[0].Name)) if err != nil { t.Fatalf("read backup: %v", err) } settings := map[string]any{} if err := json.Unmarshal(data, &settings); err != nil { t.Fatalf("parse backup: %v", err) } env := settings["env"].(map[string]any) if env["ANTHROPIC_BASE_URL"] != "https://official.example.com" { t.Errorf("backup holds post-apply content: %v", env) } } func TestApplyIsIdempotent(t *testing.T) { mgr, path := newManager(t) opts := ApplyOptions{BaseURL: "http://127.0.0.1:8787", Token: "crl_x", Models: config.Models{Default: "ds-chat"}} if _, err := mgr.Apply(opts); err != nil { t.Fatalf("first Apply: %v", err) } afterFirst := readSettings(t, path) if _, err := mgr.Apply(opts); err != nil { t.Fatalf("second Apply: %v", err) } afterSecond := readSettings(t, path) firstEnv, _ := json.Marshal(afterFirst["env"]) secondEnv, _ := json.Marshal(afterSecond["env"]) if string(firstEnv) != string(secondEnv) { t.Errorf("env changed on re-apply:\n%s\n%s", firstEnv, secondEnv) } } func TestRestore(t *testing.T) { mgr, path := newManager(t) if _, err := mgr.Apply(ApplyOptions{BaseURL: "http://127.0.0.1:8787", Token: "crl_x", Models: config.Models{Default: "m"}}); err != nil { t.Fatalf("Apply: %v", err) } name, err := mgr.Restore() if err != nil { t.Fatalf("Restore: %v", err) } if name == "" { t.Error("Restore returned no backup name") } env := readSettings(t, path)["env"].(map[string]any) if env["ANTHROPIC_BASE_URL"] != "https://official.example.com" { t.Errorf("base url after restore = %v", env["ANTHROPIC_BASE_URL"]) } if env["ANTHROPIC_AUTH_TOKEN"] != "sk-official" { t.Errorf("token after restore = %v", env["ANTHROPIC_AUTH_TOKEN"]) } } func TestRestoreWithoutBackupFails(t *testing.T) { mgr, _ := newManager(t) if _, err := mgr.Restore(); err == nil { t.Fatal("Restore succeeded without backups") } } func TestApplyModelRoles(t *testing.T) { mgr, path := newManager(t) // Only a default: every role falls back to it. if _, err := mgr.Apply(ApplyOptions{BaseURL: "http://127.0.0.1:1", Token: "t", Models: config.Models{Default: "only-default"}}); err != nil { t.Fatalf("Apply: %v", err) } env := readSettings(t, path)["env"].(map[string]any) for _, key := range []string{ "ANTHROPIC_MODEL", "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_HAIKU_MODEL", "ANTHROPIC_DEFAULT_OPUS_MODEL", } { if env[key] != "only-default" { t.Errorf("%s = %v, want only-default", key, env[key]) } } // Empty roles remove the keys again. if _, err := mgr.Apply(ApplyOptions{BaseURL: "http://127.0.0.1:1", Token: "t", Models: config.Models{Sonnet: "s"}}); err != nil { t.Fatalf("Apply: %v", err) } env = readSettings(t, path)["env"].(map[string]any) if _, ok := env["ANTHROPIC_DEFAULT_OPUS_MODEL"]; ok { t.Error("opus key should be removed when the provider has no opus model") } } func TestApplyCreatesMissingSettingsFile(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "settings.json") mgr := New(path, filepath.Join(dir, "backups")) if _, err := mgr.Apply(ApplyOptions{BaseURL: "http://127.0.0.1:1", Token: "t", Models: config.Models{Default: "m"}}); err != nil { t.Fatalf("Apply: %v", err) } if _, err := os.Stat(path); err != nil { t.Fatalf("settings.json not created: %v", err) } } func TestBackupsReturnsEmptySliceNotNil(t *testing.T) { dir := t.TempDir() mgr := New(filepath.Join(dir, "settings.json"), filepath.Join(dir, "backups")) backups, err := mgr.Backups() if err != nil { t.Fatalf("Backups: %v", err) } if backups == nil { t.Fatal("Backups returned nil, which marshals to JSON null and breaks the UI") } if len(backups) != 0 { t.Errorf("backups = %v, want empty", backups) } }