167 lines
7.6 KiB
Go
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())
|
|
}
|
|
}
|