package synthesizer import ( "context" "encoding/json" "os" "path/filepath" "testing" "time" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi" ) func TestNewFileSynthesizer(t *testing.T) { synth := NewFileSynthesizer() if synth == nil { t.Fatal("expected non-nil synthesizer") } } func TestFileSynthesizer_Synthesize_NilContext(t *testing.T) { synth := NewFileSynthesizer() auths, err := synth.Synthesize(nil) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 0 { t.Fatalf("expected empty auths, got %d", len(auths)) } } func TestFileSynthesizer_Synthesize_EmptyAuthDir(t *testing.T) { synth := NewFileSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{}, AuthDir: "", Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 0 { t.Fatalf("expected empty auths, got %d", len(auths)) } } func TestFileSynthesizer_Synthesize_NonExistentDir(t *testing.T) { synth := NewFileSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{}, AuthDir: "/non/existent/path", Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 0 { t.Fatalf("expected empty auths, got %d", len(auths)) } } func TestFileSynthesizer_Synthesize_ValidAuthFile(t *testing.T) { tempDir := t.TempDir() // Create a valid auth file authData := map[string]any{ "type": "claude", "email": "test@example.com", "proxy_url": "http://proxy.local", "prefix": "test-prefix", "headers": map[string]string{ " X-Test ": " value ", "X-Empty": " ", }, "disable_cooling": true, "request_retry": 2, } data, _ := json.Marshal(authData) err := os.WriteFile(filepath.Join(tempDir, "claude-auth.json"), data, 0644) if err != nil { t.Fatalf("failed to write auth file: %v", err) } synth := NewFileSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{}, AuthDir: tempDir, Now: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 1 { t.Fatalf("expected 1 auth, got %d", len(auths)) } if auths[0].Provider != "claude" { t.Errorf("expected provider claude, got %s", auths[0].Provider) } if auths[0].Label != "test@example.com" { t.Errorf("expected label test@example.com, got %s", auths[0].Label) } if auths[0].Prefix != "test-prefix" { t.Errorf("expected prefix test-prefix, got %s", auths[0].Prefix) } if auths[0].ProxyURL != "http://proxy.local" { t.Errorf("expected proxy_url http://proxy.local, got %s", auths[0].ProxyURL) } if got := auths[0].Attributes["header:X-Test"]; got != "value" { t.Errorf("expected header:X-Test value, got %q", got) } if _, ok := auths[0].Attributes["header:X-Empty"]; ok { t.Errorf("expected header:X-Empty to be absent, got %q", auths[0].Attributes["header:X-Empty"]) } if v, ok := auths[0].Metadata["disable_cooling"].(bool); !ok || !v { t.Errorf("expected disable_cooling true, got %v", auths[0].Metadata["disable_cooling"]) } if v, ok := auths[0].Metadata["request_retry"].(float64); !ok || int(v) != 2 { t.Errorf("expected request_retry 2, got %v", auths[0].Metadata["request_retry"]) } if auths[0].Status != coreauth.StatusActive { t.Errorf("expected status active, got %s", auths[0].Status) } } func TestFileSynthesizer_Synthesize_LegacyKimiFingerprintProfile(t *testing.T) { tempDir := t.TempDir() authData := map[string]any{ "type": "kimi", "access_token": "kimi-access-token", "refresh_token": "kimi-refresh-token", "fingerprint-profile": "claude-code-cli", } data, errMarshal := json.Marshal(authData) if errMarshal != nil { t.Fatalf("marshal kimi auth: %v", errMarshal) } if err := os.WriteFile(filepath.Join(tempDir, "kimi-auth.json"), data, 0644); err != nil { t.Fatalf("failed to write kimi auth file: %v", err) } auths, err := NewFileSynthesizer().Synthesize(&SynthesisContext{ Config: &config.Config{}, AuthDir: tempDir, Now: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), IDGenerator: NewStableIDGenerator(), }) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 1 { t.Fatalf("expected 1 auth, got %d", len(auths)) } if auths[0].Provider != "kimi" { t.Fatalf("provider = %q, want kimi", auths[0].Provider) } if got := auths[0].Attributes["fingerprint_profile"]; got != "claude-code-cli" { t.Fatalf("attributes fingerprint_profile = %q, want claude-code-cli", got) } if got, _ := auths[0].Metadata["fingerprint_profile"].(string); got != "claude-code-cli" { t.Fatalf("metadata fingerprint_profile = %q, want claude-code-cli", got) } if _, exists := auths[0].Metadata["fingerprint-profile"]; exists { t.Fatalf("legacy fingerprint-profile was not normalized: %#v", auths[0].Metadata) } } func TestFileSynthesizer_Synthesize_IgnoresGeminiProviderFile(t *testing.T) { tempDir := t.TempDir() authData := map[string]any{ "type": "gemini", "email": "gemini@example.com", } data, _ := json.Marshal(authData) err := os.WriteFile(filepath.Join(tempDir, "gemini-auth.json"), data, 0644) if err != nil { t.Fatalf("failed to write auth file: %v", err) } synth := NewFileSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{}, AuthDir: tempDir, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 0 { t.Fatalf("expected Gemini auth file to be ignored, got %d auths", len(auths)) } } func TestSynthesizeAuthFileExpandsPluginMultiAuths(t *testing.T) { tempDir := t.TempDir() fullPath := filepath.Join(tempDir, "geminicli.json") raw := []byte(`{"type":"gemini-cli","excluded_models":["model-a"],"headers":{"X-Test":"value"}}`) ctx := &SynthesisContext{ Config: &config.Config{}, AuthDir: tempDir, Now: time.Date(2026, 6, 21, 0, 0, 0, 0, time.UTC), PluginAuthParser: multiAuthParserFunc(func(ctx context.Context, req pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error) { if req.Provider != "gemini-cli" || req.Path != fullPath || req.FileName != "geminicli.json" { t.Fatalf("ParseAuths request = %#v, want file context", req) } return []*coreauth.Auth{ { ID: "geminicli.json", Provider: "gemini-cli", Metadata: map[string]any{ "type": "gemini-cli", "headers": map[string]any{ "X-Test": "value", }, }, }, nil, { ID: "geminicli-project-a.json", Provider: "gemini-cli", Metadata: map[string]any{ "type": "gemini-cli", "project_id": "project-a", "headers": map[string]any{ "X-Test": "value", }, }, }, }, true, nil }), } auths, errSynthesize := SynthesizeAuthFile(ctx, fullPath, raw) if errSynthesize != nil { t.Fatalf("SynthesizeAuthFile() error = %v", errSynthesize) } if len(auths) != 2 { t.Fatalf("SynthesizeAuthFile() len = %d, want two plugin auths", len(auths)) } if firstIndex, secondIndex := auths[0].EnsureIndex(), auths[1].EnsureIndex(); firstIndex == "" || firstIndex == secondIndex { t.Fatalf("auth indexes = %q/%q, want distinct non-empty indexes", firstIndex, secondIndex) } for _, auth := range auths { if !coreauth.IsPluginVirtualAuth(auth) { t.Fatalf("auth attributes = %#v, want plugin virtual marker", auth.Attributes) } if auth.Attributes[coreauth.AttributeVirtualSource] != fullPath { t.Fatalf("virtual_source = %q, want %q", auth.Attributes[coreauth.AttributeVirtualSource], fullPath) } if auth.Attributes["path"] != fullPath || auth.Attributes["source"] != fullPath { t.Fatalf("auth attributes = %#v, want source path", auth.Attributes) } if gotHeader := auth.Attributes["header:X-Test"]; gotHeader != "value" { t.Fatalf("header:X-Test = %q, want value", gotHeader) } if gotKind := auth.Attributes["auth_kind"]; gotKind != "oauth" { t.Fatalf("auth_kind = %q, want oauth", gotKind) } } if gotProject := auths[1].Metadata["project_id"]; gotProject != "project-a" { t.Fatalf("project_id = %#v, want project-a", gotProject) } } func TestSynthesizeAuthFileSkipsInvalidPluginAuthWeight(t *testing.T) { tempDir := t.TempDir() fullPath := filepath.Join(tempDir, "plugin.json") ctx := &SynthesisContext{ Config: &config.Config{}, AuthDir: tempDir, Now: time.Date(2026, 6, 21, 0, 0, 0, 0, time.UTC), PluginAuthParser: multiAuthParserFunc(func(context.Context, pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error) { return []*coreauth.Auth{ {ID: "invalid", Provider: "plugin", Attributes: map[string]string{coreauth.AttributeWeight: "1.5"}}, {ID: "valid", Provider: "plugin", Attributes: map[string]string{coreauth.AttributeWeight: "0"}}, }, true, nil }), } auths, errSynthesize := SynthesizeAuthFile(ctx, fullPath, []byte(`{"type":"plugin"}`)) if errSynthesize != nil { t.Fatalf("SynthesizeAuthFile() error = %v", errSynthesize) } if len(auths) != 1 || auths[0].ID != "valid" { t.Fatalf("SynthesizeAuthFile() auths = %#v, want only valid zero-weight auth", auths) } } func TestSynthesizeAuthFileAppliesSourceDisabledToPluginMultiAuths(t *testing.T) { tempDir := t.TempDir() fullPath := filepath.Join(tempDir, "geminicli.json") raw := []byte(`{"type":"gemini-cli","disabled":true}`) ctx := &SynthesisContext{ Config: &config.Config{}, AuthDir: tempDir, Now: time.Date(2026, 6, 21, 0, 0, 0, 0, time.UTC), PluginAuthParser: multiAuthParserFunc(func(context.Context, pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error) { return []*coreauth.Auth{ {ID: "geminicli.json", Provider: "gemini-cli", Metadata: map[string]any{"type": "gemini-cli"}}, {ID: "geminicli-project-a.json", Provider: "gemini-cli", Metadata: map[string]any{"type": "gemini-cli", "project_id": "project-a"}}, }, true, nil }), } auths, errSynthesize := SynthesizeAuthFile(ctx, fullPath, raw) if errSynthesize != nil { t.Fatalf("SynthesizeAuthFile() error = %v", errSynthesize) } if len(auths) != 2 { t.Fatalf("SynthesizeAuthFile() len = %d, want two plugin auths", len(auths)) } for _, auth := range auths { if !auth.Disabled || auth.Status != coreauth.StatusDisabled { t.Fatalf("auth %s disabled/status = %v/%s, want disabled", auth.ID, auth.Disabled, auth.Status) } if got, _ := auth.Metadata["disabled"].(bool); !got { t.Fatalf("auth %s metadata disabled = %#v, want true", auth.ID, auth.Metadata["disabled"]) } } } func TestSynthesizeAuthFilePluginHandledEmptySuppressesBuiltin(t *testing.T) { tempDir := t.TempDir() fullPath := filepath.Join(tempDir, "codex.json") raw := []byte(`{"type":"codex","access_token":"token"}`) ctx := &SynthesisContext{ Config: &config.Config{}, AuthDir: tempDir, Now: time.Date(2026, 6, 21, 0, 0, 0, 0, time.UTC), PluginAuthParser: multiAuthParserFunc(func(context.Context, pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error) { return nil, true, nil }), } auths, errSynthesize := SynthesizeAuthFile(ctx, fullPath, raw) if errSynthesize != nil { t.Fatalf("SynthesizeAuthFile() error = %v", errSynthesize) } if len(auths) != 0 { t.Fatalf("SynthesizeAuthFile() len = %d, want plugin-handled empty result", len(auths)) } } type multiAuthParserFunc func(context.Context, pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error) func (f multiAuthParserFunc) ParseAuth(context.Context, pluginapi.AuthParseRequest) (*coreauth.Auth, bool, error) { return nil, false, nil } func (f multiAuthParserFunc) ParseAuths(ctx context.Context, req pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error) { return f(ctx, req) } func TestFileSynthesizer_Synthesize_SkipsInvalidFiles(t *testing.T) { tempDir := t.TempDir() // Create various invalid files _ = os.WriteFile(filepath.Join(tempDir, "not-json.txt"), []byte("text content"), 0644) _ = os.WriteFile(filepath.Join(tempDir, "invalid.json"), []byte("not valid json"), 0644) _ = os.WriteFile(filepath.Join(tempDir, "empty.json"), []byte(""), 0644) _ = os.WriteFile(filepath.Join(tempDir, "no-type.json"), []byte(`{"email": "test@example.com"}`), 0644) // Create one valid file validData, _ := json.Marshal(map[string]any{"type": "claude", "email": "valid@example.com"}) _ = os.WriteFile(filepath.Join(tempDir, "valid.json"), validData, 0644) synth := NewFileSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{}, AuthDir: tempDir, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 1 { t.Fatalf("only valid auth file should be processed, got %d", len(auths)) } if auths[0].Label != "valid@example.com" { t.Errorf("expected label valid@example.com, got %s", auths[0].Label) } } func TestFileSynthesizer_Synthesize_SkipsDirectories(t *testing.T) { tempDir := t.TempDir() // Create a subdirectory with a json file inside subDir := filepath.Join(tempDir, "subdir.json") err := os.Mkdir(subDir, 0755) if err != nil { t.Fatalf("failed to create subdir: %v", err) } // Create a valid file in root validData, _ := json.Marshal(map[string]any{"type": "claude"}) _ = os.WriteFile(filepath.Join(tempDir, "valid.json"), validData, 0644) synth := NewFileSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{}, AuthDir: tempDir, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 1 { t.Fatalf("expected 1 auth, got %d", len(auths)) } } func TestFileSynthesizer_Synthesize_RelativeID(t *testing.T) { tempDir := t.TempDir() authData := map[string]any{"type": "claude"} data, _ := json.Marshal(authData) err := os.WriteFile(filepath.Join(tempDir, "my-auth.json"), data, 0644) if err != nil { t.Fatalf("failed to write auth file: %v", err) } synth := NewFileSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{}, AuthDir: tempDir, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 1 { t.Fatalf("expected 1 auth, got %d", len(auths)) } // ID should be relative path if auths[0].ID != "my-auth.json" { t.Errorf("expected ID my-auth.json, got %s", auths[0].ID) } } func TestFileSynthesizer_Synthesize_PrefixValidation(t *testing.T) { tests := []struct { name string prefix string wantPrefix string }{ {"valid prefix", "myprefix", "myprefix"}, {"prefix with slashes trimmed", "/myprefix/", "myprefix"}, {"prefix with spaces trimmed", " myprefix ", "myprefix"}, {"prefix with internal slash rejected", "my/prefix", ""}, {"empty prefix", "", ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tempDir := t.TempDir() authData := map[string]any{ "type": "claude", "prefix": tt.prefix, } data, _ := json.Marshal(authData) _ = os.WriteFile(filepath.Join(tempDir, "auth.json"), data, 0644) synth := NewFileSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{}, AuthDir: tempDir, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 1 { t.Fatalf("expected 1 auth, got %d", len(auths)) } if auths[0].Prefix != tt.wantPrefix { t.Errorf("expected prefix %q, got %q", tt.wantPrefix, auths[0].Prefix) } }) } } 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_WeightParsing(t *testing.T) { tests := []struct { name string weight any want string valid bool }{ {name: "number", weight: 5, want: "5", valid: true}, {name: "numeric string", weight: " 3 ", want: "3", valid: true}, {name: "zero excludes", weight: 0, want: "0", valid: true}, {name: "negative excludes", weight: -5, want: "0", valid: true}, {name: "maximum", weight: 1000000, want: "1000000", valid: true}, {name: "fraction rejected", weight: 1.5}, {name: "above maximum rejected", weight: 1000001}, {name: "overflow rejected", weight: "9223372036854775808"}, {name: "invalid string", weight: "heavy"}, } for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { tempDir := t.TempDir() data, errMarshal := json.Marshal(map[string]any{"type": "claude", "weight": testCase.weight}) if errMarshal != nil { t.Fatalf("json.Marshal() error = %v", errMarshal) } if errWrite := os.WriteFile(filepath.Join(tempDir, "auth.json"), data, 0644); errWrite != nil { t.Fatalf("WriteFile() error = %v", errWrite) } ctx := &SynthesisContext{ Config: &config.Config{}, AuthDir: tempDir, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, errSynthesize := NewFileSynthesizer().Synthesize(ctx) if errSynthesize != nil { t.Fatalf("Synthesize() error = %v", errSynthesize) } if !testCase.valid { if len(auths) != 0 { t.Fatalf("auth count = %d, want invalid credential skipped", len(auths)) } if _, errDirect := SynthesizeAuthFile(ctx, filepath.Join(tempDir, "auth.json"), data); errDirect == nil { t.Fatal("SynthesizeAuthFile() error = nil, want weight validation error") } return } if len(auths) != 1 { t.Fatalf("auth count = %d, want 1", len(auths)) } if gotWeight := auths[0].Attributes[coreauth.AttributeWeight]; gotWeight != testCase.want { t.Fatalf("weight = %q, want %q", gotWeight, testCase.want) } }) } } 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 TestFileSynthesizer_Synthesize_OAuthModelAliases(t *testing.T) { tempDir := t.TempDir() authData := map[string]any{ "type": "codex", "email": "codex@example.com", "model_aliases": []map[string]any{ {"name": " gpt-5.3-codex-spark ", "alias": " gpt-5.5 "}, {"name": "gpt-5.3-codex-spark", "alias": "gpt-5.4", "fork": true}, {"name": "gpt-5.3-codex-spark", "alias": "gpt-5.5"}, {"name": "", "alias": "ignored"}, }, } data, _ := json.Marshal(authData) errWriteFile := os.WriteFile(filepath.Join(tempDir, "codex-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)) } got := auths[0].Attributes["model_aliases"] want := `[{"name":"gpt-5.3-codex-spark","alias":"gpt-5.5"},{"name":"gpt-5.3-codex-spark","alias":"gpt-5.4","fork":true}]` if got != want { t.Fatalf("expected model_aliases %q, got %q", want, got) } } func TestFileSynthesizer_Synthesize_IgnoresGeminiOAuthFile(t *testing.T) { tempDir := t.TempDir() authData := map[string]any{ "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) if err != nil { t.Fatalf("failed to write auth file: %v", err) } synth := NewFileSynthesizer() ctx := &SynthesisContext{ Config: &config.Config{}, AuthDir: tempDir, Now: time.Now(), IDGenerator: NewStableIDGenerator(), } auths, err := synth.Synthesize(ctx) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(auths) != 0 { t.Fatalf("expected Gemini auth file to be ignored, got %d auths", len(auths)) } } func TestFileSynthesizer_Synthesize_NoteParsing(t *testing.T) { tests := []struct { name string note any want string hasValue bool }{ { name: "valid string note", note: "hello world", want: "hello world", hasValue: true, }, { name: "string note with whitespace", note: " trimmed note ", want: "trimmed note", hasValue: true, }, { name: "empty string note", note: "", hasValue: false, }, { name: "whitespace only note", note: " ", hasValue: false, }, { name: "non-string note ignored", note: 12345, hasValue: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tempDir := t.TempDir() authData := map[string]any{ "type": "claude", "note": tt.note, } 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["note"] if tt.hasValue { if !ok { t.Fatal("expected note attribute to be set") } if value != tt.want { t.Fatalf("expected note %q, got %q", tt.want, value) } return } if ok { t.Fatalf("expected note attribute to be absent, got %q", value) } }) } }