package tui import ( "errors" "strings" "testing" tea "github.com/charmbracelet/bubbletea" "proxyctl/internal/app" "proxyctl/internal/target" ) func completeTargetSnapshot(state target.State, primary app.TargetAction) app.Snapshot { return app.Snapshot{ActiveProfile: "office", Targets: []app.TargetSnapshot{{ ID: "shell", Description: target.Description{Name: "Shell"}, Available: true, State: state, PrimaryAction: primary, }}} } func TestCompleteModelRendersAndDispatchesContextualPrimaryActions(t *testing.T) { tests := []struct { name string state target.State action app.TargetActionKind intent app.IntentKind label string }{ {name: "Disabled", state: target.Disabled, action: app.EnableTargetAction, intent: app.EnableTarget, label: "Enable"}, {name: "Enabled", state: target.Enabled, action: app.DisableTargetAction, intent: app.DisableTarget, label: "Disable"}, {name: "External", state: target.External, action: app.ApplyProfileTargetAction, intent: app.ApplyProfileToTarget, label: "Apply Profile"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { initial := completeTargetSnapshot(test.state, app.TargetAction{Kind: test.action, Available: true}) module := &targetModule{snapshot: initial, result: app.Result{Snapshot: initial, Notice: "done"}} model := resizedModel(t, module, themeModeNoColor, 120, 36) view := model.View() if !strings.Contains(view, "Space: "+test.label) { t.Fatalf("contextual action not rendered:\n%s", view) } updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace}) model = updated.(Model) if command == nil || len(module.intents) != 1 || module.intents[0].Kind != test.intent { t.Fatalf("intent = %#v, command=%v", module.intents, command != nil) } }) } } func TestCompleteModelOffersBothExplicitDriftResolutionIntents(t *testing.T) { initial := completeTargetSnapshot(target.Drifted, app.TargetAction{Kind: app.ResolveDriftTargetAction, Available: true}) initial.Targets[0].DriftResolutionActions = []app.TargetAction{ {Kind: app.ReapplyActiveProfileAction, Available: true}, {Kind: app.RestoreOriginalAction, Available: true}, } for _, test := range []struct { key string intent app.IntentKind }{ {key: "r", intent: app.ReapplyActiveProfile}, {key: "o", intent: app.RestoreOriginal}, } { t.Run(test.key, func(t *testing.T) { module := &targetModule{snapshot: initial, result: app.Result{Snapshot: initial, Notice: "done"}} model := resizedModel(t, module, themeModeNoColor, 120, 36) model = updateKey(model, " ") view := model.View() for _, want := range []string{"Drift Resolution", "Reapply Active Profile", "Restore Original"} { if !strings.Contains(view, want) { t.Fatalf("missing %q:\n%s", want, view) } } updated, command := model.Update(keyMessage(test.key)) model = updated.(Model) if command == nil || len(module.intents) != 1 || module.intents[0].Kind != test.intent { t.Fatalf("resolution intent = %#v, command=%v", module.intents, command != nil) } }) } } func TestCompleteModelRendersUnknownTargetStateAsStructurallyUnavailable(t *testing.T) { const reason = "Target actions are unavailable because this Target state is unknown" initial := completeTargetSnapshot("", app.TargetAction{DisabledReason: reason}) initial.Targets[0].PrimaryAction.Available = false module := &targetModule{snapshot: initial} model := resizedModel(t, module, themeModeNoColor, 120, 36) view := model.View() if !strings.Contains(view, "Unavailable:") || !strings.Contains(view, "Target actions are unavailable") || !strings.Contains(view, "unknown") || !strings.Contains(view, "Space: Unavailable unavailable") { t.Fatalf("unknown Target action was not structurally unavailable:\n%s", view) } updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace}) model = updated.(Model) if command != nil || len(module.intents) != 0 || model.operation.status.Summary != reason { t.Fatalf("unknown Target action dispatched: intents=%#v status=%q\n%s", module.intents, model.operation.status.Summary, model.View()) } } func TestCompleteModelUsesStructuredDisabledReasonWithoutInferringFromState(t *testing.T) { initial := completeTargetSnapshot(target.Disabled, app.TargetAction{ Kind: app.EnableTargetAction, DisabledReason: "No Active Profile is selected; create or select one before enabling", }) module := &targetModule{snapshot: initial} model := resizedModel(t, module, themeModeNoColor, 120, 36) view := model.View() if !strings.Contains(view, "No Active Profile is selected") || !strings.Contains(view, "Space: Enable unavailable") { t.Fatalf("structured disabled reason missing:\n%s", view) } updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace}) model = updated.(Model) if command != nil || len(module.intents) != 0 || !strings.Contains(model.View(), "No Active Profile is selected") { t.Fatalf("disabled action mutated: intents=%#v\n%s", module.intents, model.View()) } } func TestCompleteModelPressingUnavailableTargetActionShowsExactReasonAndDispatchesNoIntent(t *testing.T) { const reason = "GNOME settings schema is unavailable for the owning user" initial := completeTargetSnapshot(target.External, app.TargetAction{ Kind: app.ApplyProfileTargetAction, Available: false, DisabledReason: reason, }) initial.Targets[0].Available = false module := &targetModule{snapshot: initial} model := resizedModel(t, module, themeModeNoColor, 120, 36) model = updateKey(model, "u") updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace}) model = updated.(Model) if command != nil || len(module.intents) != 0 || model.operation.status.Summary != reason { t.Fatalf("unavailable action: command=%v intents=%#v status=%q", command != nil, module.intents, model.operation.status.Summary) } } func TestCompleteModelRestoreOwnershipLossFailsTruthfully(t *testing.T) { initial := completeTargetSnapshot(target.Drifted, app.TargetAction{Kind: app.ResolveDriftTargetAction, Available: true}) initial.Targets[0].DriftResolutionActions = []app.TargetAction{ {Kind: app.ReapplyActiveProfileAction, Available: true}, {Kind: app.RestoreOriginalAction, Available: true}, } module := &targetModule{snapshot: initial, result: app.Result{Snapshot: initial, Err: errors.New("restoration evidence is insufficient; external changes were not overwritten")}} model := resizedModel(t, module, themeModeNoColor, 120, 36) model = updateKey(model, " ") updated, command := model.Update(keyMessage("o")) model = updated.(Model) if command == nil { t.Fatal("Restore Original did not start") } model, _ = applyOperationCommand(t, model, command) if len(module.intents) != 1 || module.intents[0].Kind != app.RestoreOriginal || !strings.Contains(model.View(), "Current error") || !strings.Contains(model.View(), "Drifted") { t.Fatalf("ownership-loss result: intents=%#v\n%s", module.intents, model.View()) } } func TestCompleteModelCancelsDriftResolutionWithoutMutation(t *testing.T) { initial := completeTargetSnapshot(target.Drifted, app.TargetAction{Kind: app.ResolveDriftTargetAction, Available: true}) initial.Targets[0].DriftResolutionActions = []app.TargetAction{{Kind: app.ReapplyActiveProfileAction, Available: true}, {Kind: app.RestoreOriginalAction, Available: true}} module := &targetModule{snapshot: initial} model := resizedModel(t, module, themeModeNoColor, 120, 36) model = updateKey(model, " ") updated, command := model.Update(tea.KeyMsg{Type: tea.KeyEnter}) model = updated.(Model) if command != nil || len(module.intents) != 0 || strings.Contains(model.View(), "r: Reapply Active Profile") { t.Fatalf("safe-default cancellation mutated: intents=%#v\n%s", module.intents, model.View()) } }