package cli import ( "strings" "testing" "time" tea "github.com/charmbracelet/bubbletea" ) func typeIn(m tea.Model, s string) tea.Model { return send(m, tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(s)}) } // toPart moves the focus in the list of parts to the one titled so. func toPart(t *testing.T, m tea.Model, title string) tea.Model { t.Helper() f := m.(initModel).choices for i := 0; i <= len(f.parts); i++ { if f.focus < len(f.parts) && f.parts[f.focus].title == title { return m } m = press(m, "down") } t.Fatalf("no part %q", title) return m } // toNext moves the focus to the button that goes on, and presses it. func toNext(m tea.Model) tea.Model { f := m.(initModel).choices for f.focus != len(f.parts) { m = press(m, "down") } return press(m, "enter") } func TestTheInitFormBecomesInitsSettings(t *testing.T) { in := &serverInit{pkiDir: "/etc/llmgw/pki"} var m tea.Model = newInitModel(in, []string{"10.0.0.140"}, true) m = press(toPart(t, m, "Control plane"), "enter") m = typeIn(m, "ops@example.com") m = press(m, "tab") if v := m.View(); !strings.Contains(v, "Already in its certificate: 10.0.0.140") { t.Fatalf("the addresses found are not shown:\n%s", v) } m = typeIn(m, "llm.example.com, 203.0.113.10") m = press(m, "esc") m = press(toPart(t, m, "Store"), "enter", "right", "esc") // PostgreSQL // Turning notifications on asks for the webhook at once. m = press(toPart(t, m, "Notifications"), "space") if m.(initModel).choices.dialog == nil { t.Fatal("notifications turned on without asking where") } m = typeIn(m, "https://hooks.slack.com/services/T1/B2/x, json:https://alerts.example.com/in") m = press(m, "esc") m = press(toPart(t, m, "Starter model"), "space") // off m = press(toPart(t, m, "Guardrails' judge"), "space") // off m = toNext(m) if !m.(initModel).plan { t.Fatalf("Next did not lead to the plan: %s", m.(initModel).choices.err) } if in.admin != "ops@example.com" || strings.Join(in.hosts, "|") != "llm.example.com|203.0.113.10" { t.Errorf("admin %q hosts %q", in.admin, in.hosts) } if !in.postgres || in.noMirror || in.noNode || !in.noModel || !in.noGuardModel { t.Errorf("toggles: postgres %v mirror %v node %v model %v", in.postgres, !in.noMirror, !in.noNode, !in.noModel) } if hooks, err := parseWebhooks(in.webhooks); err != nil || len(hooks) != 2 || hooks[0].Format != "slack" || hooks[1].Format != "json" { t.Errorf("webhooks %q: %+v %v", in.webhooks, hooks, err) } v := m.View() for _, want := range []string{"✓ Store", "PostgreSQL on this machine", "Left out", "Starter model", "--postgres", "--no-model"} { if !strings.Contains(v, want) { t.Errorf("the plan lacks %q:\n%s", want, v) } } m = press(m, "enter") if m.(initModel).form { t.Error("the form stayed up after the plan was agreed to") } } func TestAWebhookThatIsNoAddressKeepsTheFormUp(t *testing.T) { in := &serverInit{} var m tea.Model = newInitModel(in, nil, true) m = press(toPart(t, m, "Notifications"), "space") m = typeIn(m, "hooks.slack.com/services/x") m = toNext(press(m, "esc")) im := m.(initModel) if !im.form || im.plan || !strings.Contains(m.View(), "http(s) address") { t.Fatalf("an address without its scheme was taken:\n%s", m.View()) } // Closing the message opens the field that is wrong. m = press(m, "q") d := m.(initModel).choices.dialog if d == nil || d.Fields[d.focus] != m.(initModel).choices.webhooks { t.Errorf("the webhook was not given the cursor") } } func TestAnOfflineBundleIsGivenInTheFormWithItsDigest(t *testing.T) { in := &serverInit{} var m tea.Model = newInitModel(in, nil, true) if v := m.View(); !strings.Contains(v, "Offline bundle") { t.Fatalf("the form offers no bundle:\n%s", v) } m = press(toPart(t, m, "Offline bundle"), "space") m = typeIn(m, "/media/usb/b.tar") m = toNext(press(m, "esc")) if m.(initModel).plan || !strings.Contains(m.View(), "sha256") { t.Fatalf("a bundle without its digest was accepted:\n%s", m.View()) } m = press(m, "q") // the message closed, the digest has the cursor m = typeIn(m, strings.Repeat("AB", 32)) m = toNext(press(m, "esc")) if !m.(initModel).plan || in.bundle != "/media/usb/b.tar" || in.bundleSHA != strings.Repeat("ab", 32) { t.Errorf("bundle %q sha %q, plan %v: %s", in.bundle, in.bundleSHA, m.(initModel).plan, m.(initModel).choices.err) } } func TestAnEmailThatIsNotOneIsSaidSo(t *testing.T) { in := &serverInit{} var m tea.Model = newInitModel(in, nil, true) m = press(m, "enter") // the control plane, first m = typeIn(m, "not an address") m = toNext(press(m, "esc")) if m.(initModel).plan || !strings.Contains(m.View(), "not an address") { t.Errorf("a bad email was accepted:\n%s", m.View()) } } func TestEachPartsSettingsReachInit(t *testing.T) { in := &serverInit{} var m tea.Model = newInitModel(in, nil, true) m = press(toPart(t, m, "Control plane"), "enter", "tab", "tab", "right", "tab", "right", "esc") // Türkçe, sign-up open // The starter: qwen3-8b, the fifth, with 8192 tokens. m = press(toPart(t, m, "Starter model"), "enter", "right", "right", "right", "right", "tab") m = send(m, tea.KeyMsg{Type: tea.KeyCtrlU}) m = press(typeIn(m, "8192"), "esc") // The judge: serving flagged, three copies, 3s, and PII too. m = press(toPart(t, m, "Guardrails' judge"), "enter", "right", "tab") m = send(m, tea.KeyMsg{Type: tea.KeyCtrlU}) m = press(typeIn(m, "3"), "tab") m = send(m, tea.KeyMsg{Type: tea.KeyCtrlU}) m = typeIn(m, "3s") for range 6 { // past the five ticked, to PII m = press(m, "tab") } m = press(m, "space", "esc") m = press(toPart(t, m, "Pictures and 3D"), "space") m = press(toPart(t, m, "Limits and keys"), "enter") m = send(m, tea.KeyMsg{Type: tea.KeyCtrlU}) m = press(typeIn(m, "120"), "tab", "tab") m = send(m, tea.KeyMsg{Type: tea.KeyCtrlU}) m = press(typeIn(m, "30"), "esc") m = press(toPart(t, m, "Taking turns on a card"), "enter") m = send(m, tea.KeyMsg{Type: tea.KeyCtrlU}) m = press(typeIn(m, "1m"), "tab") m = send(m, tea.KeyMsg{Type: tea.KeyCtrlU}) m = press(typeIn(m, "1m"), "esc") m = press(toPart(t, m, "Model mirror"), "enter", "tab") m = send(m, tea.KeyMsg{Type: tea.KeyCtrlU}) m = press(typeIn(m, "/data/models"), "esc") m = toNext(m) if !m.(initModel).plan { t.Fatalf("not taken: %s", m.(initModel).choices.err) } switch { case in.language != "tr" || !in.signupOpen: t.Errorf("language %q signup %v", in.language, in.signupOpen) case in.starter.Name != "qwen3-8b" || in.starterCtx != 8192: t.Errorf("starter %q ctx %d", in.starter.Name, in.starterCtx) case in.judge.Fail != "open" || in.judge.Replicas != 3 || in.judge.Budget != 3*time.Second || len(in.judge.Categories) != 6 || in.judge.Categories[5] != "PII": t.Errorf("judge %+v", in.judge) case !in.media: t.Error("pictures and 3D not taken") case in.defaultRPM != 120 || in.defaultTPM != defaultTPM || in.maxKeyTTL != 30*24*time.Hour: t.Errorf("limits %d %d %s", in.defaultRPM, in.defaultTPM, in.maxKeyTTL) case in.evictAfterIdle != time.Minute || in.minLifetime != time.Minute: t.Errorf("turns %s %s", in.evictAfterIdle, in.minLifetime) case in.mirrorPath != "/data/models": t.Errorf("mirror %q", in.mirrorPath) } v := m.View() for _, want := range []string{"--starter qwen3-8b", "--starter-context 8192", "--judge-fail open", "--judge-replicas 3", "--language tr", "--media", "--mirror-dir /data/models", "--evict-after-idle 1m", "--max-key-ttl 720h"} { if !strings.Contains(v, want) { t.Errorf("the command lacks %q:\n%s", want, v) } } } func TestAStarterModelWantsTheMirror(t *testing.T) { in := &serverInit{} var m tea.Model = newInitModel(in, nil, true) m = toNext(press(toPart(t, m, "Model mirror"), "space")) if m.(initModel).plan || !strings.Contains(m.View(), "turn the mirror on") { t.Errorf("a starter model with no mirror to fetch it into was taken:\n%s", m.View()) } } func TestALongDialogScrollsWithItsFocus(t *testing.T) { in := &serverInit{} m := newInitModel(in, nil, true) m.choices.height = 24 var tm tea.Model = press(toPart(t, m, "Guardrails' judge"), "enter") _, top := tm.(initModel).choices.overlay() if !strings.Contains(top, "↓ more") || strings.Contains(top, "Copyright Violation") { t.Fatalf("a dialog taller than the screen is not cut:\n%s", top) } for range 11 { tm = press(tm, "tab") } if _, bottom := tm.(initModel).choices.overlay(); !strings.Contains(bottom, "Copyright Violation") { t.Errorf("the focus went where it cannot be seen:\n%s", bottom) } } func TestInitsLinesAreFiledAndThePasswordKeptInView(t *testing.T) { in := &serverInit{admin: "ops@example.com", pkiDir: "/etc/llmgw/pki"} var m tea.Model = newInitModel(in, nil, false) for _, l := range []string{ " ok system user llmgw", " kept certificate authority in /etc/llmgw/pki", " ok administrator ops@example.com (u1)", "", " password: Tr0ub4dor-3", " shown once — sign in and change it from /profile", "", " .. downloading qwen2.5-0.5b-instruct into the mirror", " .. 40%", " .. 80%", " !! an NVIDIA card is on the PCI bus but nvidia-smi does not answer", "The control plane is up. Next:", " llmgw ctl status", } { m = send(m, initLineMsg(l)) } m = send(m, initDoneMsg{}) im := m.(initModel) if im.password != "Tr0ub4dor-3" { t.Errorf("password %q", im.password) } v := m.View() for _, want := range []string{"✓ system user llmgw", "Tr0ub4dor-3", "Shown once", "80%", "! an NVIDIA card", "Back up /etc/llmgw/pki", "node add"} { if !strings.Contains(v, want) { t.Errorf("screen lacks %q:\n%s", want, v) } } if strings.Contains(v, "40%") { t.Error("progress lines piled up instead of replacing each other") } if strings.Contains(v, "llmgw ctl status") { t.Error("the closing text was listed as a step") } } func TestWithAMirrorElsewhereTheFormSaysSo(t *testing.T) { in := &serverInit{mirrorURL: "http://10.0.0.150:8082"} var m tea.Model = newInitModel(in, nil, true) if v := m.View(); !strings.Contains(v, "at http://10.0.0.150:8082") { t.Errorf("the mirror in use is not shown:\n%s", v) } m = toNext(m) if !m.(initModel).plan || in.mirrorURL != "http://10.0.0.150:8082" || !in.noMirror { t.Errorf("mirror %q noMirror %v: %s", in.mirrorURL, in.noMirror, m.(initModel).choices.err) } }