Files
proxy/internal/tui/target_test.go
T

580 lines
28 KiB
Go

package tui
import (
"errors"
"reflect"
"strings"
"testing"
tea "github.com/charmbracelet/bubbletea"
"proxyctl/internal/app"
"proxyctl/internal/profile"
"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, "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 TestHelpListsEveryTargetAndMainListsOnlyAvailableTargets(t *testing.T) {
module := &targetModule{snapshot: app.Snapshot{Targets: []app.TargetSnapshot{
{ID: "shell", Description: target.Description{Name: "Shell", Summary: "summary", Details: "affects future Shell commands; does not affect daemons; owns a generated fragment; user permission; reload side effect"}, Available: true, State: target.Disabled, Evidence: "integration installed"},
{ID: "apt", Description: target.Description{Name: "APT", Summary: "persistent package proxy", Details: "owns an overlay and requires system write permission"}, Available: false, AvailabilityEvidence: "Debian APT tool apt-config was not detected"},
}}}
model := NewModel(module)
view := model.View()
if strings.Contains(view, "APT") || !strings.Contains(view, "affects future Shell commands") || !strings.Contains(view, "integration installed") {
t.Fatalf("main Target panel:\n%s", view)
}
model = updateKey(model, "?")
view = model.View()
if !strings.Contains(view, "Shell") || !strings.Contains(view, "APT") || !strings.Contains(view, "Not detected: Debian APT tool apt-config was not detected") || !strings.Contains(view, "owns an overlay") {
t.Fatalf("Target help:\n%s", view)
}
model = updateKey(model, "esc")
if !strings.Contains(model.View(), "Target panel") {
t.Fatalf("Escape did not return from help:\n%s", model.View())
}
}
func TestRefreshKeyRunsAsynchronouslyAndRendersRefreshingThenCompleted(t *testing.T) {
module := &targetModule{snapshot: app.Snapshot{}, result: app.Result{Snapshot: app.Snapshot{Targets: []app.TargetSnapshot{{ID: "apt", Description: target.Description{Name: "APT"}, Available: true, State: target.External}}}, Notice: "Refreshed Target availability and state"}}
model := NewModel(module)
updated, command := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'r'}})
model = updated.(Model)
if command == nil || len(module.intents) != 0 || !strings.Contains(model.View(), "Refreshing Target availability and state") {
t.Fatalf("refresh did not start asynchronously: intents %#v\n%s", module.intents, model.View())
}
message := command()
if len(module.intents) != 1 || module.intents[0].Kind != app.RefreshTargets {
t.Fatalf("refresh command intents = %#v", module.intents)
}
updated, command = model.Update(message)
model = updated.(Model)
if command != nil || !strings.Contains(model.View(), "External") || !strings.Contains(model.View(), "Refreshed Target availability and state") {
t.Fatalf("refresh completion:\n%s", model.View())
}
}
func TestBusyActivationBlocksPageEntryAndEveryNewOperation(t *testing.T) {
initial := app.Snapshot{
ActiveProfile: "old",
Profiles: []profile.Profile{{Name: "new"}},
Targets: []app.TargetSnapshot{{ID: "shell", Description: target.Description{Name: "Shell"}, Available: true, State: target.Enabled}},
}
module := &progressiveTargetModule{targetModule: targetModule{snapshot: initial}, updates: make(chan app.Result)}
model := updateKey(NewModel(module), "p")
updated, command := model.Update(tea.KeyMsg{Type: tea.KeyEnter})
model = updated.(Model)
if command == nil || len(module.intents) != 1 || module.intents[0].Kind != app.ActivateProfile {
t.Fatalf("activation did not start exactly once: command=%v intents=%#v", command != nil, module.intents)
}
for _, key := range []string{"p", "?", "r", " "} {
updated, blocked := model.Update(keyMessage(key))
model = updated.(Model)
if blocked != nil || model.page != mainPage || len(module.intents) != 1 {
t.Fatalf("busy activation accepted %q: page=%v command=%v intents=%#v", key, model.page, blocked != nil, module.intents)
}
}
for _, key := range []tea.KeyMsg{keyMessage("q"), {Type: tea.KeyEsc}, {Type: tea.KeyCtrlC}} {
updated, quit := model.Update(key)
deferred := updated.(Model)
if quit != nil || !deferred.quitRequested || len(module.intents) != 1 {
t.Fatalf("busy activation did not defer quit %q: command=%v requested=%v intents=%#v", key.String(), quit != nil, deferred.quitRequested, module.intents)
}
}
// Even if a Profiles-page activation key is delivered while the transition
// is active, the single-operation guard must reject a second activation.
model.page = profilesPage
updated, blocked := model.Update(tea.KeyMsg{Type: tea.KeyEnter})
model = updated.(Model)
if blocked != nil || len(module.intents) != 1 {
t.Fatalf("busy activation started a second activation: command=%v intents=%#v", blocked != nil, module.intents)
}
}
func TestBusyRefreshBlocksTogglePageEntryRefreshAndActivation(t *testing.T) {
initial := app.Snapshot{
Profiles: []profile.Profile{{Name: "office"}},
Targets: []app.TargetSnapshot{{ID: "shell", Description: target.Description{Name: "Shell"}, Available: true, State: target.Disabled}},
}
module := &targetModule{snapshot: initial, result: app.Result{Snapshot: initial}}
model := NewModel(module)
updated, refreshCommand := model.Update(keyMessage("r"))
model = updated.(Model)
if refreshCommand == nil {
t.Fatal("refresh did not start")
}
for _, key := range []string{" ", "p", "?", "r"} {
updated, blocked := model.Update(keyMessage(key))
model = updated.(Model)
if blocked != nil || model.page != mainPage || len(module.intents) != 0 {
t.Fatalf("busy refresh accepted %q: page=%v command=%v intents=%#v", key, model.page, blocked != nil, module.intents)
}
}
for _, key := range []tea.KeyMsg{keyMessage("q"), {Type: tea.KeyEsc}, {Type: tea.KeyCtrlC}} {
updated, quit := model.Update(key)
deferred := updated.(Model)
if quit != nil || !deferred.quitRequested || len(module.intents) != 0 {
t.Fatalf("busy refresh did not defer quit %q: command=%v requested=%v intents=%#v", key.String(), quit != nil, deferred.quitRequested, module.intents)
}
}
model.page = profilesPage
updated, blocked := model.Update(tea.KeyMsg{Type: tea.KeyEnter})
if blocked != nil || len(module.intents) != 0 {
t.Fatalf("busy refresh started activation: command=%v intents=%#v", blocked != nil, module.intents)
}
}
func TestPendingActivationRemainsBusyUntilConfirmationIsResolved(t *testing.T) {
initial := app.Snapshot{Profiles: []profile.Profile{{Name: "office"}}}
updates := make(chan app.Result, 1)
updates <- app.Result{Snapshot: initial, Confirmation: "confirm transition"}
module := &progressiveTargetModule{targetModule: targetModule{snapshot: initial}, updates: updates}
model := updateKey(NewModel(module), "p")
updated, command := model.Update(tea.KeyMsg{Type: tea.KeyEnter})
model = updated.(Model)
updated, _ = model.Update(command())
model = updated.(Model)
if !model.busy() || model.pendingActivation.Kind != app.ActivateProfile || model.profileConfirm == nil {
t.Fatalf("pending activation was not guarded: %#v", model)
}
model.page = mainPage
for _, key := range []string{"p", "?", "r", " "} {
updated, blocked := model.Update(keyMessage(key))
model = updated.(Model)
if blocked != nil || model.page != mainPage || len(module.intents) != 1 {
t.Fatalf("pending activation accepted %q: page=%v command=%v intents=%#v", key, model.page, blocked != nil, module.intents)
}
}
model.page = profilesPage
updated, _ = model.Update(keyMessage("n"))
model = updated.(Model)
if model.busy() || model.profileConfirm != nil {
t.Fatal("cancelling confirmation did not clear pending activation")
}
}
func TestProfileConfirmationQuitKeysCancelWithoutLeavingBusyState(t *testing.T) {
for _, cancelKey := range []tea.KeyMsg{keyMessage("q"), {Type: tea.KeyEsc}} {
t.Run(cancelKey.String(), func(t *testing.T) {
initial := app.Snapshot{Profiles: []profile.Profile{{Name: "office"}}}
updates := make(chan app.Result, 2)
updates <- app.Result{Snapshot: initial, Confirmation: "confirm transition"}
module := &progressiveTargetModule{targetModule: targetModule{snapshot: initial}, updates: updates}
model := updateKey(NewModel(module), "p")
updated, command := model.Update(tea.KeyMsg{Type: tea.KeyEnter})
model = updated.(Model)
updated, command = model.Update(command())
model = updated.(Model)
if command != nil || model.profileConfirm == nil || !model.busy() {
t.Fatal("activation confirmation was not presented")
}
updated, command = model.Update(cancelKey)
model = updated.(Model)
if command != nil || model.profileConfirm != nil || model.busy() || model.quitRequested {
t.Fatalf("confirmation cancel %q left the model blocked: %#v", cancelKey.String(), model)
}
updates <- app.Result{Snapshot: initial, Confirmation: "confirm transition again"}
updated, command = model.Update(tea.KeyMsg{Type: tea.KeyEnter})
if command == nil {
t.Fatal("confirmation cancellation prevented a later activation")
}
})
}
}
func TestSingleOperationGuardRejectsMismatchedCompletionAndPreventsOverlap(t *testing.T) {
initial := app.Snapshot{Targets: []app.TargetSnapshot{{ID: "shell", Description: target.Description{Name: "Shell"}, Available: true, State: target.Disabled}}}
refreshed := app.Snapshot{ActiveProfile: "fresh", Targets: initial.Targets}
module := &targetModule{snapshot: initial, result: app.Result{Snapshot: refreshed}}
model := NewModel(module)
updated, refreshCommand := model.Update(keyMessage("r"))
model = updated.(Model)
updated, command := model.Update(keyMessage("q"))
model = updated.(Model)
if command != nil || !model.quitRequested {
t.Fatal("quit was not deferred during refresh")
}
updated, command = model.Update(targetOperationDone{result: app.Result{Snapshot: app.Snapshot{ActiveProfile: "stale"}}})
model = updated.(Model)
if command != nil || !model.refreshing || !model.busy() || model.snapshot.ActiveProfile != "" {
t.Fatal("completion from a non-current operation cleared or corrupted refresh state")
}
updated, command = model.Update(tea.KeyMsg{Type: tea.KeySpace})
model = updated.(Model)
if command != nil {
t.Fatal("Target operation overlapped the in-flight refresh")
}
refreshCompletion := refreshCommand()
updated, command = model.Update(refreshCompletion)
model = updated.(Model)
if command == nil || !model.busy() || model.snapshot.ActiveProfile != "fresh" {
t.Fatal("current refresh completion did not apply fresh state and enter terminal deferred-quit state")
}
if _, ok := command().(tea.QuitMsg); !ok {
t.Fatal("current completion did not return tea.Quit")
}
}
func TestBusyQuitWaitsForLongRunningTargetMutationCompletion(t *testing.T) {
for _, quitKey := range []tea.KeyMsg{keyMessage("q"), {Type: tea.KeyEsc}, {Type: tea.KeyCtrlC}} {
t.Run(quitKey.String(), func(t *testing.T) {
initial := app.Snapshot{ActiveProfile: "office", Targets: []app.TargetSnapshot{{ID: "shell", Description: target.Description{Name: "Shell"}, Available: true, State: target.Disabled}}}
completed := 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: completed, Notice: "Shell Target is now Enabled"}}
model := NewModel(module)
updated, operation := model.Update(tea.KeyMsg{Type: tea.KeySpace})
model = updated.(Model)
if operation == nil || len(module.intents) != 0 {
t.Fatal("Target mutation did not start asynchronously")
}
updated, quit := model.Update(quitKey)
model = updated.(Model)
if quit != nil || !model.quitRequested || len(module.intents) != 0 {
t.Fatalf("quit was not deferred before mutation completion: command=%v requested=%v intents=%d", quit != nil, model.quitRequested, len(module.intents))
}
completion := operation()
if len(module.intents) != 1 {
t.Fatal("deferred quit abandoned the in-flight mutation")
}
updated, quit = model.Update(completion)
model = updated.(Model)
if quit == nil || !model.busy() || model.snapshot.Targets[0].State != target.Enabled {
t.Fatalf("mutation completion was not consumed before terminal deferred quit: busy=%v snapshot=%#v", model.busy(), model.snapshot)
}
if _, ok := quit().(tea.QuitMsg); !ok {
t.Fatal("completion did not return tea.Quit")
}
})
}
}
func TestDeferredQuitIsTerminalAfterTargetConfirmationCompletion(t *testing.T) {
initial := app.Snapshot{
ActiveProfile: "office",
Profiles: []profile.Profile{{Name: "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."}}
model := NewModel(module)
updated, operation := model.Update(tea.KeyMsg{Type: tea.KeySpace})
model = updated.(Model)
updated, command := model.Update(keyMessage("q"))
model = updated.(Model)
if command != nil || !model.quitRequested {
t.Fatal("quit was not deferred during Target operation")
}
updated, queuedQuit := model.Update(operation())
model = updated.(Model)
if queuedQuit == nil || model.targetConfirm == nil || len(module.intents) != 1 {
t.Fatalf("confirmation completion did not queue quit: command=%v confirmation=%v intents=%#v", queuedQuit != nil, model.targetConfirm != nil, module.intents)
}
baselineSnapshot := model.snapshot
baselineStatus := model.status
baselinePage := model.page
baselineSelected := model.selected
baselineConfirmation := *model.targetConfirm
for _, message := range []tea.Msg{
keyMessage("y"), keyMessage("n"), tea.KeyMsg{Type: tea.KeyEsc}, keyMessage(" "),
keyMessage("r"), keyMessage("p"), keyMessage("?"), keyMessage("j"), queuedQuit(),
} {
updated, command = model.Update(message)
model = updated.(Model)
if command != nil {
t.Fatalf("terminal deferred-quit model returned a command for %T", message)
}
if !model.quitRequested || !reflect.DeepEqual(model.snapshot, baselineSnapshot) || model.status != baselineStatus || model.page != baselinePage || model.selected != baselineSelected || model.targetConfirm == nil || !reflect.DeepEqual(*model.targetConfirm, baselineConfirmation) || len(module.intents) != 1 {
t.Fatalf("terminal deferred-quit model changed for %T: model=%#v intents=%#v", message, model, module.intents)
}
}
}
func TestDeferredQuitIsTerminalAfterProfileConfirmationCompletion(t *testing.T) {
initial := app.Snapshot{Profiles: []profile.Profile{{Name: "office"}}}
updates := make(chan app.Result, 1)
updates <- app.Result{Snapshot: initial, Confirmation: "confirm transition"}
module := &progressiveTargetModule{targetModule: targetModule{snapshot: initial}, updates: updates}
model := updateKey(NewModel(module), "p")
updated, operation := model.Update(tea.KeyMsg{Type: tea.KeyEnter})
model = updated.(Model)
updated, command := model.Update(keyMessage("q"))
model = updated.(Model)
if command != nil || !model.quitRequested {
t.Fatal("quit was not deferred during Profile activation")
}
updated, queuedQuit := model.Update(operation())
model = updated.(Model)
if queuedQuit == nil || model.profileConfirm == nil || len(module.intents) != 1 {
t.Fatalf("Profile confirmation completion did not queue quit: command=%v confirmation=%v intents=%#v", queuedQuit != nil, model.profileConfirm != nil, module.intents)
}
baselineSnapshot := model.snapshot
baselineStatus := model.status
baselinePage := model.page
baselineSelected := model.selected
baselinePending := model.pendingActivation
baselineConfirmation := *model.profileConfirm
for _, message := range []tea.Msg{
keyMessage("y"), keyMessage("n"), tea.KeyMsg{Type: tea.KeyEsc}, tea.KeyMsg{Type: tea.KeyEnter},
keyMessage("d"), keyMessage("e"), keyMessage("a"), keyMessage("r"), queuedQuit(),
} {
updated, command = model.Update(message)
model = updated.(Model)
if command != nil {
t.Fatalf("terminal deferred-quit Profile model returned a command for %T", message)
}
if !model.quitRequested || !reflect.DeepEqual(model.snapshot, baselineSnapshot) || model.status != baselineStatus || model.page != baselinePage || model.selected != baselineSelected || !reflect.DeepEqual(model.pendingActivation, baselinePending) || model.profileConfirm == nil || !reflect.DeepEqual(*model.profileConfirm, baselineConfirmation) || len(module.intents) != 1 {
t.Fatalf("terminal deferred-quit Profile model changed for %T: model=%#v intents=%#v", message, model, module.intents)
}
}
}
func TestRefreshClampsSelectionAfterMultipleTargetsDisappear(t *testing.T) {
initial := app.Snapshot{Targets: []app.TargetSnapshot{
{ID: "one", Description: target.Description{Name: "One"}, Available: true},
{ID: "two", Description: target.Description{Name: "Two"}, Available: true},
{ID: "three", Description: target.Description{Name: "Three"}, Available: true},
{ID: "four", Description: target.Description{Name: "Four"}, Available: true},
}}
refreshed := app.Snapshot{Targets: []app.TargetSnapshot{{ID: "one", Description: target.Description{Name: "One"}, Available: true}}}
module := &targetModule{snapshot: initial, result: app.Result{Snapshot: refreshed}}
model := NewModel(module)
model.selected = 3
updated, command := model.Update(keyMessage("r"))
model = updated.(Model)
updated, _ = model.Update(command())
model = updated.(Model)
if model.selected != 0 || !strings.Contains(model.View(), "> [ ] One") {
t.Fatalf("selection after multi-Target reduction = %d\n%s", model.selected, model.View())
}
module.result = app.Result{Snapshot: app.Snapshot{}}
updated, command = model.Update(keyMessage("r"))
model = updated.(Model)
updated, _ = model.Update(command())
model = updated.(Model)
if model.selected != 0 {
t.Fatalf("selection with no Available Targets = %d, want 0", model.selected)
}
}
func keyMessage(key string) tea.KeyMsg {
if key == " " {
return tea.KeyMsg{Type: tea.KeySpace}
}
return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(key)}
}
func TestTargetPanelNavigationKeysAndQuitCombinations(t *testing.T) {
module := &targetModule{snapshot: app.Snapshot{Targets: []app.TargetSnapshot{
{ID: "first", Description: target.Description{Name: "First", Summary: "first summary"}, Available: true},
{ID: "second", Description: target.Description{Name: "Second", Summary: "second summary"}, Available: true},
}}}
for _, down := range []tea.KeyMsg{{Type: tea.KeyDown}, {Type: tea.KeyRunes, Runes: []rune{'j'}}} {
model := NewModel(module)
updated, _ := model.Update(down)
model = updated.(Model)
if !strings.Contains(model.View(), "> [ ] Second") {
t.Fatalf("down key %q did not select second Target:\n%s", down.String(), model.View())
}
for _, up := range []tea.KeyMsg{{Type: tea.KeyUp}, {Type: tea.KeyRunes, Runes: []rune{'k'}}} {
updated, _ = model.Update(up)
if !strings.Contains(updated.(Model).View(), "> [ ] First") {
t.Fatalf("up key %q did not select first Target:\n%s", up.String(), updated.(Model).View())
}
}
}
for _, quit := range []tea.KeyMsg{{Type: tea.KeyRunes, Runes: []rune{'q'}}, {Type: tea.KeyEsc}} {
_, command := NewModel(module).Update(quit)
if command == nil {
t.Fatalf("quit key %q returned no command", quit.String())
}
if _, ok := command().(tea.QuitMsg); !ok {
t.Fatalf("quit key %q command did not return tea.QuitMsg", quit.String())
}
}
}
func TestQAndEscapeReturnFromNestedPages(t *testing.T) {
module := &targetModule{}
for _, key := range []tea.KeyMsg{{Type: tea.KeyRunes, Runes: []rune{'q'}}, {Type: tea.KeyEsc}} {
model := updateKey(NewModel(module), "?")
updated, command := model.Update(key)
if command != nil || !strings.Contains(updated.(Model).View(), "Target panel") {
t.Fatalf("help key %q did not return to Target panel", key.String())
}
model = updateKey(NewModel(module), "p")
updated, command = model.Update(key)
if command != nil || !strings.Contains(updated.(Model).View(), "Target panel") {
t.Fatalf("Profile key %q did not return to Target panel", key.String())
}
}
}
type progressiveTargetModule struct {
targetModule
updates chan app.Result
}
func (m *progressiveTargetModule) DispatchProgress(intent app.Intent) <-chan app.Result {
m.intents = append(m.intents, intent)
return m.updates
}
func TestActiveProfileTransitionRendersPerTargetProgress(t *testing.T) {
initial := app.Snapshot{ActiveProfile: "old", Profiles: []profile.Profile{{Name: "new"}}, Targets: []app.TargetSnapshot{{ID: "shell", Description: target.Description{Name: "Shell"}, Available: true, State: target.Enabled}}}
updates := make(chan app.Result, 3)
updates <- app.Result{Snapshot: app.Snapshot{ActiveProfile: "new", Targets: initial.Targets}, Progress: []app.TargetProgress{{TargetID: "shell", Applying: true}}}
updates <- app.Result{Snapshot: app.Snapshot{ActiveProfile: "new", Targets: []app.TargetSnapshot{{ID: "shell", Description: target.Description{Name: "Shell"}, Available: true, State: target.Enabled}}}, Progress: []app.TargetProgress{{TargetID: "shell", State: target.Enabled}}, Notice: "Shell Target update completed"}
updates <- app.Result{Snapshot: app.Snapshot{ActiveProfile: "new", Targets: []app.TargetSnapshot{{ID: "shell", Description: target.Description{Name: "Shell"}, Available: true, State: target.Enabled}}}, Complete: true, Notice: "Activated Profile new"}
close(updates)
module := &progressiveTargetModule{targetModule: targetModule{snapshot: initial}, updates: updates}
model := updateKey(NewModel(module), "p")
updated, command := model.Update(tea.KeyMsg{Type: tea.KeyEnter})
model = updated.(Model)
if command == nil {
t.Fatal("activation did not start progressive operation")
}
updated, command = model.Update(command())
model = updated.(Model)
if command == nil || !strings.Contains(model.View(), "Applying") || !strings.Contains(model.View(), "Active Profile: new") {
t.Fatalf("applying progress:\n%s", model.View())
}
updated, command = model.Update(command())
model = updated.(Model)
if command == nil || !strings.Contains(model.View(), "Enabled") || !strings.Contains(model.View(), "update completed") {
t.Fatalf("Target completion:\n%s", model.View())
}
updated, _ = model.Update(command())
model = updated.(Model)
if !strings.Contains(model.View(), "Activated Profile new") {
t.Fatalf("final transition:\n%s", model.View())
}
}
func TestTargetPanelRendersEveryTargetState(t *testing.T) {
states := []target.State{target.Enabled, target.Disabled, target.External, target.Drifted}
var targets []app.TargetSnapshot
for _, state := range states {
targets = append(targets, app.TargetSnapshot{ID: string(state), Description: target.Description{Name: string(state)}, Available: true, State: state})
}
view := NewModel(&targetModule{snapshot: app.Snapshot{Targets: targets}}).View()
for _, state := range states {
if !strings.Contains(view, string(state)) {
t.Fatalf("missing state %s:\n%s", state, view)
}
}
}
func TestDriftedTargetWithoutActiveProfileTogglesTowardDisabled(t *testing.T) {
initial := app.Snapshot{Targets: []app.TargetSnapshot{{ID: "shell", Description: target.Description{Name: "Shell"}, Available: true, State: target.Drifted}}}
module := &targetModule{snapshot: initial, result: app.Result{Snapshot: app.Snapshot{Targets: []app.TargetSnapshot{{ID: "shell", Description: target.Description{Name: "Shell"}, Available: true, State: target.Disabled}}}}}
model := NewModel(module)
_, command := model.Update(tea.KeyMsg{Type: tea.KeySpace})
if command == nil {
t.Fatal("Drifted Target did not start disable")
}
_ = command()
if len(module.intents) != 1 || module.intents[0].Enabled {
t.Fatalf("Drifted Target intent = %#v", module.intents)
}
}
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, "Error:") || !strings.Contains(view, "REDACTED") {
t.Fatalf("failed operation rendering:\n%s", view)
}
}