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.OperationUpdate begins int } func (m *progressiveDetectionModule) Snapshot() app.Snapshot { snapshot := cloneDetectionSnapshot(m.snapshot) if m.begins == 0 { for i := range snapshot.Targets { snapshot.Targets[i].DetectionPhase = app.TargetChecking } } return snapshot } func (m *progressiveDetectionModule) Dispatch(app.Intent) app.Result { return app.Result{Snapshot: m.snapshot} } func (m *progressiveDetectionModule) StartOperation(intent app.Intent) app.OperationStream { m.begins++ for i := range m.snapshot.Targets { m.snapshot.Targets[i].DetectionPhase = app.TargetChecking m.snapshot.Targets[i].DetectionErr = nil } return app.OperationStream{Snapshot: cloneDetectionSnapshot(m.snapshot), Updates: m.streams[m.begins-1]} } func TestCompleteModelStreamsStartupAndRefreshByTargetWithoutMovingSelection(t *testing.T) { startup := make(chan app.OperationUpdate, 4) refresh := make(chan app.OperationUpdate, 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.OperationUpdate{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, "! 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.FailedOperationUpdate(cloneDetectionSnapshot(module.snapshot), app.OperationStatus{Summary: "Some Target checks failed", TargetID: "shell"}, nil, "shell", failure) model, command = applyDetectionCommand(t, model, command) if command != nil || model.selectedTargetID != "shell" { t.Fatalf("failed startup did not focus the first failed Target: command=%v selection=%q", command != nil, model.selectedTargetID) } assertUsableFrame(t, model.View(), 120, 36, "> ! Failed Shell", "● Enabled Docker") model = updateKey(model, "down") 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 != "docker" { t.Fatalf("refresh completion moved selection to %q", model.selectedTargetID) } } func TestCompleteModelReconcilesAuthoritativeSnapshotWhenDetectionGenerationClosesStale(t *testing.T) { startup := make(chan app.OperationUpdate) module := &progressiveDetectionModule{ snapshot: app.Snapshot{Targets: []app.TargetSnapshot{{ ID: "shell", Description: target.Description{Name: "Shell", Summary: "Shell description"}, }}}, streams: []chan app.OperationUpdate{startup}, } model := resizedModel(t, module, themeModeNoColor, 80, 24) assertUsableFrame(t, model.View(), 80, 24, "> … Checking Shell") // A newer Application generation has already established the authoritative // result, so the stale stream closes without publishing a completion update. _ = module.detectionUpdate("shell", app.TargetReady, true, target.Enabled, "shell enabled", nil) close(startup) model, next := applyDetectionCommand(t, model, model.Init()) if next != nil || model.operation.active { t.Fatalf("stale closure left detection active: next=%v active=%v", next != nil, model.operation.active) } assertUsableFrame(t, model.View(), 80, 24, "> ● Enabled Shell", "Evidence: shell enabled") } func (m *progressiveDetectionModule) detectionUpdate(id string, phase app.TargetDetectionPhase, available bool, state target.State, evidence string, err error) app.OperationUpdate { 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 } progressPhase := app.TargetProgressSucceeded if err != nil { progressPhase = app.TargetProgressFailed } return app.RunningOperationUpdate(cloneDetectionSnapshot(m.snapshot), app.OperationStatus{Summary: "Checked Target", TargetID: id}, []app.TargetProgress{{TargetID: id, Phase: progressPhase, State: state, Evidence: evidence, Err: err}}) } 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 } }