package cli import ( "encoding/json" "io" "math" "net/http" "net/http/httptest" "os" "slices" "strings" "sync" "testing" "time" tea "github.com/charmbracelet/bubbletea" "gitea.mynodes.xyz/ilker/llmgw/internal/config" "gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi" "gitea.mynodes.xyz/ilker/llmgw/internal/control/catalog" "gitea.mynodes.xyz/ilker/llmgw/internal/control/modelserve" "gitea.mynodes.xyz/ilker/llmgw/internal/guard" "gitea.mynodes.xyz/ilker/llmgw/internal/modelplan" "gitea.mynodes.xyz/ilker/llmgw/internal/promq" "gitea.mynodes.xyz/ilker/llmgw/internal/store" "gitea.mynodes.xyz/ilker/llmgw/internal/version" ) type seen struct { mu sync.Mutex reqs []string } func (s *seen) last() string { s.mu.Lock() defer s.mu.Unlock() if len(s.reqs) == 0 { return "" } return s.reqs[len(s.reqs)-1] } // adminStub answers every change with 204 and records it as "METHOD path body". func adminStub(t *testing.T) (*seen, string) { s := &seen{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { _, _ = w.Write([]byte("[]")) return } b, _ := io.ReadAll(r.Body) s.mu.Lock() s.reqs = append(s.reqs, r.Method+" "+r.URL.Path+" "+strings.TrimSpace(string(b))) s.mu.Unlock() w.WriteHeader(http.StatusNoContent) })) t.Cleanup(srv.Close) return s, srv.URL } // pressRun presses keys and runs, one level deep, whatever the last one // returns that is not a tick, the way the program would. func pressRun(m tea.Model, keys ...string) tea.Model { m = press(m, keys[:len(keys)-1]...) var msg tea.Msg = tea.KeyMsg{Type: tea.KeyEnter} if k := keys[len(keys)-1]; k != "enter" { msg = tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(k)} } m, cmd := m.Update(msg) if cmd != nil { if done, ok := cmd().(actionDoneMsg); ok { m, _ = m.Update(done) } } return m } func consoleWith(t *testing.T, menu int) (consoleModel, *seen) { s, url := adminStub(t) m := newConsoleModel(url, "/nonexistent/server.yaml") m.loaded, m.menu, m.inPanel = true, menu, true m.data.models = []adminapi.ModelView{{Name: "qwen", Min: 1, Max: 1, Class: "standard", Profiles: []string{"cpu"}}} m.data.nodes = []adminapi.NodeView{{ID: "n1", State: "ready"}} m.data.replicas = []adminapi.ReplicaView{{ID: "qwen-1", Model: "qwen"}} return m, s } func TestAModelsReplicasAreSetFromItsRow(t *testing.T) { m, s := consoleWith(t, cModels) var tm tea.Model = m tm = press(tm, "enter") if !strings.Contains(tm.View(), "Replicas (scale)") { t.Fatalf("no action menu:\n%s", tm.View()) } tm = press(tm, "down", "enter") f := tm.(consoleModel).formD if f == nil { t.Fatal("no form") } f.fields[1].text.SetValue("3") // A max below the min is refused in the form, and nothing is sent. f.fields[0].text.SetValue("5") tm = press(tm, "tab", "tab", "tab", "enter") if f.err == "" || s.last() != "" { t.Fatalf("a max below the min went through: %q %q", f.err, s.last()) } f.fields[0].text.SetValue("2") tm = pressRun(tm, "enter") var body adminapi.ScaleRequest got := s.last() if !strings.HasPrefix(got, "POST /api/v1/models/qwen/scale ") || json.Unmarshal([]byte(strings.SplitN(got, " ", 3)[2]), &body) != nil || body.Min != 2 || body.Max != 3 { t.Fatalf("sent %q", got) } if !strings.Contains(tm.View(), "keeps 2 to 3") { t.Errorf("the outcome was not shown:\n%s", tm.View()) } } func TestANodeIsCordonedAndAModelRemovedOnlyWhenConfirmed(t *testing.T) { m, s := consoleWith(t, cNodes) var tm tea.Model = m tm = pressRun(tm, "enter", "down", "enter") if got := s.last(); got != `POST /api/v1/nodes/n1/state {"state":"cordoned"}` { t.Fatalf("sent %q", got) } m, s = consoleWith(t, cModels) tm = m tm = press(tm, "enter") n := len(tm.(consoleModel).menuD.items) keys := []string{} for range n - 1 { keys = append(keys, "down") } tm = press(tm, append(keys, "enter")...) if tm.(consoleModel).confirmD == nil { t.Fatal("removing did not ask first") } tm = press(tm, "n") if s.last() != "" { t.Fatalf("removed though the answer was no: %q", s.last()) } tm = press(tm, "enter") tm = press(tm, append(keys, "enter")...) _ = pressRun(tm, "y") if got := s.last(); got != "DELETE /api/v1/models/qwen " { t.Fatalf("sent %q", got) } } func TestAQuotaIsAddedWithOnlyWhatIsGiven(t *testing.T) { m, s := consoleWith(t, cQuotas) var tm tea.Model = press(m, "a") f := tm.(consoleModel).formD f.fields[0].text.SetValue("team:acme/ml") f.fields[4].text.SetValue("2") for range len(f.fields) { tm = press(tm, "tab") } _ = pressRun(tm, "enter") if got := s.last(); got != `PUT /api/v1/quotas {"scope":"team:acme/ml","guarantee":{},"limit":{"gpus":2}}` { t.Fatalf("sent %q", got) } } func TestPeopleAreManagedFromTheConsoleAgainstItsStore(t *testing.T) { dir := t.TempDir() cfgPath := dir + "/server.yaml" yaml := "auth:\n store: " + dir + "/llmgw.db\n" if err := os.WriteFile(cfgPath, []byte(yaml), 0o600); err != nil { t.Fatal(err) } if _, err := config.Load(cfgPath); err != nil { t.Fatalf("the test's configuration: %v", err) } m := newConsoleModel("127.0.0.1:1", cfgPath) defer m.st.close() m.loaded, m.inPanel = true, true refresh := func(tm tea.Model) tea.Model { cm := tm.(consoleModel) msg := cm.fetchPeople()() tm, _ = tm.Update(msg) return tm } submit := func(tm tea.Model, vals ...string) tea.Model { f := tm.(consoleModel).formD if f == nil { t.Fatal("no form") } for i, v := range vals { f.fields[i].text.SetValue(v) } f.focus = len(f.fields) tm = pressRun(tm, "enter") if v := tm.View(); !strings.Contains(v, "✓") { t.Fatalf("did not work:\n%s", v) } return refresh(press(tm, "x")) } m.menu = cOrgs var tm tea.Model = m tm = submit(press(tm, "a"), "Acme") // Into the organization: a team, then a project in it. tm = submit(press(tm, "enter", "enter"), "ml") cm := tm.(consoleModel) if len(cm.people.orgs) != 2 || cm.people.orgs[1].path != "Acme/ml" { t.Fatalf("tree: %+v", cm.people.orgs) } tm = submit(press(tm, "down", "enter", "enter"), "training") cm = tm.(consoleModel) cm.menu, cm.row = cAccounts, 0 tm = submit(press(cm, "a"), "ayse@example.com", "a-long-s3cret-pass", "Acme/ml/training") cm = tm.(consoleModel) if len(cm.people.users) != 1 || cm.people.users[0].State != store.UserActive || !strings.Contains(strings.Join(cm.people.where[cm.people.users[0].ID], ","), "training") { t.Fatalf("account: %+v %v", cm.people.users, cm.people.where) } cm.menu, cm.row = cKeys, 0 tm = press(cm, "a") f := tm.(consoleModel).formD f.fields[0].text.SetValue("ci") f.fields[1].text.SetValue("Acme/ml/training") f.focus = len(f.fields) tm = pressRun(tm, "enter") if v := tm.View(); !strings.Contains(v, "sk-") { t.Fatalf("the new key was not shown:\n%s", v) } tm = refresh(press(tm, "x")) if n := len(tm.(consoleModel).people.keys); n != 1 { t.Fatalf("%d keys", n) } // Limits on the project, read back in its row. cm = tm.(consoleModel) cm.menu, cm.row = cOrgs, 2 tm = press(cm, "enter") items := tm.(consoleModel).menuD.items i := 0 for ; items[i].label != "Limits…"; i++ { } keys := []string{} for range i { keys = append(keys, "down") } tm = submit(press(tm, append(keys, "enter")...), "60") if c := limitsCell(tm.(consoleModel).people.limits[store.LimitOwner{Scope: store.ScopeProject, ID: tm.(consoleModel).people.orgs[2].id}.CounterKey()]); c != "60 rpm" { t.Fatalf("limits: %q", c) } } func TestTheSidebarFitsAShortScreen(t *testing.T) { m, _ := consoleWith(t, cOrgs) m.inPanel = false for _, h := range []int{20, 30, 50} { m.height = h if n := len(strings.Split(m.View(), "\n")); n > h { t.Errorf("height %d: %d lines drawn", h, n) } if !strings.Contains(m.View(), "Organizations") { t.Errorf("height %d: the current entry is not shown", h) } } } func TestAModelIsFoundOnTheRegistryAndServedFromTheConsole(t *testing.T) { reg := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/api/models": if r.URL.Query().Get("search") != "qwen" || r.URL.Query().Get("filter") != "gguf" { _, _ = w.Write([]byte(`[]`)) return } _, _ = w.Write([]byte(`[{"id":"bartowski/Qwen2.5-3B-Instruct-GGUF","downloads":1200,"gguf":{"total":3000000000,"architecture":"qwen2"}}]`)) case r.URL.Path == "/api/models/bartowski/Qwen2.5-3B-Instruct-GGUF": _, _ = w.Write([]byte(`{"id":"bartowski/Qwen2.5-3B-Instruct-GGUF","sha":"abc"}`)) case strings.HasPrefix(r.URL.Path, "/api/models/bartowski/Qwen2.5-3B-Instruct-GGUF/tree/"): _, _ = w.Write([]byte(`[{"type":"file","path":"Qwen2.5-3B-Instruct-Q4_K_M.gguf","lfs":{"oid":"x","size":2000000000}}, {"type":"file","path":"Qwen2.5-3B-Instruct-Q8_0.gguf","lfs":{"oid":"y","size":3000000000}}, {"type":"file","path":"mmproj-f16.gguf","size":10}]`)) default: http.NotFound(w, r) } })) defer reg.Close() dir := t.TempDir() cfgPath := dir + "/server.yaml" if err := os.WriteFile(cfgPath, []byte("mirror:\n api: http://127.0.0.1:1\n registry: "+reg.URL+"\n"), 0o600); err != nil { t.Fatal(err) } // The control plane's plan of the repository's files. plan := modelserve.Plan{Repo: "bartowski/Qwen2.5-3B-Instruct-GGUF", Revision: "abc", Format: "gguf", Context: 16384, Servable: true, Artifacts: []modelserve.PlannedArtifact{ {Artifact: modelplan.Artifact{Name: "Qwen2.5-3B-Instruct-Q4_K_M.gguf", Format: "gguf", Bytes: 2 << 30}}, {Artifact: modelplan.Artifact{Name: "Qwen2.5-3B-Instruct-Q8_0.gguf", Format: "gguf", Bytes: 3 << 30}}, {Artifact: modelplan.Artifact{Name: "Qwen2.5-3B-Instruct-F16.gguf", Format: "gguf", Bytes: 6 << 30}}, }} gpu := modelplan.Option{Format: "gguf", Engine: "llamacpp", Class: "6000-24g", GPUsPerNode: 1, Nodes: 1, NeedGB: 5, HaveGB: 21.6, Hosts: 1, FreeNow: true} cpu := modelplan.Option{Format: "gguf", Engine: "llamacpp", CPU: true, NeedGB: 5, HaveGB: 27, Hosts: 2, FreeNow: true} for _, o := range []struct { art string o modelplan.Option }{{"Qwen2.5-3B-Instruct-Q8_0.gguf", gpu}, {"Qwen2.5-3B-Instruct-Q4_K_M.gguf", gpu}, {"Qwen2.5-3B-Instruct-Q8_0.gguf", cpu}, {"Qwen2.5-3B-Instruct-F16.gguf", cpu}} { o.o.Artifact = o.art plan.Options = append(plan.Options, o.o) } for i := range plan.Artifacts { for j := range plan.Options { if plan.Options[j].Artifact == plan.Artifacts[i].Name { plan.Artifacts[i].Best = &plan.Options[j] break } } } var started modelserve.Request admin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/api/v1/plan" && r.URL.Query().Get("repo") == plan.Repo { _ = json.NewEncoder(w).Encode(plan) return } if r.Method == http.MethodPost && r.URL.Path == "/api/v1/models/serve" { _ = json.NewDecoder(r.Body).Decode(&started) w.WriteHeader(http.StatusAccepted) _ = json.NewEncoder(w).Encode(modelserve.Job{ID: "serve-1", State: modelserve.Running}) return } _, _ = w.Write([]byte("[]")) })) defer admin.Close() m := newConsoleModel(admin.URL, cfgPath) m.loaded, m.menu, m.inPanel = true, cServe, true var tm tea.Model = m run := func(tm tea.Model, cmd tea.Cmd) tea.Model { if cmd != nil { tm, _ = tm.Update(cmd()) } return tm } tm, _ = tm.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")}) tm = press(tm, "q", "w", "e", "n") tm, cmd := tm.Update(tea.KeyMsg{Type: tea.KeyEnter}) tm = run(tm, cmd) if v := tm.View(); !strings.Contains(v, "bartowski/Qwen2.5-3B-Instruct-GGUF") || !strings.Contains(v, "3.0B") { t.Fatalf("no result:\n%s", v) } tm, cmd = tm.Update(tea.KeyMsg{Type: tea.KeyEnter}) tm = run(tm, cmd) d := tm.(consoleModel).menuD if d == nil || len(d.items) != 3 || !strings.Contains(d.items[0].label, "★") || !strings.Contains(d.items[0].label, "Q8_0") || !strings.Contains(d.items[0].label, "1 × 6000-24g") || !strings.Contains(d.items[2].label, "CPU") { t.Fatalf("files offered: %+v", d) } tm = press(tm, "enter") f := tm.(consoleModel).formD if f == nil || f.fields[0].value() != "qwen2.5-3b-instruct" || len(f.fields[4].options) != 2 { t.Fatalf("form: %+v", f) } f.focus = len(f.fields) tm = pressRun(tm, "enter") // Started on the control plane; the console stays. if act := tm.(consoleModel).action; act.kind != "" { t.Fatalf("the console left to run %v", act) } if started.Repo != plan.Repo || started.File != "Qwen2.5-3B-Instruct-Q8_0.gguf" || started.GPUClass != "6000-24g" || started.GPUsPerNode != 1 || !started.Fallbacks || started.Name != "qwen2.5-3b-instruct" { t.Fatalf("started %+v", started) } if v := tm.View(); !strings.Contains(v, "serve-1") { t.Errorf("the start was not shown:\n%s", v) } } func TestTheManualHasEveryPageAndFindsInThem(t *testing.T) { v := newDocViewer() for _, k := range []string{"console", "install", "reference", "commands", "changes"} { if findTopic(v.topics, k) < 0 { t.Errorf("no %s page", k) } } cmds := commandsManual() for _, c := range []string{"llmgw ctl model update", "llmgw users approve", "llmgw docs"} { if !strings.Contains(cmds, "# "+c) { t.Errorf("the commands page lacks %s", c) } } // Every line fits the width it was laid out for. for _, tp := range v.topics { for _, l := range renderMarkdown(tp.src(), 80) { if !strings.HasPrefix(l.plain, " ") && len([]rune(l.plain)) > 80 { t.Errorf("%s: a line of %d: %q", tp.key, len([]rune(l.plain)), l.plain) break } } } v.show(findTopic(v.topics, "console")) v.query = "drain" v.find(80, 0, 1) if l := v.lines(80)[v.scroll+2]; !strings.Contains(strings.ToLower(l.plain), "drain") { t.Errorf("found at %q", l.plain) } v.openTOC(80) if v.toc == nil || len(v.toc.items) < 5 { t.Fatal("no contents") } } func TestQuestionMarkOpensTheConsolesPage(t *testing.T) { m, _ := consoleWith(t, cNodes) tm := press(m, "?") cm := tm.(consoleModel) if cm.menu != cManual || !cm.detail || cm.docs.topics[cm.docs.open].key != "console" { t.Fatalf("menu %d detail %v", cm.menu, cm.detail) } if !strings.Contains(tm.View(), "The console") { t.Error("the page is not shown") } } func TestNextFindsTheNextMatchAndComesRound(t *testing.T) { v := newDocViewer() v.show(findTopic(v.topics, "console")) v.query = "revok" lines := v.lines(80) var hits []int for i, l := range lines { if strings.Contains(strings.ToLower(l.plain), "revok") { hits = append(hits, i) } } if len(hits) < 2 { t.Fatalf("hits %v", hits) } v.find(80, 0, 1) seen := []int{v.scroll + 2} for range hits { v.key(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("n")}, 80, 30) seen = append(seen, v.scroll+2) } if seen[0] != hits[0] || seen[1] != hits[1] || seen[len(hits)] != hits[0] { t.Errorf("hits %v, went %v", hits, seen) } } func TestTabAndArrowsGoRoundAFormFromApplyWithoutFalling(t *testing.T) { m, _ := consoleWith(t, cQuotas) var tm tea.Model = press(m, "a") n := len(tm.(consoleModel).formD.fields) // Round the form twice each way, through Apply. for range 2 * (n + 1) { tm = press(tm, "tab") } for range 2 * (n + 1) { tm, _ = tm.Update(tea.KeyMsg{Type: tea.KeyUp}) tm, _ = tm.Update(tea.KeyMsg{Type: tea.KeyShiftTab}) } for range 2 * (n + 1) { tm = press(tm, "down") } if f := tm.(consoleModel).formD; f == nil || f.focus < 0 || f.focus > n { t.Fatalf("focus %+v", f) } } func TestEveryScreenShowsTheVersionTopRight(t *testing.T) { m, _ := consoleWith(t, cNodes) first := strings.Split(m.View(), "\n")[0] if !strings.Contains(first, version.Version) { t.Errorf("the console's top line: %q", first) } d := docsModel{v: newDocViewer(), width: 100, height: 30} d.v.show(0) if first := strings.Split(d.View(), "\n")[0]; !strings.Contains(first, version.Version) { t.Errorf("the manual's top line: %q", first) } } func TestJobsAreFollowedFromTheirOwnView(t *testing.T) { m, _ := consoleWith(t, cJobs) m.width = 160 m.data.jobs = []modelserve.Job{{ID: "serve-9", Model: "phi-4", State: modelserve.Running, Started: time.Now(), Steps: []modelserve.Step{{Name: "registry", State: modelserve.Done, Detail: "phi-4-Q8_0.gguf"}, {Name: "mirror", State: modelserve.Running, Detail: "fetching: 3.1 of 14.6 GB (21%)"}}}} v := m.View() if !strings.Contains(v, "Jobs (1)") || !strings.Contains(v, "fetching: 3.1 of 14.6 GB") { t.Fatalf("%s", v) } tm := press(m, "enter") if v := tm.View(); !strings.Contains(v, "serve-9") || !strings.Contains(v, "phi-4-Q8_0.gguf") { t.Errorf("steps not shown:\n%s", v) } } func TestTheMonitorDrawsFromPrometheusAndWithoutIt(t *testing.T) { pts := func(vals ...float64) []promq.Series { s := promq.Series{Name: "x"} for _, v := range vals { s.Points = append(s.Points, promq.Point{Value: v}) } return []promq.Series{s} } var asked adminapi.MetricsRequest admin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v1/metrics/range" { _, _ = w.Write([]byte("[]")) return } _ = json.NewDecoder(r.Body).Decode(&asked) res := adminapi.MetricsResult{Configured: true, Series: map[string][]promq.Series{ "cpu": pts(10, 40, 80), "mem": pts(50, 55), "rps": pts(1, 2, 3), "rpsby": pts(1, 2, 3), "tokout": pts(100, 200), "netin": pts(1e6, 2e6), }} res.Series["rpsby"][0].Name = "qwen" _ = json.NewEncoder(w).Encode(res) })) defer admin.Close() m := newConsoleModel(admin.URL, "/nonexistent/server.yaml") m.loaded, m.menu, m.inPanel, m.width, m.height = true, cMonitor, true, 160, 60 var tm tea.Model = m tm, cmd := tm.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("2")}) tm, _ = tm.Update(cmd()) if asked.Window != time.Hour || len(asked.Queries) < 10 { t.Fatalf("asked %+v", asked) } v := tm.View() for _, want := range []string{"[1h]", "CPU", "80%", "Requests", "qwen", "from Prometheus", "Network", "1.9 MB/s"} { if !strings.Contains(v, want) { t.Errorf("no %q:\n%s", want, v) } } if !strings.ContainsAny(v, "⣀⣤⣶⣿") { t.Errorf("no braille chart:\n%s", v) } // No Prometheus: what the console saw, and it says so. m2, _ := consoleWith(t, cMonitor) m2.width, m2.height = 160, 60 for _, c := range []float64{20, 60} { m2.data.nodes = []adminapi.NodeView{{ID: "n1", State: "ready", Load: &adminapi.LoadView{CPUPercent: c, MemPercent: 30, MemUsed: 3, MemTotal: 10}}} m2.mon.sample(m2.data) } if v := m2.View(); !strings.Contains(v, "since it opened") || !strings.Contains(v, "60%") { t.Errorf("local:\n%s", v) } } func TestBrailleChartsFillToTheirHeight(t *testing.T) { rows := brailleChart([]chartSeries{{values: []float64{0, 50, 100, 100}}}, 2, 2, 100) if len(rows) != 2 { t.Fatal(rows) } // The right cell is full top to bottom; the left holds a low point and a middle one. if !strings.Contains(rows[0], "⣿") || !strings.Contains(rows[1], "⣿") { t.Errorf("%q", rows) } if s := sparkline([]float64{0, 1, 2, 3}, 4, 3); !strings.Contains(s, "▁") || !strings.Contains(s, "█") { t.Errorf("%q", s) } } func TestANodesPageHasItsCharts(t *testing.T) { var asked adminapi.MetricsRequest admin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v1/metrics/range" { _, _ = w.Write([]byte("[]")) return } _ = json.NewDecoder(r.Body).Decode(&asked) _ = json.NewEncoder(w).Encode(adminapi.MetricsResult{Configured: true, Series: map[string][]promq.Series{ "cpu": {{Name: "n1", Points: []promq.Point{{Value: 12}, {Value: 34}}}}}}) })) defer admin.Close() m := newConsoleModel(admin.URL, "/nonexistent/server.yaml") m.loaded, m.menu, m.inPanel, m.width, m.height = true, cNodes, true, 160, 60 m.data.nodes = []adminapi.NodeView{{ID: "n1", State: "ready"}} var tm tea.Model = m tm = press(tm, "enter") // the menu; Details first tm, cmd := tm.Update(tea.KeyMsg{Type: tea.KeyEnter}) if cmd == nil { t.Fatal("opening the page read nothing") } tm, _ = tm.Update(cmd()) if len(asked.Queries) == 0 || !strings.Contains(asked.Queries[0].Expr, `node="n1"`) { t.Fatalf("asked %+v", asked.Queries) } if v := tm.View(); !strings.Contains(v, "Over the last") || !strings.Contains(v, "34%") { t.Errorf("%s", v) } } func TestAWindowsSamplesFillTheChartsWidth(t *testing.T) { // Five minutes at five-second steps is sixty samples; the chart is a // hundred and twenty dots across. Every column is drawn. to := time.Now() s := chartSeries{from: to.Add(-5 * time.Minute), to: to} for i := 0; i < 60; i++ { s.at = append(s.at, to.Add(-5*time.Minute+time.Duration(i)*5*time.Second)) s.values = append(s.values, 50) } for c, v := range s.fit(120) { if math.IsNaN(v) { t.Fatalf("column %d of 120 empty", c) } } // A series that began two minutes ago fills the last two fifths only. s.at, s.values = s.at[36:], s.values[36:] got := s.fit(100) if !math.IsNaN(got[50]) || math.IsNaN(got[70]) || math.IsNaN(got[99]) { t.Errorf("a late start: %v", got) } } func TestGuardrailsSayWhereTheyApplyFrom(t *testing.T) { var m consoleModel m.people = peopleData{ orgs: []orgRow{ {level: "org", id: "o1", path: "Acme"}, {level: "team", id: "t1", path: "Acme/ml"}, {level: "project", id: "p1", path: "Acme/ml/train"}, }, guardPolicies: []string{"base", "finance", "strict"}, guardDefault: []string{"base"}, guardAt: []guard.Attachment{ {Scope: "team", ScopeID: "t1", Policy: "finance"}, {Scope: "project", ScopeID: "p1", Policy: "strict"}, }, } labels := []string{} for _, it := range m.guardMenu(m.people.orgs[2]).items { labels = append(labels, it.label) } want := []string{"applies from everywhere", "applies from Acme/ml", "✓ strict"} for i, w := range want { if !strings.Contains(labels[i], w) { t.Errorf("%q lacks %q", labels[i], w) } } if c := m.guardCell(m.people.orgs[1]); c != "finance" { t.Errorf("team cell %q", c) } if items := m.orgMenu(m.people.orgs[0]).items; !slices.ContainsFunc(items, func(i menuItem) bool { return i.label == "Guardrails…" }) { t.Error("no Guardrails… in the menu") } if items := m.orgMenu(m.people.orgs[0]).items; !slices.ContainsFunc(items, func(i menuItem) bool { return i.label == "Longest key lifetime…" }) { t.Error("no Longest key lifetime… in the menu") } if items := m.keyMenu(store.KeyRow{}).items; !slices.ContainsFunc(items, func(i menuItem) bool { return i.label == "Rotate…" }) { t.Error("no Rotate… on a key") } } func TestTheAuditViewListsChangesAndOpensOne(t *testing.T) { m, _ := consoleWith(t, cAudit) m.width, m.height = 160, 40 m.data.audit = []store.AuditEvent{{At: time.Now(), Actor: "ilker", Source: "api", Action: "POST /api/v1/models", Target: "qwen", Status: 200, Detail: `{"name":"qwen"}`}} if v := m.View(); !strings.Contains(v, "POST /api/v1/models") || !strings.Contains(v, "ilker") { t.Fatalf("not listed:\n%s", v) } tm := press(m, "enter") if v := tm.View(); !strings.Contains(v, `"name":"qwen"`) { t.Errorf("not opened:\n%s", v) } } func TestAModelsMenuOffersACanaryAndThenRunsIt(t *testing.T) { m, _ := consoleWith(t, cModels) labels := func(md adminapi.ModelView) []string { var out []string for _, it := range m.canaryItems(md) { out = append(out, it.label) } return out } md := adminapi.ModelView{Name: "qwen", Update: &modelserve.UpdateInfo{Available: true, Latest: "def98765"}} if got := labels(md); len(got) != 1 || !strings.Contains(got[0], "canary") { t.Errorf("with new weights: %v", got) } md.Canary = &catalog.Canary{Model: "qwen-canary", Percent: 10} got := strings.Join(labels(md), " | ") for _, want := range []string{"share (10%)", "Promote", "Take the canary out"} { if !strings.Contains(got, want) { t.Errorf("with a canary, %q lacks %q", got, want) } } if got := labels(adminapi.ModelView{Name: "qwen-canary", CanaryOf: "qwen", Update: &modelserve.UpdateInfo{Available: true}}); len(got) != 0 { t.Errorf("a canary offered a canary of its own: %v", got) } }