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>
149 lines
5.7 KiB
Go
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)
|
|
}
|
|
}
|