mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-19 04:40:52 +08:00
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.
This commit is contained in:
@@ -118,8 +118,9 @@ func (s *FileSynthesizer) Synthesize(ctx *SynthesisContext) ([]*coreauth.Auth, e
|
|||||||
case float64:
|
case float64:
|
||||||
a.Attributes["priority"] = strconv.Itoa(int(v))
|
a.Attributes["priority"] = strconv.Itoa(int(v))
|
||||||
case string:
|
case string:
|
||||||
if _, err := strconv.Atoi(v); err == nil {
|
priority := strings.TrimSpace(v)
|
||||||
a.Attributes["priority"] = v
|
if _, errAtoi := strconv.Atoi(priority); errAtoi == nil {
|
||||||
|
a.Attributes["priority"] = priority
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
func TestSynthesizeGeminiVirtualAuths_NilInputs(t *testing.T) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
@@ -533,6 +644,7 @@ func TestFileSynthesizer_Synthesize_MultiProjectGemini(t *testing.T) {
|
|||||||
"type": "gemini",
|
"type": "gemini",
|
||||||
"email": "multi@example.com",
|
"email": "multi@example.com",
|
||||||
"project_id": "project-a, project-b, project-c",
|
"project_id": "project-a, project-b, project-c",
|
||||||
|
"priority": " 10 ",
|
||||||
}
|
}
|
||||||
data, _ := json.Marshal(authData)
|
data, _ := json.Marshal(authData)
|
||||||
err := os.WriteFile(filepath.Join(tempDir, "gemini-multi.json"), data, 0644)
|
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 {
|
if primary.Status != coreauth.StatusDisabled {
|
||||||
t.Errorf("expected primary status disabled, got %s", primary.Status)
|
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
|
// Remaining auths should be virtuals
|
||||||
for i := 1; i < 4; i++ {
|
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 {
|
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"])
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"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"
|
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) {
|
func TestAddConfigHeadersToAttrs(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
65
sdk/cliproxy/service_excluded_models_test.go
Normal file
65
sdk/cliproxy/service_excluded_models_test.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user