Files
llmgw/internal/config/config_test.go
T
Ilker ManapandClaude Opus 5.5 4454e1322c Read usage from sums kept as it is written, not from every request
Charts, totals and budgets summed the request table on every view. Usage is
now also summed in the writing transaction into five-minute, hourly and daily
tables per key, person, project and model, with latency and first-token
histograms. Charts read the coarsest tier their slices allow; totals and
budgets compose rows, slices, hours and days, and stay exact. Request records
can be given a retention without moving any total.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-03 19:43:10 +02:00

223 lines
6.4 KiB
Go

package config
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
"gitea.mynodes.xyz/ilker/llmgw/internal/control/catalog"
)
func write(t *testing.T, content string) string {
t.Helper()
p := filepath.Join(t.TempDir(), "server.yaml")
if err := os.WriteFile(p, []byte(content), 0o600); err != nil {
t.Fatal(err)
}
return p
}
func TestLoadDefaultsWithoutFile(t *testing.T) {
cfg, err := Load("")
if err != nil {
t.Fatal(err)
}
if cfg.Listen != ":8080" || cfg.Router.DefaultProfile != "chat" {
t.Errorf("defaults not applied: %+v", cfg)
}
}
func TestLoadOverlaysOnDefaults(t *testing.T) {
// Specifying one field must not blank out its neighbours.
p := write(t, `
listen: ":9999"
endpoints:
- id: a
url: http://127.0.0.1:8000
models: [m]
`)
cfg, err := Load(p)
if err != nil {
t.Fatal(err)
}
if cfg.Listen != ":9999" {
t.Errorf("listen=%q", cfg.Listen)
}
if cfg.Router.ScrapeInterval != 2*time.Second {
t.Errorf("an unspecified default was lost: %v", cfg.Router.ScrapeInterval)
}
if len(cfg.Endpoints) != 1 || cfg.Endpoints[0].ID != "a" {
t.Errorf("endpoints=%+v", cfg.Endpoints)
}
}
func TestDurationsParse(t *testing.T) {
p := write(t, `
router:
scrape_interval: 500ms
timeouts:
idle: 45s
`)
cfg, err := Load(p)
if err != nil {
t.Fatal(err)
}
if cfg.Router.ScrapeInterval != 500*time.Millisecond {
t.Errorf("scrape_interval=%v", cfg.Router.ScrapeInterval)
}
if cfg.Router.Timeouts.Idle != 45*time.Second {
t.Errorf("timeouts.idle=%v", cfg.Router.Timeouts.Idle)
}
}
func TestEnvOverride(t *testing.T) {
t.Setenv("LLMGW_ROUTER__RETRIES", "7")
cfg, err := Load("")
if err != nil {
t.Fatal(err)
}
if cfg.Router.Retries != 7 {
t.Errorf("retries=%d, expected the environment to win", cfg.Router.Retries)
}
}
func TestMissingFileIsAnError(t *testing.T) {
if _, err := Load("/nonexistent/server.yaml"); err == nil {
t.Error("expected an error for a missing config file")
}
}
func TestValidateRejectsBadEndpoints(t *testing.T) {
cases := []struct {
name string
ep Endpoint
want string
}{
{"no id", Endpoint{URL: "http://x", Models: []string{"m"}}, "id is required"},
{"no url", Endpoint{ID: "a", Models: []string{"m"}}, "url is required"},
{"bad scheme", Endpoint{ID: "a", URL: "127.0.0.1:8000", Models: []string{"m"}}, "http://"},
{"no models", Endpoint{ID: "a", URL: "http://x"}, "at least one model"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
s := Default()
s.Endpoints = []Endpoint{c.ep}
err := s.Validate()
if err == nil {
t.Fatal("expected an error")
}
if !strings.Contains(err.Error(), c.want) {
t.Errorf("error %q does not mention %q", err, c.want)
}
})
}
}
func TestValidateRejectsDuplicateEndpointID(t *testing.T) {
s := Default()
s.Endpoints = []Endpoint{
{ID: "a", URL: "http://x", Models: []string{"m"}},
{ID: "a", URL: "http://y", Models: []string{"m"}},
}
if err := s.Validate(); err == nil || !strings.Contains(err.Error(), "twice") {
t.Errorf("duplicate id not rejected: %v", err)
}
}
func TestValidateRejectsAliasCollision(t *testing.T) {
// An alias shadowing another model would silently misroute every request
// for that model, so it has to fail at load time.
s := Default()
s.Models = []catalog.Model{
{Name: "llama-3.3-70b", Source: "/m", Aliases: []string{"qwen"},
Profiles: []catalog.Profile{{Name: "p", GPUClass: "h100-80g", TP: 1}}},
{Name: "qwen", Source: "/m",
Profiles: []catalog.Profile{{Name: "p", GPUClass: "h100-80g", TP: 1}}},
}
err := s.Validate()
if err == nil || !strings.Contains(err.Error(), "collides") {
t.Errorf("alias collision not rejected: %v", err)
}
}
func TestValidateAcceptsDistinctAliases(t *testing.T) {
s := Default()
s.Models = []catalog.Model{
{Name: "llama-3.3-70b", Source: "/m", Aliases: []string{"llama", "llama3"},
Profiles: []catalog.Profile{{Name: "p", GPUClass: "h100-80g", TP: 1}}},
{Name: "qwen2.5-32b", Source: "/m", Aliases: []string{"qwen"},
Profiles: []catalog.Profile{{Name: "p", GPUClass: "h100-80g", TP: 1}}},
}
if err := s.Validate(); err != nil {
t.Errorf("valid aliases rejected: %v", err)
}
}
func TestAnEmptyConfigurationIsAControlPlane(t *testing.T) {
// A new cluster has no models and no endpoints. Refusing it as having
// nothing to serve left a systemd unit restarting every two seconds and
// nothing listening on 8080 — on the first start anyone ever made.
var s Server
if !s.ControlPlaneMode() {
t.Fatal("a configuration with no endpoints and no models should run a control plane")
}
}
func TestStaticEndpointsAloneAreRouterOnly(t *testing.T) {
var s Server
s.Endpoints = []Endpoint{{ID: "e", URL: "http://127.0.0.1:1"}}
s.Models = []catalog.Model{{Name: "m"}}
if s.ControlPlaneMode() {
t.Error("static endpoints and bare model names are the router-only mode")
}
s.Control.Enabled = true
if !s.ControlPlaneMode() {
t.Error("control.enabled should run the control plane alongside static endpoints")
}
}
func TestAModelWithProfilesStillMakesAControlPlane(t *testing.T) {
// The inference stays, so no existing configuration has to add a flag.
var s Server
s.Models = []catalog.Model{{
Name: "m",
Profiles: []catalog.Profile{{Name: "p", Engine: "llamacpp"}},
}}
if !s.ControlPlaneMode() {
t.Error("a declared model with profiles is still a control plane")
}
}
func TestEnginesDefaultToTheCheckedVersions(t *testing.T) {
var s Server
if got := s.WantedEngines()["llamacpp"]; got != DefaultEngines["llamacpp"] {
t.Errorf("unset config wants llamacpp %q", got)
}
s.Engines = map[string]string{"llamacpp": "b11000", "vllm": "0.29.0"}
got := s.WantedEngines()
if got["llamacpp"] != "b11000" || got["vllm"] != "0.29.0" {
t.Errorf("configured versions not taken: %v", got)
}
// Empty leaves an engine to be installed by hand.
s.Engines = map[string]string{"llamacpp": ""}
if _, ok := s.WantedEngines()["llamacpp"]; ok {
t.Error("an emptied engine is still wanted")
}
}
func TestAUsageRetentionUnderADayIsRefused(t *testing.T) {
// Shorter than a day is almost certainly a unit slip — 90m for 90 days —
// and would delete every request record within the hour.
s := Default()
s.Auth.UsageRetention = 90 * time.Minute
if err := s.Validate(); err == nil || !strings.Contains(err.Error(), "usage_retention") {
t.Errorf("a 90-minute retention was accepted: %v", err)
}
s.Auth.UsageRetention = 90 * 24 * time.Hour
if err := s.Validate(); err != nil {
t.Errorf("a 90-day retention was refused: %v", err)
}
}