package cli import ( "strings" "testing" "time" tea "github.com/charmbracelet/bubbletea" "gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi" "gitea.mynodes.xyz/ilker/llmgw/internal/onboard" ) func send(m tea.Model, msgs ...tea.Msg) tea.Model { for _, msg := range msgs { m, _ = m.Update(msg) } return m } func TestTheNodeScreenFillsInAsStepsFinish(t *testing.T) { var m tea.Model = newNodeModel("adding gpu01", func() {}) m = send(m, evMsg{Step: onboard.StepConnect, Level: onboard.Done, Detail: "gpu01 (amd64)"}, evMsg{Step: onboard.StepCheck, Level: onboard.Warn, Detail: "fabric: nvidia_peermem is not loaded", Fix: "modprobe nvidia_peermem"}, evMsg{Step: onboard.StepCheck, Level: onboard.Running, Detail: "running llmgw doctor"}, ) v := m.View() for _, want := range []string{"✓", "gpu01 (amd64)", "nvidia_peermem is not loaded", "→ modprobe nvidia_peermem", "running llmgw doctor", "engines"} { if !strings.Contains(v, want) { t.Errorf("screen lacks %q:\n%s", want, v) } } } func TestTheFabricQuestionAnswersWithWhatWasTyped(t *testing.T) { var m tea.Model = newNodeModel("adding gpu01", func() {}) reply := make(chan []onboard.Fabric, 1) m = send(m, askFabricsMsg{suggested: []onboard.Fabric{ {Name: "fabric1", HCAs: []string{"mlx5_0", "mlx5_1"}}, {Name: "fabric2", HCAs: []string{"mlx5_4"}}, }, reply: reply}) if v := m.View(); !strings.Contains(v, "Which fabric") || !strings.Contains(v, "mlx5_0, mlx5_1") { t.Fatalf("no question on screen:\n%s", m.View()) } // Rename the first to ib-a, empty the second to leave it out. for range len("fabric1") { m = send(m, tea.KeyMsg{Type: tea.KeyBackspace}) } m = send(m, tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("ib-a")}, tea.KeyMsg{Type: tea.KeyTab}) for range len("fabric2") { m = send(m, tea.KeyMsg{Type: tea.KeyBackspace}) } m = send(m, tea.KeyMsg{Type: tea.KeyEnter}) got := <-reply if len(got) != 1 || got[0].Name != "ib-a" || strings.Join(got[0].HCAs, ",") != "mlx5_0,mlx5_1" { t.Errorf("got %+v", got) } if strings.Contains(m.View(), "Which fabric") { t.Error("the question stayed on screen after it was answered") } } func TestContinuingPastAProblemIsNoUnlessSaidYes(t *testing.T) { var m tea.Model = newNodeModel("adding gpu01", func() {}) reply := make(chan bool, 1) m = send(m, askContinueMsg{fails: 2, reply: reply}, tea.KeyMsg{Type: tea.KeyEnter}) if <-reply { t.Error("enter went on past the problems") } m = send(m, askContinueMsg{fails: 2, reply: reply}, tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("y")}) if !<-reply { t.Error("y did not go on") } _ = m } func TestANodeCountsAsJoinedOnlyWhenHeardSinceItsAgentStarted(t *testing.T) { start := time.Now().Add(-10 * time.Second) cases := []struct { state, ago string want bool }{ {"ready", "3s", true}, {"cordoned", "3s", true}, // added again while cordoned {"ready", "30s", false}, // the agent before the restart {"unreachable", "3s", false}, {"ready", "", false}, } for _, c := range cases { if got := heardSince(adminapi.NodeView{State: c.state, LastSeenAgo: c.ago}, start); got != c.want { t.Errorf("%s seen %q ago: got %v", c.state, c.ago, got) } } } func TestAFabricNamedForOneNodeIsNotAskedAgainForTheNext(t *testing.T) { na := &nodeAdd{named: map[string]string{}} asked := 0 ask := func(s []onboard.Fabric) []onboard.Fabric { asked++ return []onboard.Fabric{{Name: "ib-compute", HCAs: s[0].HCAs, Key: s[0].Key}} } first := na.rememberFabrics(ask)([]onboard.Fabric{{Name: "fabric", HCAs: []string{"mlx5_0"}, Key: "sm:0xa"}}) second := na.rememberFabrics(ask)([]onboard.Fabric{{Name: "fabric", HCAs: []string{"mlx5_0", "mlx5_1"}, Key: "sm:0xa"}}) if asked != 1 { t.Errorf("asked %d times", asked) } if len(second) != 1 || second[0].Name != "ib-compute" || len(second[0].HCAs) != 2 || first[0].Name != "ib-compute" { t.Errorf("first %+v second %+v", first, second) } // A fabric not seen before is asked about; in a script nothing is asked. if got := na.rememberFabrics(nil)([]onboard.Fabric{{Name: "fabric", Key: "sm:0xb"}}); len(got) != 0 { t.Errorf("a script was given a fabric nobody named: %+v", got) } }