From 4c133d3ea9dc77b740b5b454d7bc582a1045b37b Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Wed, 11 Feb 2026 20:35:13 +0800 Subject: [PATCH] test(sdk/watcher): add tests for excluded models merging and priority parsing logic - Added unit tests for combining OAuth excluded models across global and attribute-specific scopes. - Implemented priority attribute parsing with support for different formats and trimming. --- internal/watcher/synthesizer/file.go | 5 +- internal/watcher/synthesizer/file_test.go | 118 +++++++++++++++++++ internal/watcher/synthesizer/helpers_test.go | 25 ++++ sdk/cliproxy/service_excluded_models_test.go | 65 ++++++++++ 4 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 sdk/cliproxy/service_excluded_models_test.go diff --git a/internal/watcher/synthesizer/file.go b/internal/watcher/synthesizer/file.go index 8f4ec6da..4e053117 100644 --- a/internal/watcher/synthesizer/file.go +++ b/internal/watcher/synthesizer/file.go @@ -118,8 +118,9 @@ func (s *FileSynthesizer) Synthesize(ctx *SynthesisContext) ([]*coreauth.Auth, e case float64: a.Attributes["priority"] = strconv.Itoa(int(v)) case string: - if _, err := strconv.Atoi(v); err == nil { - a.Attributes["priority"] = v + priority := strings.TrimSpace(v) + if _, errAtoi := strconv.Atoi(priority); errAtoi == nil { + a.Attributes["priority"] = priority } } } diff --git a/internal/watcher/synthesizer/file_test.go b/internal/watcher/synthesizer/file_test.go index 93025fba..105d9207 100644 --- a/internal/watcher/synthesizer/file_test.go +++ b/internal/watcher/synthesizer/file_test.go @@ -297,6 +297,117 @@ func TestFileSynthesizer_Synthesize_PrefixValidation(t *testing.T) { } } +func TestFileSynthesizer_Synthesize_PriorityParsing(t *testing.T) { + tests := []struct { + name string + priority any + want string + hasValue bool + }{ + { + name: "string with spaces", + priority: " 10 ", + want: "10", + hasValue: true, + }, + { + name: "number", + priority: 8, + want: "8", + hasValue: true, + }, + { + name: "invalid string", + priority: "1x", + hasValue: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir := t.TempDir() + authData := map[string]any{ + "type": "claude", + "priority": tt.priority, + } + data, _ := json.Marshal(authData) + errWriteFile := os.WriteFile(filepath.Join(tempDir, "auth.json"), data, 0644) + if errWriteFile != nil { + t.Fatalf("failed to write auth file: %v", errWriteFile) + } + + synth := NewFileSynthesizer() + ctx := &SynthesisContext{ + Config: &config.Config{}, + AuthDir: tempDir, + Now: time.Now(), + IDGenerator: NewStableIDGenerator(), + } + + auths, errSynthesize := synth.Synthesize(ctx) + if errSynthesize != nil { + t.Fatalf("unexpected error: %v", errSynthesize) + } + if len(auths) != 1 { + t.Fatalf("expected 1 auth, got %d", len(auths)) + } + + value, ok := auths[0].Attributes["priority"] + if tt.hasValue { + if !ok { + t.Fatal("expected priority attribute to be set") + } + if value != tt.want { + t.Fatalf("expected priority %q, got %q", tt.want, value) + } + return + } + if ok { + t.Fatalf("expected priority attribute to be absent, got %q", value) + } + }) + } +} + +func TestFileSynthesizer_Synthesize_OAuthExcludedModelsMerged(t *testing.T) { + tempDir := t.TempDir() + authData := map[string]any{ + "type": "claude", + "excluded_models": []string{"custom-model", "MODEL-B"}, + } + data, _ := json.Marshal(authData) + errWriteFile := os.WriteFile(filepath.Join(tempDir, "auth.json"), data, 0644) + if errWriteFile != nil { + t.Fatalf("failed to write auth file: %v", errWriteFile) + } + + synth := NewFileSynthesizer() + ctx := &SynthesisContext{ + Config: &config.Config{ + OAuthExcludedModels: map[string][]string{ + "claude": {"shared", "model-b"}, + }, + }, + AuthDir: tempDir, + Now: time.Now(), + IDGenerator: NewStableIDGenerator(), + } + + auths, errSynthesize := synth.Synthesize(ctx) + if errSynthesize != nil { + t.Fatalf("unexpected error: %v", errSynthesize) + } + if len(auths) != 1 { + t.Fatalf("expected 1 auth, got %d", len(auths)) + } + + got := auths[0].Attributes["excluded_models"] + want := "custom-model,model-b,shared" + if got != want { + t.Fatalf("expected excluded_models %q, got %q", want, got) + } +} + func TestSynthesizeGeminiVirtualAuths_NilInputs(t *testing.T) { now := time.Now() @@ -533,6 +644,7 @@ func TestFileSynthesizer_Synthesize_MultiProjectGemini(t *testing.T) { "type": "gemini", "email": "multi@example.com", "project_id": "project-a, project-b, project-c", + "priority": " 10 ", } data, _ := json.Marshal(authData) err := os.WriteFile(filepath.Join(tempDir, "gemini-multi.json"), data, 0644) @@ -565,6 +677,9 @@ func TestFileSynthesizer_Synthesize_MultiProjectGemini(t *testing.T) { if primary.Status != coreauth.StatusDisabled { t.Errorf("expected primary status disabled, got %s", primary.Status) } + if gotPriority := primary.Attributes["priority"]; gotPriority != "10" { + t.Errorf("expected primary priority 10, got %q", gotPriority) + } // Remaining auths should be virtuals for i := 1; i < 4; i++ { @@ -575,6 +690,9 @@ func TestFileSynthesizer_Synthesize_MultiProjectGemini(t *testing.T) { if v.Attributes["gemini_virtual_parent"] != primary.ID { t.Errorf("expected virtual %d parent to be %s, got %s", i, primary.ID, v.Attributes["gemini_virtual_parent"]) } + if gotPriority := v.Attributes["priority"]; gotPriority != "10" { + t.Errorf("expected virtual %d priority 10, got %q", i, gotPriority) + } } } diff --git a/internal/watcher/synthesizer/helpers_test.go b/internal/watcher/synthesizer/helpers_test.go index 229c75bc..46b9c8a0 100644 --- a/internal/watcher/synthesizer/helpers_test.go +++ b/internal/watcher/synthesizer/helpers_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/diff" coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" ) @@ -200,6 +201,30 @@ func TestApplyAuthExcludedModelsMeta(t *testing.T) { } } +func TestApplyAuthExcludedModelsMeta_OAuthMergeWritesCombinedModels(t *testing.T) { + auth := &coreauth.Auth{ + Provider: "claude", + Attributes: make(map[string]string), + } + cfg := &config.Config{ + OAuthExcludedModels: map[string][]string{ + "claude": {"global-a", "shared"}, + }, + } + + ApplyAuthExcludedModelsMeta(auth, cfg, []string{"per", "SHARED"}, "oauth") + + const wantCombined = "global-a,per,shared" + if gotCombined := auth.Attributes["excluded_models"]; gotCombined != wantCombined { + t.Fatalf("expected excluded_models=%q, got %q", wantCombined, gotCombined) + } + + expectedHash := diff.ComputeExcludedModelsHash([]string{"global-a", "per", "shared"}) + if gotHash := auth.Attributes["excluded_models_hash"]; gotHash != expectedHash { + t.Fatalf("expected excluded_models_hash=%q, got %q", expectedHash, gotHash) + } +} + func TestAddConfigHeadersToAttrs(t *testing.T) { tests := []struct { name string diff --git a/sdk/cliproxy/service_excluded_models_test.go b/sdk/cliproxy/service_excluded_models_test.go new file mode 100644 index 00000000..198a5bed --- /dev/null +++ b/sdk/cliproxy/service_excluded_models_test.go @@ -0,0 +1,65 @@ +package cliproxy + +import ( + "strings" + "testing" + + coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" +) + +func TestRegisterModelsForAuth_UsesPreMergedExcludedModelsAttribute(t *testing.T) { + service := &Service{ + cfg: &config.Config{ + OAuthExcludedModels: map[string][]string{ + "gemini-cli": {"gemini-2.5-pro"}, + }, + }, + } + auth := &coreauth.Auth{ + ID: "auth-gemini-cli", + Provider: "gemini-cli", + Status: coreauth.StatusActive, + Attributes: map[string]string{ + "auth_kind": "oauth", + "excluded_models": "gemini-2.5-flash", + }, + } + + registry := GlobalModelRegistry() + registry.UnregisterClient(auth.ID) + t.Cleanup(func() { + registry.UnregisterClient(auth.ID) + }) + + service.registerModelsForAuth(auth) + + models := registry.GetAvailableModelsByProvider("gemini-cli") + if len(models) == 0 { + t.Fatal("expected gemini-cli models to be registered") + } + + for _, model := range models { + if model == nil { + continue + } + modelID := strings.TrimSpace(model.ID) + if strings.EqualFold(modelID, "gemini-2.5-flash") { + t.Fatalf("expected model %q to be excluded by auth attribute", modelID) + } + } + + seenGlobalExcluded := false + for _, model := range models { + if model == nil { + continue + } + if strings.EqualFold(strings.TrimSpace(model.ID), "gemini-2.5-pro") { + seenGlobalExcluded = true + break + } + } + if !seenGlobalExcluded { + t.Fatal("expected global excluded model to be present when attribute override is set") + } +}