CI / test (push) Failing after 30m3s
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>
180 lines
7.1 KiB
Go
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)
|
|
}
|
|
}
|