internal/pluginhost/quota_provider_test.go
Rama main · réplica de lectura del repositorio original.
package pluginhost
import (
"context"
"testing"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginabi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"
)
type testQuotaProvider struct {
identifier string
describeFn func(context.Context, pluginapi.QuotaDescribeRequest) (pluginapi.QuotaDescribeResponse, error)
fetchFn func(context.Context, pluginapi.QuotaFetchRequest) (pluginapi.QuotaFetchResponse, error)
resetFn func(context.Context, pluginapi.QuotaResetRequest) (pluginapi.QuotaResetResponse, error)
}
func (p *testQuotaProvider) Identifier() string {
return p.identifier
}
func (p *testQuotaProvider) DescribeQuota(ctx context.Context, req pluginapi.QuotaDescribeRequest) (pluginapi.QuotaDescribeResponse, error) {
if p.describeFn != nil {
return p.describeFn(ctx, req)
}
return pluginapi.QuotaDescribeResponse{
SupportedProviders: []string{p.identifier},
DisplayName: "Test Quota Provider",
SupportsReset: true,
}, nil
}
func (p *testQuotaProvider) FetchQuota(ctx context.Context, req pluginapi.QuotaFetchRequest) (pluginapi.QuotaFetchResponse, error) {
if p.fetchFn != nil {
return p.fetchFn(ctx, req)
}
return pluginapi.QuotaFetchResponse{
Subscription: &pluginapi.QuotaSubscription{
Plan: "Pro",
TierName: "Pro Tier",
TierID: "pro-1",
},
ServerTimeOffsetMs: 50,
Groups: []pluginapi.QuotaGroup{
{
DisplayName: "Rolling Limits",
Buckets: []pluginapi.QuotaBucket{
{
Window: "daily",
RemainingFraction: 0.75,
ResetTime: "2026-09-13T00:00:00Z",
Description: "75% remaining",
},
},
},
},
}, nil
}
func (p *testQuotaProvider) ResetQuota(ctx context.Context, req pluginapi.QuotaResetRequest) (pluginapi.QuotaResetResponse, error) {
if p.resetFn != nil {
return p.resetFn(ctx, req)
}
return pluginapi.QuotaResetResponse{
Success: true,
Message: "quota reset successful",
}, nil
}
func TestQuotaProviderCapability_Direct(t *testing.T) {
provider := &testQuotaProvider{identifier: "opencode-go"}
host := newHostWithRecords(capabilityRecord{
id: "opencode-go-plugin",
plugin: pluginapi.Plugin{
Capabilities: pluginapi.Capabilities{
QuotaProvider: provider,
},
},
})
if !host.HasQuotaProvider("opencode-go") {
t.Fatal("expected host to have quota provider for opencode-go")
}
resp, handled, errFetch := host.FetchQuota(context.Background(), pluginapi.QuotaFetchRequest{
Provider: "opencode-go",
AuthIndex: "auth-123",
})
if errFetch != nil {
t.Fatalf("unexpected fetch error: %v", errFetch)
}
if !handled {
t.Fatal("expected fetch to be handled")
}
if resp.Subscription == nil || resp.Subscription.Plan != "Pro" {
t.Fatalf("unexpected subscription: %+v", resp.Subscription)
}
if len(resp.Groups) != 1 || len(resp.Groups[0].Buckets) != 1 {
t.Fatalf("unexpected groups: %+v", resp.Groups)
}
if resp.Groups[0].Buckets[0].RemainingFraction != 0.75 {
t.Fatalf("unexpected remaining fraction: %f", resp.Groups[0].Buckets[0].RemainingFraction)
}
}
func TestQuotaProviderCapability_Reset(t *testing.T) {
provider := &testQuotaProvider{identifier: "opencode-go"}
host := newHostWithRecords(capabilityRecord{
id: "opencode-go-plugin",
plugin: pluginapi.Plugin{
Capabilities: pluginapi.Capabilities{
QuotaProvider: provider,
},
},
})
resp, handled, errReset := host.ResetQuota(context.Background(), pluginapi.QuotaResetRequest{
Provider: "opencode-go",
AuthIndex: "auth-123",
})
if errReset != nil {
t.Fatalf("unexpected reset error: %v", errReset)
}
if !handled || !resp.Success {
t.Fatalf("expected reset handled successfully, got handled=%v, resp=%+v", handled, resp)
}
}
func TestQuotaProviderCapability_Describe(t *testing.T) {
provider := &testQuotaProvider{identifier: "opencode-go"}
host := newHostWithRecords(capabilityRecord{
id: "opencode-go-plugin",
plugin: pluginapi.Plugin{
Capabilities: pluginapi.Capabilities{
QuotaProvider: provider,
},
},
})
desc, handled, errDesc := host.DescribeQuota(context.Background(), "opencode-go-plugin")
if errDesc != nil {
t.Fatalf("unexpected describe error: %v", errDesc)
}
if !handled || desc.DisplayName != "Test Quota Provider" {
t.Fatalf("unexpected describe response: %+v", desc)
}
}
func TestQuotaProviderCapability_RegisteredPlugins(t *testing.T) {
provider := &testQuotaProvider{identifier: "opencode-go"}
host := newHostWithRecords(capabilityRecord{
id: "opencode-go-plugin",
plugin: pluginapi.Plugin{
Capabilities: pluginapi.Capabilities{
QuotaProvider: provider,
},
},
})
plugins := host.RegisteredPlugins()
if len(plugins) != 1 {
t.Fatalf("expected 1 registered plugin, got %d", len(plugins))
}
if !plugins[0].SupportsQuota {
t.Fatal("expected SupportsQuota to be true")
}
if plugins[0].QuotaProvider != "opencode-go" {
t.Fatalf("expected QuotaProvider to be opencode-go, got %q", plugins[0].QuotaProvider)
}
}
func TestQuotaProviderCapability_PanicFusing(t *testing.T) {
provider := &testQuotaProvider{
identifier: "panic-provider",
fetchFn: func(context.Context, pluginapi.QuotaFetchRequest) (pluginapi.QuotaFetchResponse, error) {
panic("boom")
},
}
host := newHostWithRecords(capabilityRecord{
id: "panic-plugin",
plugin: pluginapi.Plugin{
Capabilities: pluginapi.Capabilities{
QuotaProvider: provider,
},
},
})
_, handled, errFetch := host.FetchQuota(context.Background(), pluginapi.QuotaFetchRequest{
Provider: "panic-provider",
})
if errFetch == nil {
t.Fatal("expected error after panic")
}
if handled {
t.Fatal("expected handled to be false after panic")
}
if !host.isPluginFused("panic-plugin") {
t.Fatal("expected plugin to be fused after panic")
}
}
func TestQuotaProviderCapability_RPC(t *testing.T) {
provider := &testQuotaProvider{identifier: "rpc-quota-provider"}
plugin := validTestPlugin("rpc-quota-plugin")
plugin.Capabilities.QuotaProvider = provider
lookup := newTestSymbolLookup(&testPlugin{
registerResult: plugin,
})
host := New()
registered, errRegister := registerRPCPlugin(context.Background(), host, "rpc-quota-plugin", lookup, pluginabi.MethodPluginRegister, nil)
if errRegister != nil {
t.Fatalf("registerRPCPlugin() error = %v", errRegister)
}
if registered.Capabilities.QuotaProvider == nil {
t.Fatal("expected registered plugin to have QuotaProvider capability")
}
hostWithRPC := newHostWithRecords(capabilityRecord{
id: "rpc-quota-plugin",
plugin: registered,
})
if !hostWithRPC.HasQuotaProvider("rpc-quota-provider") {
t.Fatal("expected host to recognize rpc-quota-provider")
}
fetchResp, handled, errFetch := hostWithRPC.FetchQuota(context.Background(), pluginapi.QuotaFetchRequest{
Provider: "rpc-quota-provider",
AuthIndex: "auth-rpc-1",
})
if errFetch != nil {
t.Fatalf("unexpected rpc fetch error: %v", errFetch)
}
if !handled || fetchResp.Subscription == nil || fetchResp.Subscription.Plan != "Pro" {
t.Fatalf("unexpected rpc fetch response: %+v", fetchResp)
}
}
func TestValidPlugin_QuotaProviderOnly(t *testing.T) {
plugin := pluginapi.Plugin{
Metadata: pluginapi.Metadata{
Name: "quota-only",
Version: "1.0.0",
Author: "author",
GitHubRepository: "https://github.com/example/quota-only",
},
Capabilities: pluginapi.Capabilities{
QuotaProvider: &testQuotaProvider{identifier: "quota-only-provider"},
},
}
if !validPlugin(plugin) {
t.Fatal("expected validPlugin to return true for plugin with only QuotaProvider capability")
}
}
func TestQuotaProvider_MultipleSupportedProviders(t *testing.T) {
provider := &testQuotaProvider{
identifier: "multi-quota",
describeFn: func(ctx context.Context, req pluginapi.QuotaDescribeRequest) (pluginapi.QuotaDescribeResponse, error) {
return pluginapi.QuotaDescribeResponse{
SupportedProviders: []string{"provider-alpha", "provider-beta"},
DisplayName: "Multi Quota Provider",
}, nil
},
}
host := newHostWithRecords(capabilityRecord{
id: "multi-quota-plugin",
plugin: pluginapi.Plugin{
Capabilities: pluginapi.Capabilities{
QuotaProvider: provider,
},
},
})
if !host.HasQuotaProvider("provider-alpha") {
t.Fatal("expected host to recognize supported provider-alpha")
}
if !host.HasQuotaProvider("provider-beta") {
t.Fatal("expected host to recognize supported provider-beta")
}
resp, handled, errFetch := host.FetchQuota(context.Background(), pluginapi.QuotaFetchRequest{
Provider: "provider-alpha",
})
if errFetch != nil || !handled || resp.Subscription == nil || resp.Subscription.Plan != "Pro" {
t.Fatalf("fetch failed for provider-alpha: handled=%v err=%v resp=%+v", handled, errFetch, resp)
}
}
func TestQuotaProvider_VersionReloadInvalidatesCache(t *testing.T) {
providerV1 := &testQuotaProvider{
identifier: "dynamic-quota",
describeFn: func(ctx context.Context, req pluginapi.QuotaDescribeRequest) (pluginapi.QuotaDescribeResponse, error) {
return pluginapi.QuotaDescribeResponse{
SupportedProviders: []string{"provider-v1"},
}, nil
},
}
host := New()
setHostSnapshotForTest(host, true, capabilityRecord{
id: "dynamic-plugin",
path: "plugins/dynamic",
version: "1.0.0",
plugin: pluginapi.Plugin{
Capabilities: pluginapi.Capabilities{
QuotaProvider: providerV1,
},
},
})
if !host.HasQuotaProvider("provider-v1") {
t.Fatal("expected host to recognize provider-v1")
}
if host.HasQuotaProvider("provider-v2") {
t.Fatal("expected host not to recognize provider-v2 before upgrade")
}
// Reload with version 2.0.0 supporting provider-v2 instead
providerV2 := &testQuotaProvider{
identifier: "dynamic-quota",
describeFn: func(ctx context.Context, req pluginapi.QuotaDescribeRequest) (pluginapi.QuotaDescribeResponse, error) {
return pluginapi.QuotaDescribeResponse{
SupportedProviders: []string{"provider-v2"},
}, nil
},
}
setHostSnapshotForTest(host, true, capabilityRecord{
id: "dynamic-plugin",
path: "plugins/dynamic",
version: "2.0.0",
plugin: pluginapi.Plugin{
Capabilities: pluginapi.Capabilities{
QuotaProvider: providerV2,
},
},
})
if !host.HasQuotaProvider("provider-v2") {
t.Fatal("expected host to recognize provider-v2 after version upgrade")
}
if host.HasQuotaProvider("provider-v1") {
t.Fatal("expected host not to recognize provider-v1 after version upgrade")
}
}
func TestQuotaProvider_CrossHostIndependence(t *testing.T) {
providerA := &testQuotaProvider{
identifier: "isolated-quota",
describeFn: func(ctx context.Context, req pluginapi.QuotaDescribeRequest) (pluginapi.QuotaDescribeResponse, error) {
return pluginapi.QuotaDescribeResponse{
SupportedProviders: []string{"provider-only-on-host-a"},
}, nil
},
}
hostA := newHostWithRecords(capabilityRecord{
id: "plugin-a",
plugin: pluginapi.Plugin{
Capabilities: pluginapi.Capabilities{
QuotaProvider: providerA,
},
},
})
hostB := newHostWithRecords(capabilityRecord{
id: "plugin-b",
plugin: pluginapi.Plugin{
Capabilities: pluginapi.Capabilities{},
},
})
if !hostA.HasQuotaProvider("provider-only-on-host-a") {
t.Fatal("hostA should recognize provider-only-on-host-a")
}
if hostB.HasQuotaProvider("provider-only-on-host-a") {
t.Fatal("hostB must not leak or share quota cache with hostA")
}
}
func TestQuotaProvider_ContextCancellation(t *testing.T) {
host := newHostWithRecords(capabilityRecord{
id: "cancel-plugin",
plugin: pluginapi.Plugin{
Capabilities: pluginapi.Capabilities{
QuotaProvider: &testQuotaProvider{identifier: "cancel-provider"},
},
},
})
ctx, cancel := context.WithCancel(context.Background())
cancel() // cancel immediately
// Searching for unknown provider with canceled context should terminate promptly without error
if host.HasQuotaProviderContext(ctx, "non-existent-provider") {
t.Fatal("expected false for canceled context search")
}
}
func TestQuotaProvider_InterleavedReloadDiscardStaleCache(t *testing.T) {
describeStarted := make(chan struct{})
allowDescribeFinish := make(chan struct{})
goroutineDone := make(chan struct{})
providerV1 := &testQuotaProvider{
identifier: "interleaved-provider",
describeFn: func(ctx context.Context, req pluginapi.QuotaDescribeRequest) (pluginapi.QuotaDescribeResponse, error) {
select {
case <-describeStarted:
default:
close(describeStarted)
}
<-allowDescribeFinish
return pluginapi.QuotaDescribeResponse{
SupportedProviders: []string{"stale-provider"},
}, nil
},
}
host := New()
setHostSnapshotForTest(host, true, capabilityRecord{
id: "interleaved-plugin",
path: "plugins/interleaved",
version: "1.0.0",
plugin: pluginapi.Plugin{
Capabilities: pluginapi.Capabilities{
QuotaProvider: providerV1,
},
},
})
// Run Describe in goroutine
go func() {
defer close(goroutineDone)
_ = host.HasQuotaProvider("stale-provider")
}()
// Wait for describe to start
<-describeStarted
// Trigger reload of same plugin with version 1.0.0 supporting "fresh-provider", incrementing generation
providerV2 := &testQuotaProvider{
identifier: "interleaved-provider",
describeFn: func(ctx context.Context, req pluginapi.QuotaDescribeRequest) (pluginapi.QuotaDescribeResponse, error) {
return pluginapi.QuotaDescribeResponse{
SupportedProviders: []string{"fresh-provider"},
}, nil
},
}
setHostSnapshotForTest(host, true, capabilityRecord{
id: "interleaved-plugin",
path: "plugins/interleaved",
version: "1.0.0",
plugin: pluginapi.Plugin{
Capabilities: pluginapi.Capabilities{
QuotaProvider: providerV2,
},
},
})
// Allow the old DescribeQuota call to finally return
close(allowDescribeFinish)
// Wait for old goroutine to fully finish
<-goroutineDone
// Verify that the stale-provider is NOT cached on host because generation changed
if host.HasQuotaProvider("stale-provider") {
t.Fatal("stale-provider should not be cached after generation change")
}
if !host.HasQuotaProvider("fresh-provider") {
t.Fatal("fresh-provider should be recognized for new generation")
}
}
func TestRegisterRPCPlugin_ContextCancellation(t *testing.T) {
plugin := validTestPlugin("cancel-register-plugin")
plugin.Capabilities.QuotaProvider = &testQuotaProvider{identifier: "cancel-reg-provider"}
lookup := newTestSymbolLookup(&testPlugin{
registerResult: plugin,
})
ctx, cancel := context.WithCancel(context.Background())
cancel() // pre-canceled context
host := New()
_, err := registerRPCPlugin(ctx, host, "cancel-register-plugin", lookup, pluginabi.MethodPluginRegister, nil)
if err == nil {
t.Fatal("expected error when registering with canceled context")
}
}
func TestQuotaProviderPluginIDsSkipsPluginsFusedDuringDescribe(t *testing.T) {
for _, healthyIdentifier := range []string{"foo", "healthy-quota"} {
t.Run(healthyIdentifier, func(t *testing.T) {
broken := &testQuotaProvider{
identifier: "foo",
describeFn: func(context.Context, pluginapi.QuotaDescribeRequest) (pluginapi.QuotaDescribeResponse, error) {
panic("broken quota description")
},
}
healthy := &testQuotaProvider{
identifier: healthyIdentifier,
describeFn: func(context.Context, pluginapi.QuotaDescribeRequest) (pluginapi.QuotaDescribeResponse, error) {
return pluginapi.QuotaDescribeResponse{SupportedProviders: []string{"foo"}}, nil
},
}
host := newHostWithRecords(
capabilityRecord{id: "a-broken", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{QuotaProvider: broken}}},
capabilityRecord{id: "b-healthy", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{QuotaProvider: healthy}}},
)
ctx := context.Background()
initial := host.quotaProviderRecord(ctx, "foo")
if initial == nil || initial.id != "a-broken" {
t.Fatalf("initial selection = %#v, want a-broken", initial)
}
got := host.QuotaProviderPluginIDs(ctx)
if !host.isPluginFused("a-broken") {
t.Fatal("expected a-broken to be fused during DescribeQuota")
}
if got["foo"] != "b-healthy" {
t.Fatalf("quota plugin for foo = %q, want b-healthy", got["foo"])
}
if _, exists := got["a-broken"]; exists {
t.Fatal("fused plugin's unique alias must be removed")
}
for provider, pluginID := range got {
if !host.HasQuotaProviderForPlugin(pluginID) {
t.Fatalf("provider %q references unavailable plugin %q", provider, pluginID)
}
record := host.quotaProviderRecord(ctx, provider)
if record == nil || record.id != pluginID {
t.Fatalf("quotaProviderRecord(%q) = %#v, want plugin %q", provider, record, pluginID)
}
}
})
}
}
func TestQuotaProviderPluginIDsMatchesQuotaProviderRecordPrecedence(t *testing.T) {
multi := &testQuotaProvider{
identifier: "multi-quota",
describeFn: func(context.Context, pluginapi.QuotaDescribeRequest) (pluginapi.QuotaDescribeResponse, error) {
return pluginapi.QuotaDescribeResponse{SupportedProviders: []string{" FOO ", "bar"}}, nil
},
}
host := newHostWithRecords(
capabilityRecord{id: "a-multi", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{QuotaProvider: multi}}},
// Same-pass collision: the earlier record wins "foo" over z-foo.
capabilityRecord{id: "b-foo", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{QuotaProvider: &testQuotaProvider{identifier: "foo"}}}},
// AuthProvider alias is a first-pass match and beats a-multi's SupportedProviders for "bar".
capabilityRecord{id: "m-bar-auth", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{
QuotaProvider: &testQuotaProvider{identifier: "m-quota"},
AuthProvider: fakeAuthProvider{identifier: " Bar "},
}}},
capabilityRecord{id: "z-foo", plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{QuotaProvider: &testQuotaProvider{identifier: "foo"}}}},
)
got := host.QuotaProviderPluginIDs(context.Background())
want := map[string]string{
"multi-quota": "a-multi",
"a-multi": "a-multi",
"foo": "b-foo",
"b-foo": "b-foo",
"bar": "m-bar-auth",
"m-quota": "m-bar-auth",
"m-bar-auth": "m-bar-auth",
"z-foo": "z-foo",
}
if len(got) != len(want) {
t.Fatalf("QuotaProviderPluginIDs() = %#v, want %#v", got, want)
}
for provider, pluginID := range want {
if got[provider] != pluginID {
t.Fatalf("QuotaProviderPluginIDs()[%q] = %q, want %q", provider, got[provider], pluginID)
}
record := host.quotaProviderRecord(context.Background(), provider)
if record == nil || record.id != pluginID {
t.Fatalf("quotaProviderRecord(%q) = %#v, want plugin %q", provider, record, pluginID)
}
}
}