package pluginapi import ( "context" "encoding/json" "net/http" "net/url" "strings" "testing" ) type compileTimePlugin struct{} var _ ModelRegistrar = (*compileTimePlugin)(nil) var _ ModelProvider = (*compileTimePlugin)(nil) var _ AuthProvider = (*compileTimePlugin)(nil) var _ FrontendAuthProvider = (*compileTimePlugin)(nil) var _ Scheduler = (*compileTimePlugin)(nil) var _ ModelRouter = (*compileTimePlugin)(nil) var _ ProviderExecutor = (*compileTimePlugin)(nil) var _ HostHTTPClient = (*compileTimePlugin)(nil) var _ RequestTranslator = (*compileTimePlugin)(nil) var _ RequestNormalizer = (*compileTimePlugin)(nil) var _ ResponseTranslator = (*compileTimePlugin)(nil) var _ ResponseNormalizer = (*compileTimePlugin)(nil) var _ RequestInterceptor = (*compileTimePlugin)(nil) var _ RequestLifecyclePlugin = (*compileTimePlugin)(nil) var _ ResponseInterceptor = (*compileTimePlugin)(nil) var _ StreamChunkInterceptor = (*compileTimePlugin)(nil) var _ WebSocketResponseObserver = (*compileTimePlugin)(nil) var _ ThinkingApplier = (*compileTimePlugin)(nil) var _ UsagePlugin = (*compileTimePlugin)(nil) var _ CommandLinePlugin = (*compileTimePlugin)(nil) var _ ManagementAPI = (*compileTimePlugin)(nil) var _ ManagementHandler = (*compileTimePlugin)(nil) var _ QuotaProvider = (*compileTimePlugin)(nil) func TestMetadataConfigFieldsExposePluginSchema(t *testing.T) { meta := Metadata{ Name: "example", Version: "1.0.0", Author: "test", GitHubRepository: "https://github.com/router-for-me/CLIProxyAPI", Logo: "https://example.com/logo.svg", ConfigFields: []ConfigField{{ Name: "mode", Type: ConfigFieldTypeEnum, EnumValues: []string{"safe", "fast"}, Description: "Execution mode.", }}, } if meta.Logo == "" || len(meta.ConfigFields) != 1 { t.Fatalf("metadata missing logo or config fields: %#v", meta) } } func TestAuthParseResponseSupportsMultipleAuths(t *testing.T) { resp := AuthParseResponse{ Handled: true, Auth: AuthData{ Provider: "gemini-cli", ID: "primary.json", }, Auths: []AuthData{ {Provider: "gemini-cli", ID: "primary.json"}, {Provider: "gemini-cli", ID: "primary-project-a.json"}, }, } raw, errMarshal := json.Marshal(resp) if errMarshal != nil { t.Fatalf("Marshal() error = %v", errMarshal) } var decoded AuthParseResponse if errUnmarshal := json.Unmarshal(raw, &decoded); errUnmarshal != nil { t.Fatalf("Unmarshal() error = %v", errUnmarshal) } if !decoded.Handled || len(decoded.Auths) != 2 || decoded.Auths[1].ID != "primary-project-a.json" { t.Fatalf("decoded response = %#v, want two auths", decoded) } if decoded.Auth.ID != "primary.json" { t.Fatalf("decoded Auth.ID = %q, want primary.json", decoded.Auth.ID) } } func TestAuthLoginPollResponseSupportsMultipleAuths(t *testing.T) { resp := AuthLoginPollResponse{ Status: AuthLoginStatusSuccess, Auth: AuthData{ Provider: "gemini-cli", ID: "primary.json", }, Auths: []AuthData{ {Provider: "gemini-cli", ID: "primary.json"}, {Provider: "gemini-cli", ID: "primary-project-a.json"}, }, } raw, errMarshal := json.Marshal(resp) if errMarshal != nil { t.Fatalf("Marshal() error = %v", errMarshal) } var decoded AuthLoginPollResponse if errUnmarshal := json.Unmarshal(raw, &decoded); errUnmarshal != nil { t.Fatalf("Unmarshal() error = %v", errUnmarshal) } if decoded.Status != AuthLoginStatusSuccess || len(decoded.Auths) != 2 { t.Fatalf("decoded response = %#v, want success with two auths", decoded) } } func TestResourceRouteMenuFieldsExposeManagementUIHints(t *testing.T) { route := ResourceRoute{ Path: "/status", Menu: "Example Status", Description: "Shows example plugin status.", Handler: compileTimePlugin{}, } if route.Menu == "" || route.Description == "" { t.Fatalf("resource route missing menu fields: %#v", route) } } func TestHostInjectedHTTPClientIsNotEncodedInPluginJSON(t *testing.T) { requests := []struct { name string req any dst any }{ { name: "auth login start", req: AuthLoginStartRequest{Provider: "plugin-example", HTTPClient: compileTimePlugin{}}, dst: &AuthLoginStartRequest{}, }, { name: "auth login poll", req: AuthLoginPollRequest{Provider: "plugin-example", HTTPClient: compileTimePlugin{}}, dst: &AuthLoginPollRequest{}, }, { name: "auth refresh", req: AuthRefreshRequest{AuthID: "auth-1", HTTPClient: compileTimePlugin{}}, dst: &AuthRefreshRequest{}, }, { name: "auth model", req: AuthModelRequest{AuthID: "auth-1", HTTPClient: compileTimePlugin{}}, dst: &AuthModelRequest{}, }, { name: "executor request", req: ExecutorRequest{Model: "model-1", HTTPClient: compileTimePlugin{}}, dst: &ExecutorRequest{}, }, { name: "executor http request", req: ExecutorHTTPRequest{AuthID: "auth-1", HTTPClient: compileTimePlugin{}}, dst: &ExecutorHTTPRequest{}, }, } for _, tt := range requests { raw, errMarshal := json.Marshal(tt.req) if errMarshal != nil { t.Fatalf("%s marshal error = %v", tt.name, errMarshal) } if strings.Contains(string(raw), "HTTPClient") { t.Fatalf("%s JSON contains host HTTPClient: %s", tt.name, raw) } withLegacyHTTPClient := append(raw[:len(raw)-1], []byte(`,"HTTPClient":{}}`)...) if errUnmarshal := json.Unmarshal(withLegacyHTTPClient, tt.dst); errUnmarshal != nil { t.Fatalf("%s unmarshal with legacy HTTPClient object error = %v", tt.name, errUnmarshal) } } } func TestHostModelTypesPreserveFields(t *testing.T) { request := HostModelExecutionRequest{ EntryProtocol: "openai", ExitProtocol: "claude", Model: "gpt-test", Stream: true, Body: []byte(`{"input":"hello"}`), Headers: http.Header{"X-Test": []string{"one", "two"}}, Query: url.Values{"alt": []string{"beta"}}, Alt: "chat", ForcedProvider: "gemini", AuthID: "exact-auth-123", } rawRequest, errMarshalRequest := json.Marshal(request) if errMarshalRequest != nil { t.Fatalf("marshal HostModelExecutionRequest: %v", errMarshalRequest) } requestJSON := string(rawRequest) for _, field := range []string{"entry_protocol", "exit_protocol", "model", "stream", "body", "headers", "query", "alt", "forced_provider", "auth_id"} { if !strings.Contains(requestJSON, `"`+field+`"`) { t.Fatalf("HostModelExecutionRequest JSON missing field %q: %s", field, requestJSON) } } var decodedRequest HostModelExecutionRequest if errUnmarshalRequest := json.Unmarshal(rawRequest, &decodedRequest); errUnmarshalRequest != nil { t.Fatalf("unmarshal HostModelExecutionRequest: %v", errUnmarshalRequest) } if decodedRequest.EntryProtocol != request.EntryProtocol || decodedRequest.ExitProtocol != request.ExitProtocol || decodedRequest.Model != request.Model || decodedRequest.Stream != request.Stream || string(decodedRequest.Body) != string(request.Body) || decodedRequest.Headers.Get("X-Test") != "one" || decodedRequest.Query.Get("alt") != "beta" || decodedRequest.Alt != request.Alt || decodedRequest.ForcedProvider != request.ForcedProvider || decodedRequest.AuthID != request.AuthID { t.Fatalf("HostModelExecutionRequest round trip = %#v", decodedRequest) } if got := decodedRequest.Headers.Values("X-Test"); len(got) != 2 || got[1] != "two" { t.Fatalf("HostModelExecutionRequest headers = %#v", decodedRequest.Headers) } response := HostModelExecutionResponse{ StatusCode: http.StatusAccepted, Headers: http.Header{"Content-Type": []string{"application/json"}}, Body: []byte(`{"ok":true}`), } rawResponse, errMarshalResponse := json.Marshal(response) if errMarshalResponse != nil { t.Fatalf("marshal HostModelExecutionResponse: %v", errMarshalResponse) } responseJSON := string(rawResponse) for _, field := range []string{"status_code", "headers", "body"} { if !strings.Contains(responseJSON, `"`+field+`"`) { t.Fatalf("HostModelExecutionResponse JSON missing field %q: %s", field, responseJSON) } } var decodedResponse HostModelExecutionResponse if errUnmarshalResponse := json.Unmarshal(rawResponse, &decodedResponse); errUnmarshalResponse != nil { t.Fatalf("unmarshal HostModelExecutionResponse: %v", errUnmarshalResponse) } if decodedResponse.StatusCode != response.StatusCode || decodedResponse.Headers.Get("Content-Type") != "application/json" || string(decodedResponse.Body) != string(response.Body) { t.Fatalf("HostModelExecutionResponse round trip = %#v", decodedResponse) } streamResponse := HostModelStreamResponse{ StatusCode: http.StatusOK, Headers: http.Header{"Content-Type": []string{"text/event-stream"}}, StreamID: "stream-1", } rawStreamResponse, errMarshalStreamResponse := json.Marshal(streamResponse) if errMarshalStreamResponse != nil { t.Fatalf("marshal HostModelStreamResponse: %v", errMarshalStreamResponse) } streamResponseJSON := string(rawStreamResponse) for _, field := range []string{"status_code", "headers", "stream_id"} { if !strings.Contains(streamResponseJSON, `"`+field+`"`) { t.Fatalf("HostModelStreamResponse JSON missing field %q: %s", field, streamResponseJSON) } } var decodedStreamResponse HostModelStreamResponse if errUnmarshalStreamResponse := json.Unmarshal(rawStreamResponse, &decodedStreamResponse); errUnmarshalStreamResponse != nil { t.Fatalf("unmarshal HostModelStreamResponse: %v", errUnmarshalStreamResponse) } if decodedStreamResponse.StatusCode != streamResponse.StatusCode || decodedStreamResponse.Headers.Get("Content-Type") != "text/event-stream" || decodedStreamResponse.StreamID != streamResponse.StreamID { t.Fatalf("HostModelStreamResponse round trip = %#v", decodedStreamResponse) } readRequest := HostModelStreamReadRequest{StreamID: "stream-1"} rawReadRequest, errMarshalReadRequest := json.Marshal(readRequest) if errMarshalReadRequest != nil { t.Fatalf("marshal HostModelStreamReadRequest: %v", errMarshalReadRequest) } if !strings.Contains(string(rawReadRequest), `"stream_id"`) { t.Fatalf("HostModelStreamReadRequest JSON missing stream_id: %s", rawReadRequest) } var decodedReadRequest HostModelStreamReadRequest if errUnmarshalReadRequest := json.Unmarshal(rawReadRequest, &decodedReadRequest); errUnmarshalReadRequest != nil { t.Fatalf("unmarshal HostModelStreamReadRequest: %v", errUnmarshalReadRequest) } if decodedReadRequest.StreamID != readRequest.StreamID { t.Fatalf("HostModelStreamReadRequest round trip = %#v", decodedReadRequest) } readResponse := HostModelStreamReadResponse{ Payload: []byte("data: test\n\n"), Error: "temporary stream error", Done: true, } rawReadResponse, errMarshalReadResponse := json.Marshal(readResponse) if errMarshalReadResponse != nil { t.Fatalf("marshal HostModelStreamReadResponse: %v", errMarshalReadResponse) } readResponseJSON := string(rawReadResponse) for _, field := range []string{"payload", "error", "done"} { if !strings.Contains(readResponseJSON, `"`+field+`"`) { t.Fatalf("HostModelStreamReadResponse JSON missing field %q: %s", field, readResponseJSON) } } var decodedReadResponse HostModelStreamReadResponse if errUnmarshalReadResponse := json.Unmarshal(rawReadResponse, &decodedReadResponse); errUnmarshalReadResponse != nil { t.Fatalf("unmarshal HostModelStreamReadResponse: %v", errUnmarshalReadResponse) } if string(decodedReadResponse.Payload) != string(readResponse.Payload) || decodedReadResponse.Error != readResponse.Error || decodedReadResponse.Done != readResponse.Done { t.Fatalf("HostModelStreamReadResponse round trip = %#v", decodedReadResponse) } closeRequest := HostModelStreamCloseRequest{StreamID: "stream-1"} rawCloseRequest, errMarshalCloseRequest := json.Marshal(closeRequest) if errMarshalCloseRequest != nil { t.Fatalf("marshal HostModelStreamCloseRequest: %v", errMarshalCloseRequest) } if !strings.Contains(string(rawCloseRequest), `"stream_id"`) { t.Fatalf("HostModelStreamCloseRequest JSON missing stream_id: %s", rawCloseRequest) } var decodedCloseRequest HostModelStreamCloseRequest if errUnmarshalCloseRequest := json.Unmarshal(rawCloseRequest, &decodedCloseRequest); errUnmarshalCloseRequest != nil { t.Fatalf("unmarshal HostModelStreamCloseRequest: %v", errUnmarshalCloseRequest) } if decodedCloseRequest.StreamID != closeRequest.StreamID { t.Fatalf("HostModelStreamCloseRequest round trip = %#v", decodedCloseRequest) } } func TestSchedulerTypesExposeRoutingFields(t *testing.T) { request := SchedulerPickRequest{ Plugin: Metadata{Name: "scheduler-plugin"}, Provider: "openai", Providers: []string{"openai", "gemini"}, Model: "gpt-test", Stream: true, Options: SchedulerOptions{ Headers: map[string][]string{"X-Test": []string{"1"}}, Metadata: map[string]any{"tenant": "demo"}, }, Candidates: []SchedulerAuthCandidate{{ ID: "auth-1", Provider: "openai", Priority: 10, Status: "ready", Attributes: map[string]string{"region": "us"}, Metadata: map[string]any{"load": float64(0.5)}, }}, } response := SchedulerPickResponse{ AuthID: request.Candidates[0].ID, DelegateBuiltin: SchedulerBuiltinRoundRobin, Handled: true, } if request.Plugin.Name != "scheduler-plugin" { t.Fatalf("Plugin.Name = %q", request.Plugin.Name) } if request.Provider != "openai" { t.Fatalf("Provider = %q", request.Provider) } if len(request.Providers) != 2 || request.Providers[1] != "gemini" { t.Fatalf("Providers = %#v", request.Providers) } if request.Model != "gpt-test" { t.Fatalf("Model = %q", request.Model) } if !request.Stream { t.Fatalf("Stream = %v", request.Stream) } if got := request.Options.Headers["X-Test"]; len(got) != 1 || got[0] != "1" { t.Fatalf("Options.Headers = %#v", request.Options.Headers) } if request.Options.Metadata["tenant"] != "demo" { t.Fatalf("Options.Metadata = %#v", request.Options.Metadata) } if len(request.Candidates) != 1 { t.Fatalf("Candidates = %#v", request.Candidates) } candidate := request.Candidates[0] if candidate.ID != "auth-1" || candidate.Provider != "openai" || candidate.Priority != 10 || candidate.Status != "ready" { t.Fatalf("Candidate = %#v", candidate) } if candidate.Attributes["region"] != "us" { t.Fatalf("Candidate.Attributes = %#v", candidate.Attributes) } if candidate.Metadata["load"] != float64(0.5) { t.Fatalf("Candidate.Metadata = %#v", candidate.Metadata) } if response.AuthID != "auth-1" || response.DelegateBuiltin != SchedulerBuiltinRoundRobin || !response.Handled { t.Fatalf("SchedulerPickResponse = %#v", response) } } func TestModelRouteTypesExposeRoutingFields(t *testing.T) { request := ModelRouteRequest{ Plugin: Metadata{Name: "router-plugin"}, PluginID: "router-plugin-id", SourceFormat: "anthropic", RequestedModel: "claude-sonnet", Stream: true, Headers: http.Header{"X-Test": []string{"1"}}, Query: url.Values{"beta": []string{"true"}}, Body: []byte(`{"model":"claude-sonnet"}`), Metadata: map[string]any{"tenant": "demo"}, } response := ModelRouteResponse{ Handled: true, TargetKind: ModelRouteTargetExecutor, Target: "claude-websearch-plugin", Reason: "typed websearch", } if request.Plugin.Name != "router-plugin" { t.Fatalf("Plugin.Name = %q", request.Plugin.Name) } if request.PluginID != "router-plugin-id" { t.Fatalf("PluginID = %q", request.PluginID) } if request.SourceFormat != "anthropic" || request.RequestedModel != "claude-sonnet" || !request.Stream { t.Fatalf("request main fields = %#v", request) } if request.Headers.Get("X-Test") != "1" { t.Fatalf("Headers = %#v", request.Headers) } if request.Query.Get("beta") != "true" { t.Fatalf("Query = %#v", request.Query) } if string(request.Body) != `{"model":"claude-sonnet"}` { t.Fatalf("Body = %q", request.Body) } if request.Metadata["tenant"] != "demo" { t.Fatalf("Metadata = %#v", request.Metadata) } if !response.Handled || response.Target != "claude-websearch-plugin" || response.Reason != "typed websearch" { t.Fatalf("ModelRouteResponse = %#v", response) } } func (compileTimePlugin) RegisterModels(context.Context, ModelRegistrationRequest) (ModelRegistrationResponse, error) { return ModelRegistrationResponse{}, nil } func (compileTimePlugin) StaticModels(context.Context, StaticModelRequest) (ModelResponse, error) { return ModelResponse{}, nil } func (compileTimePlugin) ModelsForAuth(context.Context, AuthModelRequest) (ModelResponse, error) { return ModelResponse{}, nil } func (compileTimePlugin) Identifier() string { return "compile-time" } func (compileTimePlugin) ParseAuth(context.Context, AuthParseRequest) (AuthParseResponse, error) { return AuthParseResponse{}, nil } func (compileTimePlugin) StartLogin(context.Context, AuthLoginStartRequest) (AuthLoginStartResponse, error) { return AuthLoginStartResponse{}, nil } func (compileTimePlugin) PollLogin(context.Context, AuthLoginPollRequest) (AuthLoginPollResponse, error) { return AuthLoginPollResponse{}, nil } func (compileTimePlugin) RefreshAuth(context.Context, AuthRefreshRequest) (AuthRefreshResponse, error) { return AuthRefreshResponse{}, nil } func (compileTimePlugin) Authenticate(context.Context, FrontendAuthRequest) (FrontendAuthResponse, error) { return FrontendAuthResponse{}, nil } func (compileTimePlugin) Pick(context.Context, SchedulerPickRequest) (SchedulerPickResponse, error) { return SchedulerPickResponse{}, nil } func (compileTimePlugin) RouteModel(context.Context, ModelRouteRequest) (ModelRouteResponse, error) { return ModelRouteResponse{}, nil } func (compileTimePlugin) Execute(context.Context, ExecutorRequest) (ExecutorResponse, error) { return ExecutorResponse{}, nil } func (compileTimePlugin) ExecuteStream(context.Context, ExecutorRequest) (ExecutorStreamResponse, error) { return ExecutorStreamResponse{}, nil } func (compileTimePlugin) CountTokens(context.Context, ExecutorRequest) (ExecutorResponse, error) { return ExecutorResponse{}, nil } func (compileTimePlugin) HttpRequest(context.Context, ExecutorHTTPRequest) (ExecutorHTTPResponse, error) { return ExecutorHTTPResponse{}, nil } func (compileTimePlugin) Do(context.Context, HTTPRequest) (HTTPResponse, error) { return HTTPResponse{}, nil } func (compileTimePlugin) DoStream(context.Context, HTTPRequest) (HTTPStreamResponse, error) { return HTTPStreamResponse{}, nil } func (compileTimePlugin) TranslateRequest(context.Context, RequestTransformRequest) (PayloadResponse, error) { return PayloadResponse{}, nil } func (compileTimePlugin) NormalizeRequest(context.Context, RequestTransformRequest) (PayloadResponse, error) { return PayloadResponse{}, nil } func (compileTimePlugin) TranslateResponse(context.Context, ResponseTransformRequest) (PayloadResponse, error) { return PayloadResponse{}, nil } func (compileTimePlugin) NormalizeResponse(context.Context, ResponseTransformRequest) (PayloadResponse, error) { return PayloadResponse{}, nil } func (compileTimePlugin) InterceptRequestBeforeAuth(context.Context, RequestInterceptRequest) (RequestInterceptResponse, error) { return RequestInterceptResponse{}, nil } func (compileTimePlugin) InterceptRequestAfterAuth(context.Context, RequestInterceptRequest) (RequestInterceptResponse, error) { return RequestInterceptResponse{}, nil } func (compileTimePlugin) HandleRequestComplete(context.Context, RequestCompletion) error { return nil } func (compileTimePlugin) InterceptResponse(context.Context, ResponseInterceptRequest) (ResponseInterceptResponse, error) { return ResponseInterceptResponse{}, nil } func (compileTimePlugin) InterceptStreamChunk(context.Context, StreamChunkInterceptRequest) (StreamChunkInterceptResponse, error) { return StreamChunkInterceptResponse{}, nil } func (compileTimePlugin) ObserveWebSocketResponseEvent(context.Context, WebSocketResponseEvent) error { return nil } func (compileTimePlugin) ApplyThinking(context.Context, ThinkingApplyRequest) (PayloadResponse, error) { return PayloadResponse{}, nil } func (compileTimePlugin) HandleUsage(context.Context, UsageRecord) {} func (compileTimePlugin) RegisterCommandLine(context.Context, CommandLineRegistrationRequest) (CommandLineRegistrationResponse, error) { return CommandLineRegistrationResponse{}, nil } func (compileTimePlugin) ExecuteCommandLine(context.Context, CommandLineExecutionRequest) (CommandLineExecutionResponse, error) { return CommandLineExecutionResponse{}, nil } func (compileTimePlugin) RegisterManagement(context.Context, ManagementRegistrationRequest) (ManagementRegistrationResponse, error) { return ManagementRegistrationResponse{}, nil } func (compileTimePlugin) HandleManagement(context.Context, ManagementRequest) (ManagementResponse, error) { return ManagementResponse{}, nil } func (compileTimePlugin) DescribeQuota(context.Context, QuotaDescribeRequest) (QuotaDescribeResponse, error) { return QuotaDescribeResponse{}, nil } func (compileTimePlugin) FetchQuota(context.Context, QuotaFetchRequest) (QuotaFetchResponse, error) { return QuotaFetchResponse{}, nil } func (compileTimePlugin) ResetQuota(context.Context, QuotaResetRequest) (QuotaResetResponse, error) { return QuotaResetResponse{}, nil } func TestHostAffinityLookupTypes(t *testing.T) { if HostAffinityStatusBound != "bound" { t.Fatalf("HostAffinityStatusBound = %q", HostAffinityStatusBound) } if HostAffinityStatusUnbound != "unbound" { t.Fatalf("HostAffinityStatusUnbound = %q", HostAffinityStatusUnbound) } if HostAffinityStatusAmbiguous != "ambiguous" { t.Fatalf("HostAffinityStatusAmbiguous = %q", HostAffinityStatusAmbiguous) } if HostAffinityStatusUnsupported != "unsupported" { t.Fatalf("HostAffinityStatusUnsupported = %q", HostAffinityStatusUnsupported) } req := HostAffinityLookupRequest{ Provider: "anthropic", Model: "claude-3-7-sonnet", SessionID: "sess-1", } data, errMarshal := json.Marshal(req) if errMarshal != nil { t.Fatalf("marshal request: %v", errMarshal) } var decodedReq HostAffinityLookupRequest if errUnmarshal := json.Unmarshal(data, &decodedReq); errUnmarshal != nil { t.Fatalf("unmarshal request: %v", errUnmarshal) } if decodedReq != req { t.Fatalf("decoded request = %#v, want %#v", decodedReq, req) } resp := HostAffinityLookupResponse{ Status: HostAffinityStatusBound, AuthIndex: "idx-1", Disabled: true, Unavailable: false, } respData, errRespMarshal := json.Marshal(resp) if errRespMarshal != nil { t.Fatalf("marshal response: %v", errRespMarshal) } var decodedResp HostAffinityLookupResponse if errRespUnmarshal := json.Unmarshal(respData, &decodedResp); errRespUnmarshal != nil { t.Fatalf("unmarshal response: %v", errRespUnmarshal) } if decodedResp.Status != resp.Status || decodedResp.AuthIndex != resp.AuthIndex || decodedResp.Disabled != resp.Disabled { t.Fatalf("decoded response = %#v, want %#v", decodedResp, resp) } } func TestBaseURLInUsageRecordAndHostAuthFileEntry(t *testing.T) { record := UsageRecord{ Provider: "codex", Model: "gpt-5.6-luna", BaseURL: "https://custom-gateway.example.com/v1", } if record.BaseURL != "https://custom-gateway.example.com/v1" { t.Fatalf("UsageRecord.BaseURL = %q, want %q", record.BaseURL, "https://custom-gateway.example.com/v1") } entry := HostAuthFileEntry{ ID: "test-auth", BaseURL: "https://custom-auth.example.com/v1", } data, errMarshal := json.Marshal(entry) if errMarshal != nil { t.Fatalf("marshal HostAuthFileEntry: %v", errMarshal) } if !strings.Contains(string(data), `"base_url":"https://custom-auth.example.com/v1"`) { t.Fatalf("marshaled json does not contain base_url key: %s", string(data)) } var decoded HostAuthFileEntry if errUnmarshal := json.Unmarshal(data, &decoded); errUnmarshal != nil { t.Fatalf("unmarshal HostAuthFileEntry: %v", errUnmarshal) } if decoded.BaseURL != "https://custom-auth.example.com/v1" { t.Fatalf("decoded HostAuthFileEntry.BaseURL = %q, want %q", decoded.BaseURL, "https://custom-auth.example.com/v1") } entryEmpty := HostAuthFileEntry{ID: "test-empty"} emptyData, errEmptyMarshal := json.Marshal(entryEmpty) if errEmptyMarshal != nil { t.Fatalf("marshal empty HostAuthFileEntry: %v", errEmptyMarshal) } if strings.Contains(string(emptyData), "base_url") { t.Fatalf("empty base_url should be omitted, got: %s", string(emptyData)) } } func TestQuotaPayloadJSON(t *testing.T) { // Test camelCase input camelJSON := []byte(`{ "subscription": {"plan":"Pro","tierName":"Tier 1","tierId":"t-1"}, "summary": [ {"key":"credits_used","label":"Credits used","value":1740.28,"unit":"credits","format":"number"}, {"key":"charged","label":"Charged","value":29.61,"format":"currency","currency":"USD"} ], "serverTimeOffsetMs": 100, "groups": [ { "displayName": "Daily Quota", "buckets": [ {"window":"daily","remainingFraction":0.8,"resetTime":"2026-09-13T00:00:00Z","description":"80% left"} ] } ] }`) var respCamel QuotaFetchResponse if err := json.Unmarshal(camelJSON, &respCamel); err != nil { t.Fatalf("unmarshal camelCase JSON failed: %v", err) } if respCamel.Subscription == nil || respCamel.Subscription.TierName != "Tier 1" || respCamel.Subscription.TierID != "t-1" { t.Fatalf("unexpected camel subscription: %+v", respCamel.Subscription) } if len(respCamel.Summary) != 2 || respCamel.Summary[0].Key != "credits_used" || respCamel.Summary[0].Value != 1740.28 || respCamel.Summary[1].Currency != "USD" { t.Fatalf("unexpected camel summary: %+v", respCamel.Summary) } if respCamel.ServerTimeOffsetMs != 100 { t.Fatalf("unexpected server time offset: %d", respCamel.ServerTimeOffsetMs) } if len(respCamel.Groups) != 1 || respCamel.Groups[0].DisplayName != "Daily Quota" { t.Fatalf("unexpected camel groups: %+v", respCamel.Groups) } if len(respCamel.Groups[0].Buckets) != 1 || respCamel.Groups[0].Buckets[0].RemainingFraction != 0.8 || respCamel.Groups[0].Buckets[0].ResetTime != "2026-09-13T00:00:00Z" { t.Fatalf("unexpected camel bucket: %+v", respCamel.Groups[0].Buckets) } // Test snake_case input snakeJSON := []byte(`{ "subscription": {"plan":"Free","tier_name":"Tier Free","tier_id":"t-free"}, "server_time_offset_ms": 200, "groups": [ { "display_name": "Monthly Quota", "buckets": [ {"window":"monthly","remaining_fraction":0.5,"reset_time":"2026-10-01T00:00:00Z","description":"50% left"} ] } ] }`) var respSnake QuotaFetchResponse if err := json.Unmarshal(snakeJSON, &respSnake); err != nil { t.Fatalf("unmarshal snake_case JSON failed: %v", err) } if respSnake.Subscription == nil || respSnake.Subscription.TierName != "Tier Free" || respSnake.Subscription.TierID != "t-free" { t.Fatalf("unexpected snake subscription: %+v", respSnake.Subscription) } if respSnake.ServerTimeOffsetMs != 200 { t.Fatalf("unexpected server time offset: %d", respSnake.ServerTimeOffsetMs) } if len(respSnake.Groups) != 1 || respSnake.Groups[0].DisplayName != "Monthly Quota" { t.Fatalf("unexpected snake groups: %+v", respSnake.Groups) } if len(respSnake.Groups[0].Buckets) != 1 || respSnake.Groups[0].Buckets[0].RemainingFraction != 0.5 || respSnake.Groups[0].Buckets[0].ResetTime != "2026-10-01T00:00:00Z" { t.Fatalf("unexpected snake bucket: %+v", respSnake.Groups[0].Buckets) } } func TestQuotaBucketZeroRemainingFractionPreserved(t *testing.T) { // remainingFraction is 0 (exhausted), while remaining_fraction has a fallback value rawJSON := []byte(`{"remainingFraction":0,"remaining_fraction":0.8}`) var bucket QuotaBucket if err := json.Unmarshal(rawJSON, &bucket); err != nil { t.Fatalf("unmarshal error: %v", err) } if bucket.RemainingFraction != 0 { t.Fatalf("expected remainingFraction 0 to be preserved, got %f", bucket.RemainingFraction) } }