package tui import ( "errors" "strings" "testing" tea "github.com/charmbracelet/bubbletea" "proxyctl/internal/app" "proxyctl/internal/target" ) type targetModule struct { snapshot app.Snapshot result app.Result intents []app.Intent } func (m *targetModule) Snapshot() app.Snapshot { return m.snapshot } func (m *targetModule) Dispatch(intent app.Intent) app.Result { m.intents = append(m.intents, intent) m.snapshot = m.result.Snapshot return m.result } func TestTargetPanelShowsOnlyAvailableTargetsAndAppliesAsynchronously(t *testing.T) { module := &targetModule{snapshot: app.Snapshot{ActiveProfile: "office", Targets: []app.TargetSnapshot{ {ID: "shell", Description: target.Description{Name: "Shell", Summary: "Exports for Bash/Zsh"}, Available: true, State: target.External, Evidence: "existing proxy"}, {ID: "apt", Description: target.Description{Name: "APT"}, Available: false}, }}} module.result = app.Result{Snapshot: app.Snapshot{ActiveProfile: "office", Targets: []app.TargetSnapshot{{ID: "shell", Description: target.Description{Name: "Shell", Summary: "Exports for Bash/Zsh"}, Available: true, State: target.Enabled, Evidence: "matches"}}}, Notice: "Shell Target is now Enabled"} model := NewModel(module) view := model.View() if !strings.Contains(view, "Shell") || strings.Contains(view, "] APT") || !strings.Contains(view, "Unavailable APT") || !strings.Contains(view, "Exports for Bash/Zsh") { t.Fatalf("Target panel:\n%s", view) } updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace}) model = updated.(Model) if command == nil || !strings.Contains(model.View(), "Applying") || len(module.intents) != 0 { t.Fatalf("operation did not expose applying state before effect:\n%s", model.View()) } message := command() updated, _ = model.Update(message) model = updated.(Model) if len(module.intents) != 1 || module.intents[0].Kind != app.SetTargetDesired || !module.intents[0].Enabled || !strings.Contains(model.View(), "Enabled") { t.Fatalf("completed toggle = %#v\n%s", module.intents, model.View()) } } func TestTargetRestartConfirmationCancelsWithoutConfirmedMutation(t *testing.T) { initial := app.Snapshot{ActiveProfile: "office", Targets: []app.TargetSnapshot{{ID: "docker-daemon", Description: target.Description{Name: "Docker daemon"}, Available: true, State: target.Disabled}}} module := &targetModule{snapshot: initial, result: app.Result{Snapshot: initial, Confirmation: "Docker will restart immediately and may disrupt running work."}} model := NewModel(module) updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace}) model = updated.(Model) updated, _ = model.Update(command()) model = updated.(Model) if !strings.Contains(model.View(), "restart immediately") || len(module.intents) != 1 || module.intents[0].Confirm { t.Fatalf("warning = intents %#v\n%s", module.intents, model.View()) } updated, command = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'n'}}) model = updated.(Model) if command != nil || len(module.intents) != 1 || strings.Contains(model.View(), "restart immediately") { t.Fatalf("cancel mutated = intents %#v\n%s", module.intents, model.View()) } updated, command = model.Update(tea.KeyMsg{Type: tea.KeySpace}) model = updated.(Model) updated, _ = model.Update(command()) model = updated.(Model) module.result = app.Result{ Snapshot: app.Snapshot{ActiveProfile: "office", Targets: []app.TargetSnapshot{{ID: "docker-daemon", Description: target.Description{Name: "Docker daemon"}, Available: true, State: target.Enabled}}}, Notice: "Docker daemon Target is now Enabled", } updated, command = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'y'}}) model = updated.(Model) if command == nil { t.Fatal("confirmation did not start operation") } _ = command() if len(module.intents) != 3 || !module.intents[2].Confirm { t.Fatalf("confirmed intents = %#v", module.intents) } } func TestUnavailableShellIsOutsideMainListWithDetectionExplanation(t *testing.T) { module := &targetModule{snapshot: app.Snapshot{Targets: []app.TargetSnapshot{ {ID: "shell", Description: target.Description{Name: "Shell"}, Available: false, AvailabilityEvidence: "Shell Target requires Bash or Zsh; neither was detected"}, }}} view := NewModel(module).View() if strings.Contains(view, "[Disabled ] Shell") || !strings.Contains(view, "Unavailable Shell") || !strings.Contains(view, "requires Bash or Zsh") { t.Fatalf("unavailable Shell explanation:\n%s", view) } } func TestTargetFailurePreservesTruthfulStateAndRendersRedactedError(t *testing.T) { initial := app.Snapshot{ActiveProfile: "office", Targets: []app.TargetSnapshot{{ID: "shell", Description: target.Description{Name: "Shell"}, Available: true, State: target.Enabled}}} module := &targetModule{snapshot: initial, result: app.Result{Snapshot: initial, Err: errors.New("permission denied for alice:REDACTED@proxy")}} model := NewModel(module) updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace}) model = updated.(Model) updated, _ = model.Update(command()) model = updated.(Model) view := model.View() if !strings.Contains(view, "Enabled") || !strings.Contains(view, "\x1b[31mError:") || !strings.Contains(view, "REDACTED") { t.Fatalf("failed operation rendering:\n%s", view) } }