Files
llmgw/internal/cli/tui_init_test.go
T
Ilker ManapandClaude Opus 5.5 d1e91868ee Setup asks where alerts go, and sends each a test
notify.webhooks existed and setup never asked for one, so a new site
learnt of a parked model or a lost node from its users. `llmgw server
init --webhook URL` (repeatable) and a Notification webhooks field on
both setup screens take them now. Slack's address, Mattermost's and
Rocket.Chat's /hooks/ and Discord's …/slack are sent Slack's text, any
other the event as JSON; slack: or json: in front says which.

Once the control plane runs, they are added to the cluster's settings
beside the webhooks already there, none twice, and setup waits for the
control planes to start again on the change before sending each a test,
saying which answered. A webhook's path is its secret, so it is shown by
host only. A failure is said, not fatal: the cluster serves without its
alerts, and ctl notify test tries again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:55:48 +02:00

149 lines
5.7 KiB
Go

package cli
import (
"strings"
"testing"
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)})
}
func TestTheInitFormBecomesInitsSettings(t *testing.T) {
in := &serverInit{pkiDir: "/etc/llmgw/pki"}
var m tea.Model = newInitModel(in, []string{"10.0.0.140"}, true)
if v := m.View(); !strings.Contains(v, "Already in the certificate: 10.0.0.140") {
t.Fatalf("the addresses found are not shown:\n%s", v)
}
m = typeIn(m, "ops@example.com")
m = send(m, tea.KeyMsg{Type: tea.KeyTab})
m = typeIn(m, "llm.example.com, 203.0.113.10")
m = send(m, tea.KeyMsg{Type: tea.KeyTab})
m = typeIn(m, "https://hooks.slack.com/services/T1/B2/x, json:https://alerts.example.com/in")
m = send(m, tea.KeyMsg{Type: tea.KeyTab}, tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(" ")}) // PostgreSQL on
m = send(m, tea.KeyMsg{Type: tea.KeyTab}, tea.KeyMsg{Type: tea.KeyTab}, tea.KeyMsg{Type: tea.KeyTab},
tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(" ")}) // starter model off
m = send(m, tea.KeyMsg{Type: tea.KeyTab}, tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(" ")}) // judge off
m = send(m, tea.KeyMsg{Type: tea.KeyTab}, tea.KeyMsg{Type: tea.KeyTab}, tea.KeyMsg{Type: tea.KeyTab}) // no bundle
m = send(m, tea.KeyMsg{Type: tea.KeyEnter})
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)
}
if m.(initModel).form {
t.Error("the form stayed up after Set up")
}
}
func TestAWebhookThatIsNoAddressKeepsTheFormUp(t *testing.T) {
in := &serverInit{}
var m tea.Model = newInitModel(in, nil, true)
for range fWebhooks {
m = send(m, tea.KeyMsg{Type: tea.KeyTab})
}
m = typeIn(m, "hooks.slack.com/services/x")
for range fStart - fWebhooks {
m = send(m, tea.KeyMsg{Type: tea.KeyTab})
}
m = send(m, tea.KeyMsg{Type: tea.KeyEnter})
if !m.(initModel).form || m.(initModel).focus != fWebhooks || !strings.Contains(m.View(), "http(s) address") {
t.Errorf("an address without its scheme was taken:\n%s", m.View())
}
}
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)
}
for range fBundle {
m = send(m, tea.KeyMsg{Type: tea.KeyTab})
}
m = typeIn(m, "/media/usb/b.tar")
m = send(m, tea.KeyMsg{Type: tea.KeyTab}, tea.KeyMsg{Type: tea.KeyTab}, tea.KeyMsg{Type: tea.KeyEnter})
if !m.(initModel).form || !strings.Contains(m.View(), "sha256") || m.(initModel).focus != fBundleSHA {
t.Fatalf("a bundle without its digest was accepted:\n%s", m.View())
}
m = typeIn(m, strings.Repeat("AB", 32))
m = send(m, tea.KeyMsg{Type: tea.KeyTab}, tea.KeyMsg{Type: tea.KeyEnter})
if m.(initModel).form || in.bundle != "/media/usb/b.tar" || in.bundleSHA != strings.Repeat("ab", 32) {
t.Errorf("bundle %q sha %q, form up %v", in.bundle, in.bundleSHA, m.(initModel).form)
}
}
func TestAnEmailThatIsNotOneIsSaidSo(t *testing.T) {
in := &serverInit{}
var m tea.Model = newInitModel(in, nil, true)
m = typeIn(m, "not an address")
for range fStart {
m = send(m, tea.KeyMsg{Type: tea.KeyTab})
}
m = send(m, tea.KeyMsg{Type: tea.KeyEnter})
if !m.(initModel).form || !strings.Contains(m.View(), "not an address") {
t.Errorf("a bad email was accepted:\n%s", m.View())
}
}
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 TestWithAMirrorElsewhereTheFormDoesNotOfferOne(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, from --mirror") {
t.Errorf("the mirror in use is not shown:\n%s", v)
}
// Tab from the addresses goes to the webhooks, past the mirror line to
// PostgreSQL, then the node.
m = send(m, tea.KeyMsg{Type: tea.KeyTab}, tea.KeyMsg{Type: tea.KeyTab}, tea.KeyMsg{Type: tea.KeyTab},
tea.KeyMsg{Type: tea.KeyTab})
if f := m.(initModel).focus; f != fNode {
t.Errorf("focus %d, want the node toggle %d", f, fNode)
}
}