369 lines
9.7 KiB
Go
369 lines
9.7 KiB
Go
package backend_test
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/backend"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
|
)
|
|
|
|
const validEndpoint = "https://backend.example/v1"
|
|
|
|
func TestRegistryIncludesExactOpenRouterDefinition(t *testing.T) {
|
|
registry, err := backend.NewRegistry(nil)
|
|
if err != nil {
|
|
t.Fatalf("construct registry: %v", err)
|
|
}
|
|
|
|
definition, err := registry.GetBackend(backend.OpenRouterID)
|
|
if err != nil {
|
|
t.Fatalf("look up OpenRouter: %v", err)
|
|
}
|
|
if definition.ID != "openrouter" ||
|
|
definition.Endpoint != "https://openrouter.ai/api/v1" ||
|
|
definition.APIKeyEnv != "OPENROUTER_API_KEY" ||
|
|
definition.ConcurrencyLimit != 16 ||
|
|
definition.QueueCapacity != 1024 ||
|
|
!definition.QueueCapacitySet ||
|
|
definition.ExtraParams != nil {
|
|
t.Fatalf("unexpected OpenRouter definition: %#v", definition)
|
|
}
|
|
policies := registry.CapacityPolicies()
|
|
if len(policies) != 1 ||
|
|
policies["openrouter"] != (domain.BackendCapacityPolicy{
|
|
ConcurrencyLimit: 16,
|
|
QueueCapacity: 1024,
|
|
}) {
|
|
t.Fatalf("unexpected OpenRouter capacity policies: %#v", policies)
|
|
}
|
|
}
|
|
|
|
func TestRegistryNormalizesUniqueAdditionsAndIsolatesMutations(t *testing.T) {
|
|
nested := map[string]int{"limit": 2}
|
|
extraParams := map[string]any{
|
|
"count": int64(7),
|
|
"nested": nested,
|
|
}
|
|
registry, err := backend.NewRegistry([]domain.Backend{
|
|
{
|
|
ID: " custom ",
|
|
Endpoint: " https://custom.example/openai/v1 ",
|
|
APIKeyEnv: " CUSTOM_API_KEY ",
|
|
ExtraParams: extraParams,
|
|
ConcurrencyLimit: 3,
|
|
QueueCapacity: 2,
|
|
QueueCapacitySet: true,
|
|
},
|
|
{
|
|
ID: "Custom",
|
|
Endpoint: validEndpoint,
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("construct registry: %v", err)
|
|
}
|
|
|
|
nested["limit"] = 99
|
|
extraParams["added"] = true
|
|
|
|
got, err := registry.GetBackend("custom")
|
|
if err != nil {
|
|
t.Fatalf("look up custom backend: %v", err)
|
|
}
|
|
if got.ID != "custom" ||
|
|
got.Endpoint != "https://custom.example/openai/v1" ||
|
|
got.APIKeyEnv != "CUSTOM_API_KEY" ||
|
|
got.ConcurrencyLimit != 3 ||
|
|
got.QueueCapacity != 2 ||
|
|
!got.QueueCapacitySet {
|
|
t.Fatalf("unexpected normalized definition: %#v", got)
|
|
}
|
|
if count, ok := got.ExtraParams["count"].(int64); !ok || count != 7 {
|
|
t.Fatalf("integer type or value changed: %#v", got.ExtraParams["count"])
|
|
}
|
|
gotNested, ok := got.ExtraParams["nested"].(map[string]int)
|
|
if !ok || gotNested["limit"] != 2 {
|
|
t.Fatalf("container type or value changed: %#v", got.ExtraParams["nested"])
|
|
}
|
|
if _, exists := got.ExtraParams["added"]; exists {
|
|
t.Fatalf("registry retained caller map: %#v", got.ExtraParams)
|
|
}
|
|
|
|
gotNested["limit"] = 100
|
|
got.ExtraParams["added"] = true
|
|
again, err := registry.GetBackend("custom")
|
|
if err != nil {
|
|
t.Fatalf("look up custom backend again: %v", err)
|
|
}
|
|
if again.ExtraParams["nested"].(map[string]int)["limit"] != 2 {
|
|
t.Fatalf("lookup exposed registry nested map: %#v", again.ExtraParams)
|
|
}
|
|
if _, exists := again.ExtraParams["added"]; exists {
|
|
t.Fatalf("lookup exposed registry map: %#v", again.ExtraParams)
|
|
}
|
|
|
|
if _, err := registry.GetBackend("Custom"); err != nil {
|
|
t.Fatalf("backend IDs should be case-sensitive: %v", err)
|
|
}
|
|
|
|
policies := registry.CapacityPolicies()
|
|
if len(policies) != 2 {
|
|
t.Fatalf("unexpected capacity policy count: %#v", policies)
|
|
}
|
|
policies["custom"] = domain.BackendCapacityPolicy{}
|
|
delete(policies, backend.OpenRouterID)
|
|
againPolicies := registry.CapacityPolicies()
|
|
if againPolicies["custom"] != (domain.BackendCapacityPolicy{
|
|
ConcurrencyLimit: 3,
|
|
QueueCapacity: 2,
|
|
}) {
|
|
t.Fatalf("capacity policy map mutated registry state: %#v", againPolicies)
|
|
}
|
|
if _, ok := againPolicies[backend.OpenRouterID]; !ok {
|
|
t.Fatalf("capacity policy deletion mutated registry state: %#v", againPolicies)
|
|
}
|
|
}
|
|
|
|
func TestNewRegistryNormalizesCapacityPolicy(t *testing.T) {
|
|
maxInt := int(^uint(0) >> 1)
|
|
tests := []struct {
|
|
name string
|
|
definition domain.Backend
|
|
want domain.BackendCapacityPolicy
|
|
wantSet bool
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "unlimited when omitted",
|
|
definition: domain.Backend{},
|
|
},
|
|
{
|
|
name: "default queue",
|
|
definition: domain.Backend{
|
|
ConcurrencyLimit: 2,
|
|
},
|
|
want: domain.BackendCapacityPolicy{
|
|
ConcurrencyLimit: 2,
|
|
QueueCapacity: 1024,
|
|
},
|
|
wantSet: true,
|
|
},
|
|
{
|
|
name: "explicit zero queue",
|
|
definition: domain.Backend{
|
|
ConcurrencyLimit: 2,
|
|
QueueCapacitySet: true,
|
|
},
|
|
want: domain.BackendCapacityPolicy{
|
|
ConcurrencyLimit: 2,
|
|
},
|
|
wantSet: true,
|
|
},
|
|
{
|
|
name: "negative concurrency limit",
|
|
definition: domain.Backend{
|
|
ConcurrencyLimit: -1,
|
|
},
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "negative queue capacity",
|
|
definition: domain.Backend{
|
|
ConcurrencyLimit: 1,
|
|
QueueCapacity: -1,
|
|
QueueCapacitySet: true,
|
|
},
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "queue without limit",
|
|
definition: domain.Backend{
|
|
QueueCapacitySet: true,
|
|
},
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "total overflow",
|
|
definition: domain.Backend{
|
|
ConcurrencyLimit: maxInt,
|
|
QueueCapacity: 1,
|
|
QueueCapacitySet: true,
|
|
},
|
|
wantError: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
tc.definition.ID = "custom"
|
|
tc.definition.Endpoint = validEndpoint
|
|
registry, err := backend.NewRegistry([]domain.Backend{tc.definition})
|
|
if tc.wantError {
|
|
if err == nil {
|
|
t.Fatal("expected invalid capacity policy error")
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("construct registry: %v", err)
|
|
}
|
|
|
|
definition, err := registry.GetBackend("custom")
|
|
if err != nil {
|
|
t.Fatalf("look up custom backend: %v", err)
|
|
}
|
|
if definition.ConcurrencyLimit != tc.want.ConcurrencyLimit ||
|
|
definition.QueueCapacity != tc.want.QueueCapacity ||
|
|
definition.QueueCapacitySet != tc.wantSet {
|
|
t.Fatalf("normalized capacity=(%d, %d, %t), want (%d, %d, %t)",
|
|
definition.ConcurrencyLimit,
|
|
definition.QueueCapacity,
|
|
definition.QueueCapacitySet,
|
|
tc.want.ConcurrencyLimit,
|
|
tc.want.QueueCapacity,
|
|
tc.wantSet,
|
|
)
|
|
}
|
|
policies := registry.CapacityPolicies()
|
|
got, ok := policies["custom"]
|
|
if ok != tc.wantSet || got != tc.want {
|
|
t.Fatalf("capacity policy=(%#v, %t), want (%#v, %t)", got, ok, tc.want, tc.wantSet)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewRegistryRejectsDuplicateIDs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
additions []domain.Backend
|
|
wantID string
|
|
}{
|
|
{
|
|
name: "built-in collision after normalization",
|
|
additions: []domain.Backend{{
|
|
ID: " openrouter ",
|
|
}},
|
|
wantID: "openrouter",
|
|
},
|
|
{
|
|
name: "consumer collision after normalization",
|
|
additions: []domain.Backend{
|
|
{ID: "custom", Endpoint: validEndpoint},
|
|
{ID: " custom ", Endpoint: "https://other.example/v1"},
|
|
},
|
|
wantID: "custom",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := backend.NewRegistry(tc.additions)
|
|
if err == nil {
|
|
t.Fatal("expected duplicate ID error")
|
|
}
|
|
if !strings.Contains(err.Error(), tc.wantID) {
|
|
t.Fatalf("expected error to identify %q, got %v", tc.wantID, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewRegistryValidatesIDs(t *testing.T) {
|
|
for _, id := range []string{"", " \t\n "} {
|
|
t.Run(id, func(t *testing.T) {
|
|
_, err := backend.NewRegistry([]domain.Backend{{
|
|
ID: id,
|
|
Endpoint: validEndpoint,
|
|
}})
|
|
if err == nil {
|
|
t.Fatal("expected blank ID error")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewRegistryValidatesEndpoints(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
endpoint string
|
|
}{
|
|
{name: "blank", endpoint: ""},
|
|
{name: "relative", endpoint: "/v1"},
|
|
{name: "missing host", endpoint: "https:///v1"},
|
|
{name: "unsupported scheme", endpoint: "ftp://backend.example/v1"},
|
|
{name: "user information", endpoint: "https://user@backend.example/v1"},
|
|
{name: "query", endpoint: "https://backend.example/v1?mode=chat"},
|
|
{name: "empty query", endpoint: "https://backend.example/v1?"},
|
|
{name: "fragment", endpoint: "https://backend.example/v1#chat"},
|
|
{name: "empty fragment", endpoint: "https://backend.example/v1#"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := backend.NewRegistry([]domain.Backend{{
|
|
ID: "custom",
|
|
Endpoint: tc.endpoint,
|
|
}})
|
|
if err == nil {
|
|
t.Fatal("expected invalid endpoint error")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewRegistryValidatesEnvironmentVariableNames(t *testing.T) {
|
|
for _, name := range []string{"1API_KEY", "API-KEY", "API KEY", "ÅPI_KEY"} {
|
|
t.Run(name, func(t *testing.T) {
|
|
_, err := backend.NewRegistry([]domain.Backend{{
|
|
ID: "custom",
|
|
Endpoint: validEndpoint,
|
|
APIKeyEnv: name,
|
|
}})
|
|
if err == nil {
|
|
t.Fatal("expected invalid environment-variable name error")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewRegistryRejectsInvalidAndReservedExtraParameters(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
extraParams map[string]any
|
|
}{
|
|
{name: "unsupported value", extraParams: map[string]any{"value": make(chan int)}},
|
|
{name: "reserved key", extraParams: map[string]any{"model": "override"}},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := backend.NewRegistry([]domain.Backend{{
|
|
ID: "custom",
|
|
Endpoint: validEndpoint,
|
|
ExtraParams: tc.extraParams,
|
|
}})
|
|
if err == nil {
|
|
t.Fatal("expected invalid extra parameters error")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegistryLookupReportsNotFound(t *testing.T) {
|
|
registry, err := backend.NewRegistry(nil)
|
|
if err != nil {
|
|
t.Fatalf("construct registry: %v", err)
|
|
}
|
|
|
|
_, err = registry.GetBackend("missing")
|
|
if !errors.Is(err, backend.ErrBackendNotFound) {
|
|
t.Fatalf("expected ErrBackendNotFound, got %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "missing") {
|
|
t.Fatalf("expected error to identify backend, got %v", err)
|
|
}
|
|
}
|