package tui import ( "errors" "strings" "testing" tea "github.com/charmbracelet/bubbletea" "proxyctl/internal/app" "proxyctl/internal/profile" "proxyctl/internal/target" ) type progressiveDetectionModule struct { snapshot app.Snapshot streams []chan app.TargetDetectionUpdate begins int } func (m *progressiveDetectionModule) Snapshot() app.Snapshot { return cloneDetectionSnapshot(m.snapshot) } func (m *progressiveDetectionModule) Dispatch(app.Intent) app.Result { return app.Result{Snapshot: m.snapshot} } func (m *progressiveDetectionModule) BeginTargetDetection() (app.Snapshot, <-chan app.TargetDetectionUpdate) { m.begins++ for i := range m.snapshot.Targets { m.snapshot.Targets[i].DetectionPhase = app.TargetChecking m.snapshot.Targets[i].DetectionErr = nil } return cloneDetectionSnapshot(m.snapshot), m.streams[m.begins-1] } func TestCompleteModelStreamsStartupAndRefreshByTargetWithoutMovingSelection(t *testing.T) { startup := make(chan app.TargetDetectionUpdate, 4) refresh := make(chan app.TargetDetectionUpdate, 4) module := &progressiveDetectionModule{ snapshot: app.Snapshot{Targets: []app.TargetSnapshot{ {ID: "shell", Description: target.Description{Name: "Shell", Summary: "Shell description"}}, {ID: "apt", Description: target.Description{Name: "APT", Summary: "APT description"}}, {ID: "docker", Description: target.Description{Name: "Docker", Summary: "Docker description"}}, }}, streams: []chan app.TargetDetectionUpdate{startup, refresh}, } model := resizedModel(t, module, themeModeNoColor, 120, 36) assertUsableFrame(t, model.View(), 120, 36, "> … Checking Shell", "… Checking APT", "… Checking Docker", "Shell description", "Availability and configuration inspection are in progress.", ) // Selection remains usable while checks run, and is retained by identity // even when a later product row completes first. model = updateKey(model, "down") if model.selectedTargetID != "apt" { t.Fatalf("selection during startup = %q, want apt", model.selectedTargetID) } command := model.Init() if command == nil { t.Fatal("startup did not wait for progressive detection") } startup <- module.detectionUpdate("docker", app.TargetReady, true, target.Enabled, "docker enabled", nil) model, command = applyDetectionCommand(t, model, command) view := model.View() assertOrder(t, view, "Shell", "APT", "Docker") assertUsableFrame(t, view, 120, 36, "… Checking Shell", "> … Checking APT", "● Enabled Docker") if model.selectedTargetID != "apt" { t.Fatalf("out-of-order completion moved selection to %q", model.selectedTargetID) } failure := errors.New("apt-config inspection failed") startup <- module.detectionUpdate("shell", app.TargetDetectionFailed, false, "", "", failure) model, command = applyDetectionCommand(t, model, command) assertUsableFrame(t, model.View(), 120, 36, "! Check failed Shell", "> … Checking APT", "● Enabled Docker") startup <- module.detectionUpdate("apt", app.TargetReady, false, target.Disabled, "apt-config is unavailable", nil) model, command = applyDetectionCommand(t, model, command) startup <- app.TargetDetectionUpdate{Snapshot: cloneDetectionSnapshot(module.snapshot), Complete: true} model, command = applyDetectionCommand(t, model, command) if command != nil || model.selectedTargetID != "apt" { t.Fatalf("startup completion command=%v selection=%q", command != nil, model.selectedTargetID) } assertUsableFrame(t, model.View(), 120, 36, "! Check failed Shell", "> ○ Unavailable APT", "● Enabled Docker", "Evidence: apt-config is unavailable") updated, command := model.Update(keyMessage("r")) model = updated.(Model) if command == nil || module.begins != 2 { t.Fatalf("refresh did not reuse progressive detection: command=%v begins=%d", command != nil, module.begins) } assertUsableFrame(t, model.View(), 120, 36, "… Checking Shell", "> … Checking APT", "… Checking Docker") refresh <- module.detectionUpdate("apt", app.TargetReady, true, target.Enabled, "apt enabled", nil) model, command = applyDetectionCommand(t, model, command) assertUsableFrame(t, model.View(), 120, 36, "… Checking Shell", "> ● Enabled APT", "… Checking Docker") if model.selectedTargetID != "apt" { t.Fatalf("refresh completion moved selection to %q", model.selectedTargetID) } } func (m *progressiveDetectionModule) detectionUpdate(id string, phase app.TargetDetectionPhase, available bool, state target.State, evidence string, err error) app.TargetDetectionUpdate { for i := range m.snapshot.Targets { if m.snapshot.Targets[i].ID != id { continue } m.snapshot.Targets[i].DetectionPhase = phase m.snapshot.Targets[i].DetectionErr = err m.snapshot.Targets[i].Available = available m.snapshot.Targets[i].AvailabilityEvidence = evidence m.snapshot.Targets[i].State = state m.snapshot.Targets[i].Evidence = evidence break } return app.TargetDetectionUpdate{Snapshot: cloneDetectionSnapshot(m.snapshot), TargetID: id} } func cloneDetectionSnapshot(snapshot app.Snapshot) app.Snapshot { snapshot.Targets = append([]app.TargetSnapshot(nil), snapshot.Targets...) snapshot.Profiles = append([]profile.Profile(nil), snapshot.Profiles...) return snapshot } func applyDetectionCommand(t *testing.T, model Model, command tea.Cmd) (Model, tea.Cmd) { t.Helper() updated, next := model.Update(command()) return updated.(Model), next } func assertOrder(t *testing.T, view string, values ...string) { t.Helper() last := -1 for _, value := range values { index := strings.Index(view, value) if index <= last { t.Fatalf("%q did not follow prior Target in frame:\n%s", value, view) } last = index } }