package cli import ( "fmt" "io" "net/http" "net/http/httptest" "os" "path/filepath" "slices" "strings" "testing" "time" "gitea.mynodes.xyz/ilker/llmgw/internal/config" "gitea.mynodes.xyz/ilker/llmgw/internal/control/catalog" "gitea.mynodes.xyz/ilker/llmgw/internal/guard" ) func TestTheInitAgentUnitIsTheShippedUnit(t *testing.T) { b, err := os.ReadFile("../../deploy/systemd/llmgw-agent.service") if err != nil { t.Fatal(err) } if string(b) != agentUnit { t.Error("deploy/systemd/llmgw-agent.service and the unit 'server init' writes have drifted") } } func TestTheStarterUsesTheGPUAndFallsBackToCPUOnThisMachine(t *testing.T) { m := starterModel(starterChoices[0], defaultContext, 0, "http://10.0.0.5:8081/qwen2.5-0.5b-instruct/starter/q.gguf", []string{"4090-24g"}, true) c, err := catalog.New([]catalog.Model{m}) if err != nil { t.Fatalf("the starter is not a valid catalog entry: %v", err) } got, _ := c.Resolve(starterName) if len(got.Profiles) != 2 { t.Fatalf("profiles = %+v", got.Profiles) } gpu, cpu := got.Profiles[0], got.Profiles[1] if gpu.GPUClass != "4090-24g" || gpu.Priority <= cpu.Priority { t.Errorf("the GPU profile should come first: %+v", gpu) } if cpu.GPUClass != "" || cpu.Engine != "llamacpp" { t.Errorf("the fallback should be llama.cpp on CPU: %+v", cpu) } for _, p := range got.Profiles { if p.NodeSelector[localLabelKey] != localLabelValue { t.Errorf("profile %s is not pinned to this machine", p.Name) } } if got.Scaling.Min != 1 { t.Error("the starter should be running, not waiting to be asked for") } if m := starterModel(starterChoices[0], defaultContext, 0, "http://x/q.gguf", nil, true); len(m.Profiles) != 1 || m.Profiles[0].Name != "cpu" { t.Errorf("without a GPU only the CPU profile: %+v", m.Profiles) } } func TestTheMirrorBlockIsAddedAndReads(t *testing.T) { in, out := newTestInit(t) if err := in.run(t.Context()); err != nil { t.Fatalf("%v\n%s", err, out) } if err := appendMirrorBlockFor(in.configPath(), "http://"+mirrorAPI, "# the mirror"); err != nil { t.Fatal(err) } cfg, err := config.Load(in.configPath()) if err != nil { t.Fatal(err) } if cfg.Mirror.API != "http://"+mirrorAPI { t.Errorf("mirror.api = %q", cfg.Mirror.API) } if err := cfg.Validate(); err != nil { t.Error(err) } backups, _ := filepath.Glob(in.configPath() + ".bak-*") if len(backups) != 1 { t.Errorf("backups = %v", backups) } } func TestTheMirrorUnitAdvertisesAnAddressNodesCanReach(t *testing.T) { in := &serverInit{hosts: []string{"10.0.0.140"}} u := in.mirrorUnit() if !strings.Contains(u, "--base http://10.0.0.140:8081") { t.Errorf("base: %s", u) } // The management port can delete models; nothing but this machine may // reach it, which is what lets it go without a certificate. if !strings.Contains(u, "--api-listen 127.0.0.1:8082") { t.Errorf("the management API is not on loopback: %s", u) } } func TestWithNoNodeHereTheStarterGoesToTheFirstNodeThatJoins(t *testing.T) { m := starterModel(starterChoices[0], defaultContext, 0, "http://x/q.gguf", nil, false) for _, p := range m.Profiles { if len(p.NodeSelector) > 0 { t.Errorf("profile %s is pinned to a machine that is not a node: %v", p.Name, p.NodeSelector) } } } func TestTheMirrorAPIOnlyAnswersItsControlPlane(t *testing.T) { allowed, err := parseAllow([]string{"10.0.0.140", "10.1.0.0/24"}) if err != nil { t.Fatal(err) } h := onlyFrom(allowed, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {})) for addr, want := range map[string]int{ "10.0.0.140:5000": 200, "10.1.0.7:5000": 200, "127.0.0.1:5000": 200, "[::1]:5000": 200, "10.0.0.141:5000": 403, "192.168.1.9:5000": 403, } { r := httptest.NewRequest("GET", "/api/v1/health", nil) r.RemoteAddr = addr w := httptest.NewRecorder() h.ServeHTTP(w, r) if w.Code != want { t.Errorf("%s: %d, want %d", addr, w.Code, want) } } if _, err := parseAllow([]string{"not-an-address"}); err == nil { t.Error("nonsense was accepted") } } func TestTheInstalledConfigurationIsHardenedAndLoads(t *testing.T) { for _, judge := range []bool{false, true} { body := fmt.Sprintf(serverConfigTemplate, "/etc/llmgw", "/etc/llmgw", "/etc/llmgw", "/var/lib/llmgw/llmgw.db", "ops@example.com", guardSection(judge)) 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("judge=%v: %v", judge, err) } if err := cfg.Validate(); err != nil { t.Fatalf("judge=%v: %v", judge, err) } if len(cfg.Guard.Default) != 1 || cfg.Guard.Default[0] != "baseline" { t.Errorf("judge=%v: guard.default %v", judge, cfg.Guard.Default) } p := cfg.Guard.Policies["baseline"] if p.Fail != "closed" { t.Errorf("judge=%v: the baseline fails %q", judge, p.Fail) } if (p.Judge != nil) != judge { t.Errorf("judge=%v: the policy's judge is %+v", judge, p.Judge) } if judge && (!slices.Equal(p.Judge.Judges(), []string{guardModelName}) || p.Judge.Input != "block") { t.Errorf("judge %+v", p.Judge) } // A category the judge never names would never count. if judge { for _, c := range p.Judge.Categories { if !slices.Contains(guard.DefaultCategories("qwen3guard"), c) { t.Errorf("the baseline counts %q, which Qwen3Guard does not name", c) } } } if cfg.Auth.MaxKeyTTL != 90*24*time.Hour || cfg.Auth.SignupOpen || cfg.DebugPprof || cfg.AdminRemote { t.Errorf("not hardened: ttl %v signup %v pprof %v remote %v", cfg.Auth.MaxKeyTTL, cfg.Auth.SignupOpen, cfg.DebugPprof, cfg.AdminRemote) } } } func TestTheGuardModelIsAValidJudge(t *testing.T) { m := guardModel("http://10.0.0.5:8081/qwen3guard-0.6b/guard/g.gguf", 2, 2, nil) c, err := catalog.New([]catalog.Model{m}) if err != nil { t.Fatalf("not a valid catalog entry: %v", err) } got, _ := c.Resolve(guardModelName) if !got.IsGuard() || got.Guard.Format != "qwen3guard" || got.Scaling.Min != 2 { t.Errorf("%+v", got) } for _, p := range got.Profiles { if p.GPUClass != "" { t.Errorf("the judge takes a card: %+v", p) } } } func TestInitSaysAtOnceWhenAModelFitsNowhere(t *testing.T) { api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/v1/replicas": w.Write([]byte(`[]`)) case "/api/v1/explain-placement": w.Write([]byte(`{"model":"qwen","rejections":[{"profile":"cpu","node":"n1","reason":"has 3 GB of memory with 1 free, needs 2"}]}`)) default: http.NotFound(w, r) } })) defer api.Close() dir := t.TempDir() token := filepath.Join(dir, "token") os.WriteFile(token, []byte(strings.Repeat("t", 40)), 0o600) os.WriteFile(filepath.Join(dir, "server.yaml"), []byte("admin_listen: "+strings.TrimPrefix(api.URL, "http://")+"\nadmin_token_file: "+token+"\n"), 0o600) defer func(g time.Duration) { unplacedGrace = g }(unplacedGrace) unplacedGrace = 0 in := &serverInit{etcDir: dir, out: io.Discard} start := time.Now() err := in.waitServing(t.Context(), "qwen") if err == nil || !strings.Contains(err.Error(), "needs 2") || !strings.Contains(err.Error(), "explain-placement") { t.Fatalf("%v", err) } if time.Since(start) > 10*time.Second { t.Errorf("took %s to say so", time.Since(start)) } }