Files
CLIProxyAPI/internal/pluginhost/scheduler_test.go
Luis Pater 693ce1c55a feat(pluginhost, scheduler): introduce Go-based plugin with scheduler capabilities
- Added a Go scheduler plugin demonstrating CLIProxyAPI capabilities, such as `plugin.register`, `plugin.reconfigure`, and `scheduler.pick`.
- Implemented methods for plugin configuration, built-in scheduler delegation (`fill-first`, `round-robin`), dynamic candidate selection, and error handling.
- Extended `pluginhost` with scheduler handling, candidate normalization, and fallback mechanisms.
- Included examples, tests, and detailed documentation for scheduler usage and implementation.
2026-06-09 13:57:36 +08:00

218 lines
7.2 KiB
Go

package pluginhost
import (
"context"
"errors"
"strings"
"testing"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
)
func TestHostPickAuthUsesHighestPrioritySchedulerOnly(t *testing.T) {
var highCalls int
var lowCalls int
host := newHostWithRecords(
capabilityRecord{
id: "low",
priority: 1,
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{Scheduler: schedulerFunc(func(context.Context, pluginapi.SchedulerPickRequest) (pluginapi.SchedulerPickResponse, error) {
lowCalls++
return pluginapi.SchedulerPickResponse{Handled: true, AuthID: "auth-low"}, nil
})}},
},
capabilityRecord{
id: "high",
priority: 10,
meta: pluginapi.Metadata{Name: "high", Version: "1.0.0"},
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{Scheduler: schedulerFunc(func(ctx context.Context, req pluginapi.SchedulerPickRequest) (pluginapi.SchedulerPickResponse, error) {
highCalls++
if req.Plugin.Name != "high" {
t.Fatalf("req.Plugin.Name = %q, want high", req.Plugin.Name)
}
return pluginapi.SchedulerPickResponse{Handled: true, AuthID: "auth-high"}, nil
})}},
},
)
resp, handled, errPick := host.PickAuth(context.Background(), schedulerRequest("auth-high", "auth-low"))
if errPick != nil {
t.Fatalf("PickAuth() error = %v, want nil", errPick)
}
if !handled {
t.Fatal("PickAuth() handled = false, want true")
}
if resp.AuthID != "auth-high" {
t.Fatalf("PickAuth() AuthID = %q, want auth-high", resp.AuthID)
}
if highCalls != 1 {
t.Fatalf("high calls = %d, want 1", highCalls)
}
if lowCalls != 0 {
t.Fatalf("low calls = %d, want 0", lowCalls)
}
}
func TestHostPickAuthReturnsSchedulerError(t *testing.T) {
host := newHostWithRecords(capabilityRecord{
id: "scheduler",
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{Scheduler: schedulerFunc(func(context.Context, pluginapi.SchedulerPickRequest) (pluginapi.SchedulerPickResponse, error) {
return pluginapi.SchedulerPickResponse{}, errors.New("tenant quota exhausted")
})}},
})
_, handled, errPick := host.PickAuth(context.Background(), schedulerRequest("auth-1"))
if !handled {
t.Fatal("PickAuth() handled = false, want true")
}
if errPick == nil || !strings.Contains(errPick.Error(), "tenant quota exhausted") {
t.Fatalf("PickAuth() error = %v, want tenant quota exhausted", errPick)
}
}
func TestHostPickAuthPanicFusesAndFallsBack(t *testing.T) {
host := newHostWithRecords(capabilityRecord{
id: "scheduler",
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{Scheduler: schedulerFunc(func(context.Context, pluginapi.SchedulerPickRequest) (pluginapi.SchedulerPickResponse, error) {
panic("boom")
})}},
})
_, handled, errPick := host.PickAuth(context.Background(), schedulerRequest("auth-1"))
if handled {
t.Fatal("PickAuth() handled = true, want false")
}
if errPick != nil {
t.Fatalf("PickAuth() error = %v, want nil", errPick)
}
if !host.isPluginFused("scheduler") {
t.Fatal("scheduler plugin was not fused after panic")
}
}
func TestHostPickAuthUnhandledDoesNotCallLowerPriorityScheduler(t *testing.T) {
var lowCalls int
host := newHostWithRecords(
capabilityRecord{
id: "low",
priority: 1,
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{Scheduler: schedulerFunc(func(context.Context, pluginapi.SchedulerPickRequest) (pluginapi.SchedulerPickResponse, error) {
lowCalls++
return pluginapi.SchedulerPickResponse{Handled: true, AuthID: "auth-low"}, nil
})}},
},
capabilityRecord{
id: "high",
priority: 10,
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{Scheduler: schedulerFunc(func(context.Context, pluginapi.SchedulerPickRequest) (pluginapi.SchedulerPickResponse, error) {
return pluginapi.SchedulerPickResponse{Handled: false}, nil
})}},
},
)
_, handled, errPick := host.PickAuth(context.Background(), schedulerRequest("auth-low"))
if errPick != nil {
t.Fatalf("PickAuth() error = %v, want nil", errPick)
}
if handled {
t.Fatal("PickAuth() handled = true, want false")
}
if lowCalls != 0 {
t.Fatalf("low calls = %d, want 0", lowCalls)
}
}
func TestHostPickAuthInvalidResponseFallsBack(t *testing.T) {
tests := []struct {
name string
resp pluginapi.SchedulerPickResponse
}{
{
name: "unknown auth id",
resp: pluginapi.SchedulerPickResponse{Handled: true, AuthID: "missing"},
},
{
name: "unknown delegate",
resp: pluginapi.SchedulerPickResponse{Handled: true, DelegateBuiltin: "unknown"},
},
{
name: "handled without decision",
resp: pluginapi.SchedulerPickResponse{Handled: true},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
host := newHostWithRecords(capabilityRecord{
id: "scheduler",
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{Scheduler: schedulerFunc(func(context.Context, pluginapi.SchedulerPickRequest) (pluginapi.SchedulerPickResponse, error) {
return tt.resp, nil
})}},
})
_, handled, errPick := host.PickAuth(context.Background(), schedulerRequest("auth-1"))
if errPick != nil {
t.Fatalf("PickAuth() error = %v, want nil", errPick)
}
if handled {
t.Fatal("PickAuth() handled = true, want false")
}
})
}
}
func TestHostPickAuthPrefersValidAuthIDOverInvalidDelegate(t *testing.T) {
host := newHostWithRecords(capabilityRecord{
id: "scheduler",
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{Scheduler: schedulerFunc(func(context.Context, pluginapi.SchedulerPickRequest) (pluginapi.SchedulerPickResponse, error) {
return pluginapi.SchedulerPickResponse{Handled: true, AuthID: "auth-a", DelegateBuiltin: "unknown"}, nil
})}},
})
resp, handled, errPick := host.PickAuth(context.Background(), schedulerRequest("auth-a"))
if errPick != nil {
t.Fatalf("PickAuth() error = %v, want nil", errPick)
}
if !handled {
t.Fatal("PickAuth() handled = false, want true")
}
if resp.AuthID != "auth-a" {
t.Fatalf("PickAuth() AuthID = %q, want auth-a", resp.AuthID)
}
}
func TestHostPickAuthAllowsKnownBuiltinDelegates(t *testing.T) {
for _, delegate := range []string{pluginapi.SchedulerBuiltinRoundRobin, pluginapi.SchedulerBuiltinFillFirst} {
t.Run(delegate, func(t *testing.T) {
host := newHostWithRecords(capabilityRecord{
id: "scheduler",
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{Scheduler: schedulerFunc(func(context.Context, pluginapi.SchedulerPickRequest) (pluginapi.SchedulerPickResponse, error) {
return pluginapi.SchedulerPickResponse{Handled: true, DelegateBuiltin: delegate}, nil
})}},
})
resp, handled, errPick := host.PickAuth(context.Background(), schedulerRequest("auth-1"))
if errPick != nil {
t.Fatalf("PickAuth() error = %v, want nil", errPick)
}
if !handled {
t.Fatal("PickAuth() handled = false, want true")
}
if resp.DelegateBuiltin != delegate {
t.Fatalf("PickAuth() DelegateBuiltin = %q, want %q", resp.DelegateBuiltin, delegate)
}
})
}
}
func schedulerRequest(ids ...string) pluginapi.SchedulerPickRequest {
req := pluginapi.SchedulerPickRequest{
Provider: "test",
Model: "test-model",
}
for _, id := range ids {
req.Candidates = append(req.Candidates, pluginapi.SchedulerAuthCandidate{ID: id})
}
return req
}