Files
llmgw/internal/config/config_test.go
T
Ilker ManapandClaude Opus 5.5 6dba6bd93a Run a control plane when there is nothing else to run
A configuration with no static endpoints and no models was refused as
having nothing to serve. That is every new cluster: under systemd the
unit restarted every two seconds and nothing ever listened on 8080.

With no endpoints a router-only process has nothing it could serve, so
such a configuration is now a control plane with an empty catalog.
Static endpoints without profiled models remain router-only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 13:06:29 +02:00

209 lines
5.9 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")
}
}