Something went wrong. Try again.
sloproxy
Something went wrong. Try again.
27 kB · 633 lines
Go
at commit e8e39526
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634package diff
import ( "strings" "testing"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config" sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config")
func TestBuildConfigChangeDetails(t *testing.T) { oldCfg := &config.Config{ Port: 8080, AuthDir: "/tmp/auth-old", GeminiKey: []config.GeminiKey{ {APIKey: "old", BaseURL: "http://old", ExcludedModels: []string{"old-model"}}, }, RemoteManagement: config.RemoteManagement{ AllowRemote: false, SecretKey: "old", DisableControlPanel: false, DisableAutoUpdatePanel: false, PanelGitHubRepository: "repo-old", }, OAuthExcludedModels: map[string][]string{ "providerA": {"m1"}, }, OpenAICompatibility: []config.OpenAICompatibility{ { Name: "compat-a", APIKeyEntries: []config.OpenAICompatibilityAPIKey{ {APIKey: "k1"}, }, Models: []config.OpenAICompatibilityModel{{Name: "m1"}}, }, }, }
newCfg := &config.Config{ Port: 9090, AuthDir: "/tmp/auth-new", GeminiKey: []config.GeminiKey{ {APIKey: "old", BaseURL: "http://old", ExcludedModels: []string{"old-model", "extra"}}, }, RemoteManagement: config.RemoteManagement{ AllowRemote: true, SecretKey: "new", DisableControlPanel: true, DisableAutoUpdatePanel: true, PanelGitHubRepository: "repo-new", }, OAuthExcludedModels: map[string][]string{ "providerA": {"m1", "m2"}, "providerB": {"x"}, }, OpenAICompatibility: []config.OpenAICompatibility{ { Name: "compat-a", APIKeyEntries: []config.OpenAICompatibilityAPIKey{ {APIKey: "k1"}, }, Models: []config.OpenAICompatibilityModel{{Name: "m1"}, {Name: "m2"}}, }, { Name: "compat-b", APIKeyEntries: []config.OpenAICompatibilityAPIKey{ {APIKey: "k2"}, }, }, }, }
details := BuildConfigChangeDetails(oldCfg, newCfg)
expectContains(t, details, "port: 8080 -> 9090") expectContains(t, details, "auth-dir: /tmp/auth-old -> /tmp/auth-new") expectContains(t, details, "gemini[0].excluded-models: updated (1 -> 2 entries)") expectContains(t, details, "remote-management.allow-remote: false -> true") expectContains(t, details, "remote-management.disable-auto-update-panel: false -> true") expectContains(t, details, "remote-management.secret-key: updated") expectContains(t, details, "oauth-excluded-models[providera]: updated (1 -> 2 entries)") expectContains(t, details, "oauth-excluded-models[providerb]: added (1 entries)") expectContains(t, details, "openai-compatibility:") expectContains(t, details, " provider added: compat-b (api-keys=1, models=0)") expectContains(t, details, " provider updated: compat-a (models 1 -> 2)")}
func TestBuildConfigChangeDetails_NoChanges(t *testing.T) { cfg := &config.Config{ Port: 8080, } if details := BuildConfigChangeDetails(cfg, cfg); len(details) != 0 { t.Fatalf("expected no change entries, got %v", details) }}
func TestBuildConfigChangeDetails_CodexLiveMediaRelay(t *testing.T) { oldCfg := &config.Config{Codex: config.CodexConfig{LiveMediaRelay: config.CodexLiveMediaRelayConfig{ Enabled: false, MaxSessions: 16, ICEServers: []config.CodexLiveICEServer{{ URLs: []string{"turn:old.example.com"}, Username: "old-user", Credential: "old-secret", }}, }}} newCfg := &config.Config{Codex: config.CodexConfig{LiveMediaRelay: config.CodexLiveMediaRelayConfig{ Enabled: true, MaxSessions: 32, DisablePrivateRemoteIPs: true, PublicIP: "203.0.113.10", UDPPortMin: 40000, UDPPortMax: 40063, ICEServers: []config.CodexLiveICEServer{{ URLs: []string{"turn:new.example.com"}, Username: "new-user", Credential: "new-secret", }}, }}}
details := BuildConfigChangeDetails(oldCfg, newCfg) expectContains(t, details, "codex.live-media-relay.enabled: false -> true") expectContains(t, details, "codex.live-media-relay.max-sessions: 16 -> 32") expectContains(t, details, "codex.live-media-relay.disable-private-remote-ips: false -> true") expectContains(t, details, "codex.live-media-relay.public-ip: <none> -> 203.0.113.10") expectContains(t, details, "codex.live-media-relay.udp-port-min: 0 -> 40000") expectContains(t, details, "codex.live-media-relay.udp-port-max: 0 -> 40063") expectContains(t, details, "codex.live-media-relay.ice-servers: updated (1 -> 1 entries, credentials redacted)") joined := strings.Join(details, "\n") for _, secret := range []string{"old-secret", "new-secret", "old-user", "new-user"} { if strings.Contains(joined, secret) { t.Fatalf("config change details leaked %q: %s", secret, joined) } }}
func TestBuildConfigChangeDetails_GeminiVertexHeaders(t *testing.T) { oldCfg := &config.Config{ GeminiKey: []config.GeminiKey{ {APIKey: "g1", Headers: map[string]string{"H": "1"}, ExcludedModels: []string{"a"}}, }, VertexCompatAPIKey: []config.VertexCompatKey{ {APIKey: "v1", BaseURL: "http://v-old", Models: []config.VertexCompatModel{{Name: "m1"}}}, }, } newCfg := &config.Config{ GeminiKey: []config.GeminiKey{ {APIKey: "g1", Headers: map[string]string{"H": "2"}, ExcludedModels: []string{"a", "b"}}, }, VertexCompatAPIKey: []config.VertexCompatKey{ {APIKey: "v1", BaseURL: "http://v-new", Models: []config.VertexCompatModel{{Name: "m1"}, {Name: "m2"}}}, }, }
details := BuildConfigChangeDetails(oldCfg, newCfg) expectContains(t, details, "gemini[0].headers: updated") expectContains(t, details, "gemini[0].excluded-models: updated (1 -> 2 entries)")}
func TestBuildConfigChangeDetails_ModelPrefixes(t *testing.T) { oldCfg := &config.Config{ GeminiKey: []config.GeminiKey{ {APIKey: "g1", Prefix: "old-g", BaseURL: "http://g", ProxyURL: "http://gp"}, }, ClaudeKey: []config.ClaudeKey{ {APIKey: "c1", Prefix: "old-c", BaseURL: "http://c", ProxyURL: "http://cp"}, }, CodexKey: []config.CodexKey{ {APIKey: "x1", Prefix: "old-x", BaseURL: "http://x", ProxyURL: "http://xp"}, }, VertexCompatAPIKey: []config.VertexCompatKey{ {APIKey: "v1", Prefix: "old-v", BaseURL: "http://v", ProxyURL: "http://vp"}, }, } newCfg := &config.Config{ GeminiKey: []config.GeminiKey{ {APIKey: "g1", Prefix: "new-g", BaseURL: "http://g", ProxyURL: "http://gp"}, }, ClaudeKey: []config.ClaudeKey{ {APIKey: "c1", Prefix: "new-c", BaseURL: "http://c", ProxyURL: "http://cp"}, }, CodexKey: []config.CodexKey{ {APIKey: "x1", Prefix: "new-x", BaseURL: "http://x", ProxyURL: "http://xp"}, }, VertexCompatAPIKey: []config.VertexCompatKey{ {APIKey: "v1", Prefix: "new-v", BaseURL: "http://v", ProxyURL: "http://vp"}, }, }
changes := BuildConfigChangeDetails(oldCfg, newCfg) expectContains(t, changes, "gemini[0].prefix: old-g -> new-g") expectContains(t, changes, "claude[0].prefix: old-c -> new-c") expectContains(t, changes, "codex[0].prefix: old-x -> new-x") expectContains(t, changes, "vertex[0].prefix: old-v -> new-v")}
func TestBuildConfigChangeDetails_XAIKeys(t *testing.T) { oldCfg := &config.Config{XAIKey: []config.XAIKey{{ APIKey: "old-key", Priority: 1, Prefix: "old", BaseURL: "https://old.example.com/v1", ProxyURL: "http://old-proxy", Websockets: false, DisableCooling: false, Headers: map[string]string{"X-Test": "old"}, Models: []config.XAIModel{{Name: "grok-old", Alias: "grok"}}, ExcludedModels: []string{"grok-hidden"}, }}} newCfg := &config.Config{XAIKey: []config.XAIKey{{ APIKey: "new-key", Priority: 2, Prefix: "new", BaseURL: "https://new.example.com/v1", ProxyURL: "http://new-proxy", Websockets: true, DisableCooling: true, Headers: map[string]string{"X-Test": "new"}, Models: []config.XAIModel{{Name: "grok-new", Alias: "grok"}}, ExcludedModels: []string{"grok-other"}, }}}
changes := BuildConfigChangeDetails(oldCfg, newCfg) expectContains(t, changes, "xai[0].base-url: https://old.example.com -> https://new.example.com") expectContains(t, changes, "xai[0].proxy-url: http://old-proxy -> http://new-proxy") expectContains(t, changes, "xai[0].prefix: old -> new") expectContains(t, changes, "xai[0].priority: 1 -> 2") expectContains(t, changes, "xai[0].websockets: false -> true") expectContains(t, changes, "xai[0].disable-cooling: false -> true") expectContains(t, changes, "xai[0].api-key: updated") expectContains(t, changes, "xai[0].headers: updated") expectContains(t, changes, "xai[0].models: updated (1 -> 1 entries)") expectContains(t, changes, "xai[0].excluded-models: updated (1 -> 1 entries)")}
func TestBuildConfigChangeDetails_XAIForceMappingOnly(t *testing.T) { oldCfg := &config.Config{XAIKey: []config.XAIKey{{ APIKey: "xai-key", BaseURL: "https://api.x.ai/v1", Models: []config.XAIModel{{Name: "grok-4.5", Alias: "grok-latest"}}, }}} newCfg := &config.Config{XAIKey: []config.XAIKey{{ APIKey: "xai-key", BaseURL: "https://api.x.ai/v1", Models: []config.XAIModel{{Name: "grok-4.5", Alias: "grok-latest", ForceMapping: true}}, }}}
changes := BuildConfigChangeDetails(oldCfg, newCfg) expectContains(t, changes, "xai[0].models: updated (1 -> 1 entries)")}
func TestBuildConfigChangeDetails_NilSafe(t *testing.T) { if details := BuildConfigChangeDetails(nil, &config.Config{}); len(details) != 0 { t.Fatalf("expected empty change list when old nil, got %v", details) } if details := BuildConfigChangeDetails(&config.Config{}, nil); len(details) != 0 { t.Fatalf("expected empty change list when new nil, got %v", details) }}
func TestBuildConfigChangeDetails_SecretsAndCounts(t *testing.T) { oldCfg := &config.Config{ SDKConfig: sdkconfig.SDKConfig{ APIKeys: []string{"a"}, }, RemoteManagement: config.RemoteManagement{ SecretKey: "", }, } newCfg := &config.Config{ SDKConfig: sdkconfig.SDKConfig{ APIKeys: []string{"a", "b", "c"}, }, RemoteManagement: config.RemoteManagement{ SecretKey: "new-secret", }, }
details := BuildConfigChangeDetails(oldCfg, newCfg) expectContains(t, details, "api-keys count: 1 -> 3") expectContains(t, details, "remote-management.secret-key: created")}
func TestBuildConfigChangeDetails_RedactsEndpointURLs(t *testing.T) { oldCfg := &config.Config{ GeminiKey: []config.GeminiKey{{BaseURL: "https://old-user:old-pass@old.example/v1?token=old-token"}}, RemoteManagement: config.RemoteManagement{ PanelGitHubRepository: "https://old-user:old-pass@old-panel.example/private?token=old-token", }, OpenAICompatibility: []config.OpenAICompatibility{{ BaseURL: "https://old-user:old-pass@old-compat.example/v1?token=old-token", }}, } newCfg := &config.Config{ GeminiKey: []config.GeminiKey{{BaseURL: "https://new-user:new-pass@new.example/v1?token=new-token"}}, RemoteManagement: config.RemoteManagement{ PanelGitHubRepository: "https://new-user:new-pass@new-panel.example/private?token=new-token", }, OpenAICompatibility: []config.OpenAICompatibility{{ BaseURL: "https://new-user:new-pass@new-compat.example/v1?token=new-token", }}, }
details := BuildConfigChangeDetails(oldCfg, newCfg) expectContains(t, details, "gemini[0].base-url: https://old.example -> https://new.example") expectContains(t, details, "remote-management.panel-github-repository: https://old-panel.example -> https://new-panel.example") joined := strings.Join(details, "\n") for _, sensitive := range []string{"old-user", "new-user", "old-pass", "new-pass", "old-token", "new-token", "/private", "/v1"} { if strings.Contains(joined, sensitive) { t.Fatalf("config change details leaked %q: %s", sensitive, joined) } }}
func TestBuildConfigChangeDetails_FlagsAndKeys(t *testing.T) { oldCfg := &config.Config{ Port: 1000, AuthDir: "/old", Debug: false, LoggingToFile: false, UsageStatisticsEnabled: false, DisableCooling: false, SaveCooldownStatus: false, TransientErrorCooldownSeconds: 0, RequestRetry: 1, MaxRetryCredentials: 1, MaxRetryInterval: 1, WebsocketAuth: false, QuotaExceeded: config.QuotaExceeded{SwitchProject: false, SwitchPreviewModel: false, AntigravityCredits: false}, ClaudeKey: []config.ClaudeKey{{APIKey: "c1"}}, CodexKey: []config.CodexKey{{APIKey: "x1"}}, RemoteManagement: config.RemoteManagement{DisableControlPanel: false, PanelGitHubRepository: "old/repo", SecretKey: "keep"}, SDKConfig: sdkconfig.SDKConfig{ RequestLog: false, ProxyURL: "http://old-proxy", APIKeys: []string{"key-1"}, ForceModelPrefix: false, NonStreamKeepAliveInterval: 0, }, } newCfg := &config.Config{ Port: 2000, AuthDir: "/new", Debug: true, LoggingToFile: true, UsageStatisticsEnabled: true, DisableCooling: true, SaveCooldownStatus: true, TransientErrorCooldownSeconds: -1, RequestRetry: 2, MaxRetryCredentials: 3, MaxRetryInterval: 3, WebsocketAuth: true, QuotaExceeded: config.QuotaExceeded{SwitchProject: true, SwitchPreviewModel: true, AntigravityCredits: true}, XAI: config.XAIConfig{InjectXSearch: true}, ClaudeKey: []config.ClaudeKey{ {APIKey: "c1", BaseURL: "http://new", ProxyURL: "http://p", Headers: map[string]string{"H": "1"}, ExcludedModels: []string{"a"}}, {APIKey: "c2"}, }, CodexKey: []config.CodexKey{ {APIKey: "x1", BaseURL: "http://x", ProxyURL: "http://px", Headers: map[string]string{"H": "2"}, ExcludedModels: []string{"b"}}, {APIKey: "x2"}, }, RemoteManagement: config.RemoteManagement{ DisableControlPanel: true, DisableAutoUpdatePanel: true, PanelGitHubRepository: "new/repo", SecretKey: "", }, SDKConfig: sdkconfig.SDKConfig{ RequestLog: true, ProxyURL: "http://new-proxy", APIKeys: []string{" key-1 ", "key-2"}, ForceModelPrefix: true, NonStreamKeepAliveInterval: 5, DisableImageGeneration: config.DisableImageGenerationAll, ClaudeCode: sdkconfig.ClaudeCodeConfig{ DisableCloakingModelList: true, }, }, }
details := BuildConfigChangeDetails(oldCfg, newCfg) expectContains(t, details, "debug: false -> true") expectContains(t, details, "logging-to-file: false -> true") expectContains(t, details, "usage-statistics-enabled: false -> true") expectContains(t, details, "disable-cooling: false -> true") expectContains(t, details, "save-cooldown-status: false -> true") expectContains(t, details, "transient-error-cooldown-seconds: 0 -> -1") expectContains(t, details, "disable-image-generation: false -> true") expectContains(t, details, "claude-code.disable-cloaking-model-list: false -> true") expectContains(t, details, "request-log: false -> true") expectContains(t, details, "request-retry: 1 -> 2") expectContains(t, details, "max-retry-credentials: 1 -> 3") expectContains(t, details, "max-retry-interval: 1 -> 3") expectContains(t, details, "proxy-url: http://old-proxy -> http://new-proxy") expectContains(t, details, "ws-auth: false -> true") expectContains(t, details, "force-model-prefix: false -> true") expectContains(t, details, "nonstream-keepalive-interval: 0 -> 5") expectContains(t, details, "quota-exceeded.switch-project: false -> true") expectContains(t, details, "quota-exceeded.switch-preview-model: false -> true") expectContains(t, details, "quota-exceeded.antigravity-credits: false -> true") expectContains(t, details, "xai.inject-x-search: false -> true") expectContains(t, details, "api-keys count: 1 -> 2") expectContains(t, details, "claude-api-key count: 1 -> 2") expectContains(t, details, "codex-api-key count: 1 -> 2") expectContains(t, details, "remote-management.disable-control-panel: false -> true") expectContains(t, details, "remote-management.disable-auto-update-panel: false -> true") expectContains(t, details, "remote-management.panel-github-repository: old -> new") expectContains(t, details, "remote-management.secret-key: deleted")}
func TestBuildConfigChangeDetails_AllBranches(t *testing.T) { oldCfg := &config.Config{ Port: 1, AuthDir: "/a", Debug: false, LoggingToFile: false, UsageStatisticsEnabled: false, DisableCooling: false, SaveCooldownStatus: false, TransientErrorCooldownSeconds: 0, RequestRetry: 1, MaxRetryCredentials: 1, MaxRetryInterval: 1, WebsocketAuth: false, QuotaExceeded: config.QuotaExceeded{SwitchProject: false, SwitchPreviewModel: false, AntigravityCredits: false}, GeminiKey: []config.GeminiKey{ {APIKey: "g-old", BaseURL: "http://g-old", ProxyURL: "http://gp-old", Headers: map[string]string{"A": "1"}}, }, ClaudeKey: []config.ClaudeKey{ {APIKey: "c-old", BaseURL: "http://c-old", ProxyURL: "http://cp-old", Headers: map[string]string{"H": "1"}, ExcludedModels: []string{"x"}}, }, CodexKey: []config.CodexKey{ {APIKey: "x-old", BaseURL: "http://x-old", ProxyURL: "http://xp-old", Headers: map[string]string{"H": "1"}, ExcludedModels: []string{"x"}}, }, VertexCompatAPIKey: []config.VertexCompatKey{ {APIKey: "v-old", BaseURL: "http://v-old", ProxyURL: "http://vp-old", Headers: map[string]string{"H": "1"}, Models: []config.VertexCompatModel{{Name: "m1"}}}, }, RemoteManagement: config.RemoteManagement{ AllowRemote: false, DisableControlPanel: false, DisableAutoUpdatePanel: false, PanelGitHubRepository: "old/repo", SecretKey: "old", }, SDKConfig: sdkconfig.SDKConfig{ RequestLog: false, ProxyURL: "http://old-proxy", APIKeys: []string{" keyA "}, }, OAuthExcludedModels: map[string][]string{"p1": {"a"}}, OpenAICompatibility: []config.OpenAICompatibility{ { Name: "prov-old", APIKeyEntries: []config.OpenAICompatibilityAPIKey{ {APIKey: "k1"}, }, Models: []config.OpenAICompatibilityModel{{Name: "m1"}}, }, }, } newCfg := &config.Config{ Port: 2, AuthDir: "/b", Debug: true, LoggingToFile: true, UsageStatisticsEnabled: true, DisableCooling: true, SaveCooldownStatus: true, TransientErrorCooldownSeconds: -1, RequestRetry: 2, MaxRetryCredentials: 3, MaxRetryInterval: 3, WebsocketAuth: true, QuotaExceeded: config.QuotaExceeded{SwitchProject: true, SwitchPreviewModel: true, AntigravityCredits: true}, GeminiKey: []config.GeminiKey{ {APIKey: "g-new", BaseURL: "http://g-new", ProxyURL: "http://gp-new", Headers: map[string]string{"A": "2"}, ExcludedModels: []string{"x", "y"}}, }, ClaudeKey: []config.ClaudeKey{ {APIKey: "c-new", BaseURL: "http://c-new", ProxyURL: "http://cp-new", Headers: map[string]string{"H": "2"}, ExcludedModels: []string{"x", "y"}}, }, CodexKey: []config.CodexKey{ {APIKey: "x-new", BaseURL: "http://x-new", ProxyURL: "http://xp-new", Headers: map[string]string{"H": "2"}, ExcludedModels: []string{"x", "y"}}, }, VertexCompatAPIKey: []config.VertexCompatKey{ {APIKey: "v-new", BaseURL: "http://v-new", ProxyURL: "http://vp-new", Headers: map[string]string{"H": "2"}, Models: []config.VertexCompatModel{{Name: "m1"}, {Name: "m2"}}}, }, RemoteManagement: config.RemoteManagement{ AllowRemote: true, DisableControlPanel: true, DisableAutoUpdatePanel: true, PanelGitHubRepository: "new/repo", SecretKey: "", }, SDKConfig: sdkconfig.SDKConfig{ RequestLog: true, ProxyURL: "http://new-proxy", APIKeys: []string{"keyB"}, DisableImageGeneration: config.DisableImageGenerationAll, }, OAuthExcludedModels: map[string][]string{"p1": {"b", "c"}, "p2": {"d"}}, OpenAICompatibility: []config.OpenAICompatibility{ { Name: "prov-old", APIKeyEntries: []config.OpenAICompatibilityAPIKey{ {APIKey: "k1"}, {APIKey: "k2"}, }, Models: []config.OpenAICompatibilityModel{{Name: "m1"}, {Name: "m2"}}, }, { Name: "prov-new", APIKeyEntries: []config.OpenAICompatibilityAPIKey{{APIKey: "k3"}}, }, }, }
changes := BuildConfigChangeDetails(oldCfg, newCfg) expectContains(t, changes, "port: 1 -> 2") expectContains(t, changes, "auth-dir: /a -> /b") expectContains(t, changes, "debug: false -> true") expectContains(t, changes, "logging-to-file: false -> true") expectContains(t, changes, "usage-statistics-enabled: false -> true") expectContains(t, changes, "disable-cooling: false -> true") expectContains(t, changes, "save-cooldown-status: false -> true") expectContains(t, changes, "transient-error-cooldown-seconds: 0 -> -1") expectContains(t, changes, "disable-image-generation: false -> true") expectContains(t, changes, "request-retry: 1 -> 2") expectContains(t, changes, "max-retry-credentials: 1 -> 3") expectContains(t, changes, "max-retry-interval: 1 -> 3") expectContains(t, changes, "proxy-url: http://old-proxy -> http://new-proxy") expectContains(t, changes, "ws-auth: false -> true") expectContains(t, changes, "quota-exceeded.switch-project: false -> true") expectContains(t, changes, "quota-exceeded.switch-preview-model: false -> true") expectContains(t, changes, "quota-exceeded.antigravity-credits: false -> true") expectContains(t, changes, "api-keys: values updated (count unchanged, redacted)") expectContains(t, changes, "gemini[0].base-url: http://g-old -> http://g-new") expectContains(t, changes, "gemini[0].proxy-url: http://gp-old -> http://gp-new") expectContains(t, changes, "gemini[0].api-key: updated") expectContains(t, changes, "gemini[0].headers: updated") expectContains(t, changes, "gemini[0].excluded-models: updated (0 -> 2 entries)") expectContains(t, changes, "claude[0].base-url: http://c-old -> http://c-new") expectContains(t, changes, "claude[0].proxy-url: http://cp-old -> http://cp-new") expectContains(t, changes, "claude[0].api-key: updated") expectContains(t, changes, "claude[0].headers: updated") expectContains(t, changes, "claude[0].excluded-models: updated (1 -> 2 entries)") expectContains(t, changes, "codex[0].base-url: http://x-old -> http://x-new") expectContains(t, changes, "codex[0].proxy-url: http://xp-old -> http://xp-new") expectContains(t, changes, "codex[0].api-key: updated") expectContains(t, changes, "codex[0].headers: updated") expectContains(t, changes, "codex[0].excluded-models: updated (1 -> 2 entries)") expectContains(t, changes, "vertex[0].base-url: http://v-old -> http://v-new") expectContains(t, changes, "vertex[0].proxy-url: http://vp-old -> http://vp-new") expectContains(t, changes, "vertex[0].api-key: updated") expectContains(t, changes, "vertex[0].models: updated (1 -> 2 entries)") expectContains(t, changes, "vertex[0].headers: updated") expectContains(t, changes, "oauth-excluded-models[p1]: updated (1 -> 2 entries)") expectContains(t, changes, "oauth-excluded-models[p2]: added (1 entries)") expectContains(t, changes, "remote-management.allow-remote: false -> true") expectContains(t, changes, "remote-management.disable-control-panel: false -> true") expectContains(t, changes, "remote-management.disable-auto-update-panel: false -> true") expectContains(t, changes, "remote-management.panel-github-repository: old -> new") expectContains(t, changes, "remote-management.secret-key: deleted") expectContains(t, changes, "openai-compatibility:")}
func TestFormatProxyURL(t *testing.T) { tests := []struct { name string in string want string }{ {name: "empty", in: "", want: "<none>"}, {name: "invalid", in: "http://[::1", want: "<redacted>"}, {name: "fullURLRedactsUserinfoAndPath", in: "http://user:pass@example.com:8080/path?x=1#frag", want: "http://example.com:8080"}, {name: "socks5RedactsUserinfoAndPath", in: "socks5://user:pass@192.168.1.1:1080/path?x=1", want: "socks5://192.168.1.1:1080"}, {name: "socks5HostPort", in: "socks5://proxy.example.com:1080/", want: "socks5://proxy.example.com:1080"}, {name: "hostPortNoScheme", in: "example.com:1234/path?x=1", want: "example.com:1234"}, {name: "relativePathRedacted", in: "/just/path", want: "<redacted>"}, {name: "schemeAndHost", in: "https://example.com", want: "https://example.com"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := formatProxyURL(tt.in); got != tt.want { t.Fatalf("expected %q, got %q", tt.want, got) } }) }}
func TestBuildConfigChangeDetails_RemoteManagementSecretUpdated(t *testing.T) { oldCfg := &config.Config{ RemoteManagement: config.RemoteManagement{ SecretKey: "old", }, } newCfg := &config.Config{ RemoteManagement: config.RemoteManagement{ SecretKey: "new", }, }
changes := BuildConfigChangeDetails(oldCfg, newCfg) expectContains(t, changes, "remote-management.secret-key: updated")}
func TestBuildConfigChangeDetails_CountBranches(t *testing.T) { oldCfg := &config.Config{} newCfg := &config.Config{ GeminiKey: []config.GeminiKey{{APIKey: "g"}}, ClaudeKey: []config.ClaudeKey{{APIKey: "c"}}, CodexKey: []config.CodexKey{{APIKey: "c"}}, XAIKey: []config.XAIKey{{APIKey: "x"}}, VertexCompatAPIKey: []config.VertexCompatKey{ {APIKey: "v", BaseURL: "http://v"}, }, }
changes := BuildConfigChangeDetails(oldCfg, newCfg) expectContains(t, changes, "gemini-api-key count: 0 -> 1") expectContains(t, changes, "claude-api-key count: 0 -> 1") expectContains(t, changes, "codex-api-key count: 0 -> 1") expectContains(t, changes, "xai-api-key count: 0 -> 1") expectContains(t, changes, "vertex-api-key count: 0 -> 1")}
func TestTrimStrings(t *testing.T) { out := trimStrings([]string{" a ", "b", " c"}) if len(out) != 3 || out[0] != "a" || out[1] != "b" || out[2] != "c" { t.Fatalf("unexpected trimmed strings: %v", out) }}