package config import ( "errors" "os" "path/filepath" "testing" ) func TestSetAliases_CloudModel(t *testing.T) { // Test the SetAliases logic by checking the alias map behavior aliases := map[string]string{ "primary": "kimi-k2.5:cloud", "fast": "kimi-k2.5:cloud", } // Verify fast is set (cloud model behavior) if aliases["fast"] == "" { t.Error("cloud model should have fast alias set") } if aliases["fast"] != aliases["primary"] { t.Errorf("fast should equal primary for auto-set, got fast=%q primary=%q", aliases["fast"], aliases["primary"]) } } func TestSetAliases_LocalModel(t *testing.T) { aliases := map[string]string{ "primary": "llama3.2:latest", } // Simulate local model behavior: fast should be empty delete(aliases, "fast") if aliases["fast"] != "" { t.Error("local model should have empty fast alias") } } func TestSaveAliases_ReplacesNotMerges(t *testing.T) { tmpDir := t.TempDir() setTestHome(t, tmpDir) // First save with both primary and fast initial := map[string]string{ "primary": "cloud-model", "fast": "cloud-model", } if err := SaveAliases("claude", initial); err != nil { t.Fatalf("failed to save initial aliases: %v", err) } // Verify both are saved loaded, err := LoadIntegration("claude") if err != nil { t.Fatalf("failed to load: %v", err) } if loaded.Aliases["fast"] != "cloud-model" { t.Errorf("expected fast=cloud-model, got %q", loaded.Aliases["fast"]) } // Now save without fast (simulating switch to local model) updated := map[string]string{ "primary": "local-model", // fast intentionally missing } if err := SaveAliases("claude", updated); err != nil { t.Fatalf("failed to save updated aliases: %v", err) } // Verify fast is GONE (not merged/preserved) loaded, err = LoadIntegration("claude") if err != nil { t.Fatalf("failed to load after update: %v", err) } if loaded.Aliases["fast"] != "" { t.Errorf("fast should be removed after saving without it, got %q", loaded.Aliases["fast"]) } if loaded.Aliases["primary"] != "local-model" { t.Errorf("primary should be updated to local-model, got %q", loaded.Aliases["primary"]) } } func TestSaveAliases_PreservesModels(t *testing.T) { tmpDir := t.TempDir() setTestHome(t, tmpDir) // First save integration with models if err := SaveIntegration("claude", []string{"model1", "model2"}); err != nil { t.Fatalf("failed to save integration: %v", err) } // Then update aliases aliases := map[string]string{"primary": "new-model"} if err := SaveAliases("claude", aliases); err != nil { t.Fatalf("failed to save aliases: %v", err) } // Verify models are preserved loaded, err := LoadIntegration("claude") if err != nil { t.Fatalf("failed to load: %v", err) } if len(loaded.Models) != 2 || loaded.Models[0] != "model1" { t.Errorf("models should be preserved, got %v", loaded.Models) } } // TestSaveAliases_EmptyMap clears all aliases func TestSaveAliases_EmptyMap(t *testing.T) { tmpDir := t.TempDir() setTestHome(t, tmpDir) // Save with aliases if err := SaveAliases("claude", map[string]string{"primary": "model", "fast": "model"}); err != nil { t.Fatalf("failed to save: %v", err) } // Save empty map if err := SaveAliases("claude", map[string]string{}); err != nil { t.Fatalf("failed to save empty: %v", err) } loaded, err := LoadIntegration("claude") if err != nil { t.Fatalf("failed to load: %v", err) } if len(loaded.Aliases) != 0 { t.Errorf("aliases should be empty, got %v", loaded.Aliases) } } // TestSaveAliases_NilMap handles nil gracefully func TestSaveAliases_NilMap(t *testing.T) { tmpDir := t.TempDir() setTestHome(t, tmpDir) // Save with aliases first if err := SaveAliases("claude", map[string]string{"primary": "model"}); err != nil { t.Fatalf("failed to save: %v", err) } // Save nil map - should clear aliases if err := SaveAliases("claude", nil); err != nil { t.Fatalf("failed to save nil: %v", err) } loaded, err := LoadIntegration("claude") if err != nil { t.Fatalf("failed to load: %v", err) } if len(loaded.Aliases) > 0 { t.Errorf("aliases should be nil or empty, got %v", loaded.Aliases) } } // TestSaveAliases_EmptyAppName returns error func TestSaveAliases_EmptyAppName(t *testing.T) { err := SaveAliases("", map[string]string{"primary": "model"}) if err == nil { t.Error("expected error for empty app name") } } func TestSaveAliases_CaseInsensitive(t *testing.T) { tmpDir := t.TempDir() setTestHome(t, tmpDir) if err := SaveAliases("Claude", map[string]string{"primary": "model1"}); err != nil { t.Fatalf("failed to save: %v", err) } // Load with different case loaded, err := LoadIntegration("claude") if err != nil { t.Fatalf("failed to load: %v", err) } if loaded.Aliases["primary"] != "model1" { t.Errorf("expected primary=model1, got %q", loaded.Aliases["primary"]) } // Update with different case if err := SaveAliases("CLAUDE", map[string]string{"primary": "model2"}); err != nil { t.Fatalf("failed to update: %v", err) } loaded, err = LoadIntegration("claude") if err != nil { t.Fatalf("failed to load after update: %v", err) } if loaded.Aliases["primary"] != "model2" { t.Errorf("expected primary=model2, got %q", loaded.Aliases["primary"]) } } // TestSaveAliases_CreatesIntegration creates integration if it doesn't exist func TestSaveAliases_CreatesIntegration(t *testing.T) { tmpDir := t.TempDir() setTestHome(t, tmpDir) // Save aliases for non-existent integration if err := SaveAliases("newintegration", map[string]string{"primary": "model"}); err != nil { t.Fatalf("failed to save: %v", err) } loaded, err := LoadIntegration("newintegration") if err != nil { t.Fatalf("failed to load: %v", err) } if loaded.Aliases["primary"] != "model" { t.Errorf("expected primary=model, got %q", loaded.Aliases["primary"]) } } func TestConfigureAliases_AliasMap(t *testing.T) { t.Run("cloud model auto-sets fast to primary", func(t *testing.T) { aliases := make(map[string]string) aliases["primary"] = "cloud-model" // Simulate cloud model behavior isCloud := true if isCloud { if aliases["fast"] == "" { aliases["fast"] = aliases["primary"] } } if aliases["fast"] != "cloud-model" { t.Errorf("expected fast=cloud-model, got %q", aliases["fast"]) } }) t.Run("cloud model preserves custom fast", func(t *testing.T) { aliases := map[string]string{ "primary": "cloud-model", "fast": "custom-fast-model", } // Simulate cloud model behavior - should preserve existing fast isCloud := true if isCloud { if aliases["fast"] == "" { aliases["fast"] = aliases["primary"] } } if aliases["fast"] != "custom-fast-model" { t.Errorf("expected fast=custom-fast-model (preserved), got %q", aliases["fast"]) } }) t.Run("local model clears fast", func(t *testing.T) { aliases := map[string]string{ "primary": "local-model", "fast": "should-be-cleared", } // Simulate local model behavior isCloud := false if !isCloud { delete(aliases, "fast") } if aliases["fast"] != "" { t.Errorf("expected fast to be cleared, got %q", aliases["fast"]) } }) t.Run("switching cloud to local clears fast", func(t *testing.T) { // Start with cloud config aliases := map[string]string{ "primary": "cloud-model", "fast": "cloud-model", } // Switch to local aliases["primary"] = "local-model" isCloud := false if !isCloud { delete(aliases, "fast") } if aliases["fast"] != "" { t.Errorf("fast should be cleared when switching to local, got %q", aliases["fast"]) } if aliases["primary"] != "local-model" { t.Errorf("primary should be updated, got %q", aliases["primary"]) } }) t.Run("switching local to cloud sets fast", func(t *testing.T) { // Start with local config (no fast) aliases := map[string]string{ "primary": "local-model", } // Switch to cloud aliases["primary"] = "cloud-model" isCloud := true if isCloud { if aliases["fast"] == "" { aliases["fast"] = aliases["primary"] } } if aliases["fast"] != "cloud-model" { t.Errorf("fast should be set when switching to cloud, got %q", aliases["fast"]) } }) } func TestSetAliases_PrefixMapping(t *testing.T) { // This tests the expected mapping without needing a real client aliases := map[string]string{ "primary": "my-cloud-model", "fast": "my-fast-model", } expectedMappings := map[string]string{ "claude-sonnet-": aliases["primary"], "claude-haiku-": aliases["fast"], } if expectedMappings["claude-sonnet-"] != "my-cloud-model" { t.Errorf("claude-sonnet- should map to primary") } if expectedMappings["claude-haiku-"] != "my-fast-model" { t.Errorf("claude-haiku- should map to fast") } } func TestSetAliases_LocalDeletesPrefixes(t *testing.T) { aliases := map[string]string{ "primary": "local-model", // fast is empty/missing - indicates local model } prefixesToDelete := []string{"claude-sonnet-", "claude-haiku-"} // Verify the logic: when fast is empty, we should delete if aliases["fast"] != "" { t.Error("fast should be empty for local model") } // Verify we have the right prefixes to delete if len(prefixesToDelete) != 2 { t.Errorf("expected 2 prefixes to delete, got %d", len(prefixesToDelete)) } } // TestAtomicUpdate_ServerFailsConfigNotSaved simulates atomic update behavior func TestAtomicUpdate_ServerFailsConfigNotSaved(t *testing.T) { tmpDir := t.TempDir() setTestHome(t, tmpDir) // Simulate: server fails, config should NOT be saved serverErr := errors.New("server unavailable") if serverErr == nil { t.Error("config should NOT be saved when server fails") } } // TestAtomicUpdate_ServerSucceedsConfigSaved simulates successful atomic update func TestAtomicUpdate_ServerSucceedsConfigSaved(t *testing.T) { tmpDir := t.TempDir() setTestHome(t, tmpDir) // Simulate: server succeeds, config should be saved var serverErr error if serverErr != nil { t.Fatal("server should succeed") } if err := SaveAliases("claude", map[string]string{"primary": "model"}); err != nil { t.Fatalf("saveAliases failed: %v", err) } // Verify it was actually saved loaded, err := LoadIntegration("claude") if err != nil { t.Fatalf("failed to load: %v", err) } if loaded.Aliases["primary"] != "model" { t.Errorf("expected primary=model, got %q", loaded.Aliases["primary"]) } } func TestConfigFile_PreservesUnknownFields(t *testing.T) { tmpDir := t.TempDir() setTestHome(t, tmpDir) // Write config with extra fields configPath := filepath.Join(tmpDir, ".ollama", "config.json") os.MkdirAll(filepath.Dir(configPath), 0o755) // Note: Our config struct only has Integrations, so top-level unknown fields // won't be preserved by our current implementation. This test documents that. initialConfig := `{ "integrations": { "claude": { "models": ["model1"], "aliases": {"primary": "model1"}, "unknownField": "should be lost" } }, "topLevelUnknown": "will be lost" }` os.WriteFile(configPath, []byte(initialConfig), 0o644) // Update aliases if err := SaveAliases("claude", map[string]string{"primary": "model2"}); err != nil { t.Fatalf("failed to save: %v", err) } // Read raw file to check data, _ := os.ReadFile(configPath) content := string(data) // models should be preserved if !contains(content, "model1") { t.Error("models should be preserved") } // primary should be updated if !contains(content, "model2") { t.Error("primary should be updated to model2") } } func contains(s, substr string) bool { return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr)) } func containsHelper(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false } func TestModelNameEdgeCases(t *testing.T) { testCases := []struct { name string model string }{ {"simple", "llama3.2"}, {"with tag", "llama3.2:latest"}, {"with cloud tag", "kimi-k2.5:cloud"}, {"with namespace", "library/llama3.2"}, {"with dots", "glm-4.7-flash"}, {"with numbers", "qwen3:8b"}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { tmpDir := t.TempDir() setTestHome(t, tmpDir) aliases := map[string]string{"primary": tc.model} if err := SaveAliases("claude", aliases); err != nil { t.Fatalf("failed to save model %q: %v", tc.model, err) } loaded, err := LoadIntegration("claude") if err != nil { t.Fatalf("failed to load: %v", err) } if loaded.Aliases["primary"] != tc.model { t.Errorf("expected primary=%q, got %q", tc.model, loaded.Aliases["primary"]) } }) } } func TestSwitchingScenarios(t *testing.T) { t.Run("cloud to local removes fast", func(t *testing.T) { tmpDir := t.TempDir() setTestHome(t, tmpDir) // Initial cloud config if err := SaveAliases("claude", map[string]string{ "primary": "cloud-model", "fast": "cloud-model", }); err != nil { t.Fatal(err) } // Switch to local (no fast) if err := SaveAliases("claude", map[string]string{ "primary": "local-model", }); err != nil { t.Fatal(err) } loaded, _ := LoadIntegration("claude") if loaded.Aliases["fast"] != "" { t.Errorf("fast should be removed, got %q", loaded.Aliases["fast"]) } if loaded.Aliases["primary"] != "local-model" { t.Errorf("primary should be local-model, got %q", loaded.Aliases["primary"]) } }) t.Run("local to cloud adds fast", func(t *testing.T) { tmpDir := t.TempDir() setTestHome(t, tmpDir) // Initial local config if err := SaveAliases("claude", map[string]string{ "primary": "local-model", }); err != nil { t.Fatal(err) } // Switch to cloud (with fast) if err := SaveAliases("claude", map[string]string{ "primary": "cloud-model", "fast": "cloud-model", }); err != nil { t.Fatal(err) } loaded, _ := LoadIntegration("claude") if loaded.Aliases["fast"] != "cloud-model" { t.Errorf("fast should be cloud-model, got %q", loaded.Aliases["fast"]) } }) t.Run("cloud to different cloud updates both", func(t *testing.T) { tmpDir := t.TempDir() setTestHome(t, tmpDir) // Initial cloud config if err := SaveAliases("claude", map[string]string{ "primary": "cloud-model-1", "fast": "cloud-model-1", }); err != nil { t.Fatal(err) } // Switch to different cloud if err := SaveAliases("claude", map[string]string{ "primary": "cloud-model-2", "fast": "cloud-model-2", }); err != nil { t.Fatal(err) } loaded, _ := LoadIntegration("claude") if loaded.Aliases["primary"] != "cloud-model-2" { t.Errorf("primary should be cloud-model-2, got %q", loaded.Aliases["primary"]) } if loaded.Aliases["fast"] != "cloud-model-2" { t.Errorf("fast should be cloud-model-2, got %q", loaded.Aliases["fast"]) } }) } func TestModelsAndAliasesMustStayInSync(t *testing.T) { t.Run("saveAliases followed by saveIntegration keeps them in sync", func(t *testing.T) { tmpDir := t.TempDir() setTestHome(t, tmpDir) // Save aliases with one model if err := SaveAliases("claude", map[string]string{"primary": "model-a"}); err != nil { t.Fatal(err) } // Save integration with same model (this is the pattern we use) if err := SaveIntegration("claude", []string{"model-a"}); err != nil { t.Fatal(err) } loaded, _ := LoadIntegration("claude") if loaded.Aliases["primary"] != loaded.Models[0] { t.Errorf("aliases.primary (%q) != models[0] (%q)", loaded.Aliases["primary"], loaded.Models[0]) } }) t.Run("out of sync config is detectable", func(t *testing.T) { tmpDir := t.TempDir() setTestHome(t, tmpDir) // Simulate out-of-sync state (like manual edit or bug) if err := SaveIntegration("claude", []string{"old-model"}); err != nil { t.Fatal(err) } if err := SaveAliases("claude", map[string]string{"primary": "new-model"}); err != nil { t.Fatal(err) } loaded, _ := LoadIntegration("claude") // They should be different (this is the bug state) if loaded.Models[0] == loaded.Aliases["primary"] { t.Error("expected out-of-sync state for this test") } // The fix: when updating aliases, also update models if err := SaveIntegration("claude", []string{loaded.Aliases["primary"]}); err != nil { t.Fatal(err) } loaded, _ = LoadIntegration("claude") if loaded.Models[0] != loaded.Aliases["primary"] { t.Errorf("after fix: models[0] (%q) should equal aliases.primary (%q)", loaded.Models[0], loaded.Aliases["primary"]) } }) t.Run("updating primary alias updates models too", func(t *testing.T) { tmpDir := t.TempDir() setTestHome(t, tmpDir) // Initial state if err := SaveIntegration("claude", []string{"initial-model"}); err != nil { t.Fatal(err) } if err := SaveAliases("claude", map[string]string{"primary": "initial-model"}); err != nil { t.Fatal(err) } // Update aliases AND models together newAliases := map[string]string{"primary": "updated-model"} if err := SaveAliases("claude", newAliases); err != nil { t.Fatal(err) } if err := SaveIntegration("claude", []string{newAliases["primary"]}); err != nil { t.Fatal(err) } loaded, _ := LoadIntegration("claude") if loaded.Models[0] != "updated-model" { t.Errorf("models[0] should be updated-model, got %q", loaded.Models[0]) } if loaded.Aliases["primary"] != "updated-model" { t.Errorf("aliases.primary should be updated-model, got %q", loaded.Aliases["primary"]) } }) }