Files
proxy/internal/tui/contextual_actions_test.go
2026-08-30 15:49:04 +08:00

167 lines
7.6 KiB
Go

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())
}
}