A bundle carries what a cluster would download — llmgw's release with its signed checksums, every llama.cpp build per architecture with GitHub's checksums, model files pinned and checked against Hugging Face's — into a directory or a tar whose manifest sha256 is carried apart. Import checks everything, the release against the signing key, before anything is put in the mirror. With offline.mirror set, nodes install llama.cpp from the mirror against checksums the control plane holds and sends over the agent stream; releases default to the mirror for node add, ctl upgrade and agent upgrades. server init and the setup form install from a bundle. The Helm chart runs one control plane with SQLite on a claim or an external PostgreSQL, Secrets for the agent certificate and admin token, Services, Ingress and ServiceMonitor; deploy/container/Dockerfile builds its image. GPU nodes stay outside Kubernetes, as docs/kubernetes.md explains. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
127 lines
4.8 KiB
Go
127 lines
4.8 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}, 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 m.(initModel).form {
|
|
t.Error("the form stayed up after Set up")
|
|
}
|
|
}
|
|
|
|
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 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})
|
|
if f := m.(initModel).focus; f != fNode {
|
|
t.Errorf("focus %d, want the node toggle %d", f, fNode)
|
|
}
|
|
}
|