Files
llmgw/internal/cli/serverinit_choices_test.go
T
Ilker ManapandClaude Opus 5.5 eeaffd764d A floating address keeps to its own network, and Caddy follows the leader
Found by a three-machine install on fresh VMs. The second standby's run
asked for the floating address's interface with `ip route get <vip>`.
The machine already held the address, so that answered lo, and keepalived
would not start. The interface now comes from the route to the address's
network. Every cluster's VRRP router id was 51, so the PoC's two control
planes and the new three on one LAN each logged the other's adverts as an
invalid password; the id now comes from the address.

The Caddyfile, deploy/ha and install.md send clients to the control plane
whose /leader is 200, not /ready: /ready is 503 on a leader with no model
yet and would take the portal away with it. Setup no longer hides a key
file's path as if it were a password, and the starter's text no longer
says it runs elsewhere when this machine is a node.

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

180 lines
7.1 KiB
Go

package cli
import (
"context"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"time"
"gitea.mynodes.xyz/ilker/llmgw/internal/config"
"gitea.mynodes.xyz/ilker/llmgw/internal/pgha"
)
func TestEveryChoiceIsInTheConfigurationWrittenAndItLoads(t *testing.T) {
in := &serverInit{
language: "tr", signupOpen: true, defaultRPM: 120, defaultTPM: 50000, maxKeyTTL: 30 * 24 * time.Hour,
media: true, evictAfterIdle: time.Minute, minLifetime: 90 * time.Second,
judge: judgeSpec{Fail: "open", Categories: []string{"Violent", "PII"}, Replicas: 3, Budget: 3 * time.Second},
}
j := in.judgeFor()
body := in.tuneConfig(fmt.Sprintf(serverConfigTemplate, "/etc/llmgw", "/etc/llmgw", "/etc/llmgw",
"/var/lib/llmgw/llmgw.db", "ops@example.com", guardSectionFor(&j)))
path := filepath.Join(t.TempDir(), "server.yaml")
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
cfg, err := config.Load(path)
if err != nil {
t.Fatalf("%v\n%s", err, body)
}
if err := cfg.Validate(); err != nil {
t.Fatal(err)
}
switch {
case cfg.Landing.Language != "tr" || !cfg.Auth.SignupOpen:
t.Errorf("language %q signup %v", cfg.Landing.Language, cfg.Auth.SignupOpen)
case cfg.Auth.DefaultRPM != 120 || cfg.Auth.DefaultTPM != 50000 || cfg.Auth.MaxKeyTTL != 30*24*time.Hour:
t.Errorf("limits %d %d %s", cfg.Auth.DefaultRPM, cfg.Auth.DefaultTPM, cfg.Auth.MaxKeyTTL)
case !cfg.Presets.Auto || cfg.Engines["comfyui"] == "":
t.Errorf("pictures and 3D: presets %v comfyui %q", cfg.Presets.Auto, cfg.Engines["comfyui"])
case cfg.OnDemand.EvictAfterIdle != time.Minute || cfg.OnDemand.MinLifetime != 90*time.Second:
t.Errorf("on demand %+v", cfg.OnDemand)
}
base := cfg.Guard.Policies["baseline"]
if base.Fail != "open" || base.Judge == nil || !slices.Equal(base.Judge.Categories, []string{"Violent", "PII"}) ||
base.Judge.LatencyBudget != 3*time.Second {
t.Errorf("the judge as written: fail %q %+v", base.Fail, base.Judge)
}
if !strings.Contains(body, "Three replicas run on CPUs") {
t.Error("the comment says another number of judges than run")
}
}
func TestNothingChosenIsTheTemplateAsItWas(t *testing.T) {
body := fmt.Sprintf(serverConfigTemplate, "/e", "/e", "/e", "/s.db", "a@b", guardSection(true))
if got := (&serverInit{}).tuneConfig(body); got != body {
t.Error("an install with nothing chosen writes another configuration than before")
}
// Each line tuneConfig changes is in the template, or a choice would be
// lost without a word.
for _, line := range []string{" signup_open: false", " default_rpm: 60", " default_tpm: 200000",
" max_key_ttl: 2160h # 90 days", " # language: tr"} {
if !strings.Contains(serverConfigTemplate, line) {
t.Errorf("the template has no %q", line)
}
}
}
func TestAStarterIsChosenByNameOrByItsFile(t *testing.T) {
s, err := starterFrom("qwen3-8b")
if err != nil || s.SizeGB != 5.0 || !strings.HasPrefix(s.Source, "hf:Qwen/Qwen3-8B-GGUF/") {
t.Errorf("by name: %+v %v", s, err)
}
s, err = starterFrom("hf:bartowski/Phi-4-GGUF/Phi-4-Q4_K_M.gguf")
if err != nil || s.Name != "phi-4-q4_k_m" {
t.Errorf("by file: %+v %v", s, err)
}
if _, err := starterFrom("gpt-5"); err == nil {
t.Error("a model nobody offers was taken")
}
m := starterModel(s, 8192, "http://x/p.gguf", []string{"6000-24g"}, true)
if m.Name != "phi-4-q4_k_m" || m.ContextLength != 8192 || len(m.Aliases) != 0 ||
!slices.Equal(m.Profiles[0].Args, []string{"--ctx-size", "32768"}) {
t.Errorf("entry %+v", m)
}
big, _ := starterFrom("qwen3-14b")
if m := starterModel(big, 16384, "http://x/q.gguf", nil, true); m.Profiles[0].MinMemoryGB != 11 ||
!slices.Equal(m.Profiles[0].Args, []string{"--ctx-size", "65536"}) {
t.Errorf("a 9 GB starter asks for %d GB, %v", m.Profiles[0].MinMemoryGB, m.Profiles[0].Args)
}
}
func TestTheChoicesAreTheirFlagsAgain(t *testing.T) {
in := &serverInit{admin: "ops@example.com", hosts: []string{"llm.example.com"}, postgres: true,
mirrorPath: "/data/models", noNode: true, language: "tr", media: true,
webhooks: []string{"https://hooks.slack.com/services/T/B/x, json:https://a.example.com/in"},
judge: judgeSpec{Categories: []string{"Violent", "Jailbreak"}}, maxKeyTTL: 7 * 24 * time.Hour}
in.starter, _ = starterFrom("qwen3-4b")
got := strings.Join(in.initFlags(), " ")
want := "--admin ops@example.com --host llm.example.com --language tr --max-key-ttl 168h --postgres " +
"--mirror-dir /data/models --no-node --starter qwen3-4b --judge-categories Violent,Jailbreak " +
"--webhook https://hooks.slack.com/services/T/B/x --webhook json:https://a.example.com/in --media"
if got != want {
t.Errorf("flags\n got %s\nwant %s", got, want)
}
if got := strings.Join((&serverInit{}).initFlags(), " "); got != "" {
t.Errorf("nothing chosen is %q", got)
}
if w := shellWord("Non-violent Illegal Acts,PII"); w != "'Non-violent Illegal Acts,PII'" {
t.Errorf("quoted %s", w)
}
}
func TestChoicesThatCannotBeInstalledAreRefused(t *testing.T) {
for _, in := range []serverInit{
{language: "de"},
{judge: judgeSpec{Fail: "maybe"}},
{judge: judgeSpec{Categories: []string{"Rudeness"}}},
{judge: judgeSpec{Replicas: 9}},
{starterCtx: 100},
{maxKeyTTL: time.Minute},
{vip: "10.0.0.160/24"},
{standbys: true, vip: "10.0.0.160"},
} {
if err := in.checkChoices(); err == nil {
t.Errorf("taken: %+v", in)
}
}
if err := (&serverInit{standbys: true, vip: "10.0.0.160/24"}).checkChoices(); err != nil {
t.Error(err)
}
}
func TestWithStandbysTheMirrorAnswersEveryControlPlane(t *testing.T) {
in := &serverInit{hosts: []string{"10.0.0.5"}, patroniNodes: []pgha.Node{
{Member: pgha.Member{Name: "db1", IP: "10.0.0.6"}}, {Member: pgha.Member{Name: "db2", IP: "10.0.0.7"}},
}}
if u := in.mirrorUnit(); !strings.Contains(u, "--api-listen 127.0.0.1:8082") || strings.Contains(u, "--api-allow") {
t.Errorf("one control plane: the API is for this machine only:\n%s", u)
}
in.standbys = true
u := in.mirrorUnit()
for _, want := range []string{"--api-listen :8082", "--api-allow 10.0.0.5", "--api-allow 10.0.0.6", "--api-allow 10.0.0.7"} {
if !strings.Contains(u, want) {
t.Errorf("the unit lacks %q:\n%s", want, u)
}
}
if got := in.mirrorAPIURL(); got != "http://10.0.0.5:8082" {
t.Errorf("the control planes reach the mirror at %s", got)
}
}
func TestTheFloatingAddressIsInTheCertificate(t *testing.T) {
in := &serverInit{vip: "10.0.0.160/24"}
if !slices.Contains(in.certHosts(), "10.0.0.160") {
t.Errorf("hosts %v", in.certHosts())
}
}
func TestStandbysWantTheStoresMachines(t *testing.T) {
in := &serverInit{standbys: true, out: &strings.Builder{}}
if err := in.ensureStandbys(context.Background()); err == nil || !strings.Contains(err.Error(), "--patroni") {
t.Errorf("standbys with nowhere to go: %v", err)
}
}
func TestTwoClustersOnOneNetworkKeepApartFloatingAddresses(t *testing.T) {
a, b := vrrpRouterID("10.0.0.161"), vrrpRouterID("10.0.0.164")
if a == b || a < 1 || a > 255 || b < 1 || b > 255 {
t.Errorf("router ids %d and %d", a, b)
}
conf := keepalivedConf("ens18", "10.0.0.164", 24, 100, "secret12")
if !strings.Contains(conf, fmt.Sprintf("virtual_router_id %d\n", b)) || !strings.Contains(conf, "interface ens18") {
t.Errorf("conf:\n%s", conf)
}
}