feat: unify interactive operation coordination
This commit is contained in:
@@ -57,9 +57,9 @@ func TestCompleteModelKeepsDashboardAndHelpBrowsableWhileMutationIsSerialized(t
|
||||
|
||||
updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||
model = updated.(Model)
|
||||
updates <- app.OperationUpdate{Snapshot: module.snapshot, Phase: app.OperationRunning,
|
||||
Status: app.OperationStatus{Kind: app.StatusProgress, Summary: "Writing Shell configuration", TargetID: "shell"},
|
||||
Progress: []app.TargetProgress{{TargetID: "shell", Phase: app.TargetProgressRunning}}}
|
||||
updates <- app.RunningOperationUpdate(module.snapshot,
|
||||
app.OperationStatus{Summary: "Writing Shell configuration", TargetID: "shell"},
|
||||
[]app.TargetProgress{{TargetID: "shell", Phase: app.TargetProgressRunning}})
|
||||
model, command = applyOperationCommand(t, model, command)
|
||||
assertUsableFrame(t, model.View(), 120, 36, "Applying", "Writing Shell configuration")
|
||||
|
||||
@@ -96,9 +96,10 @@ func TestCompleteModelPreservesExpandableCurrentErrorAndFocusesFirstFailedTarget
|
||||
failedSnapshot := operationSnapshot()
|
||||
failedSnapshot.Targets[0].State = target.Enabled
|
||||
failedSnapshot.Targets[1].State = target.Drifted
|
||||
failure <- app.OperationUpdate{Snapshot: failedSnapshot, Phase: app.OperationFailed, FirstFailedTargetID: "apt", Err: errors.New("apt write failed: permission denied"),
|
||||
Status: app.OperationStatus{Kind: app.StatusError, Summary: "Active Profile changed with one Target failure", Detail: "apt write failed: permission denied", TargetID: "apt"},
|
||||
Progress: []app.TargetProgress{{TargetID: "shell", Phase: app.TargetProgressSucceeded}, {TargetID: "apt", Phase: app.TargetProgressFailed}}}
|
||||
failure <- app.FailedOperationUpdate(failedSnapshot,
|
||||
app.OperationStatus{Summary: "Active Profile changed with one Target failure", TargetID: "apt"},
|
||||
[]app.TargetProgress{{TargetID: "shell", Phase: app.TargetProgressSucceeded}, {TargetID: "apt", Phase: app.TargetProgressFailed}},
|
||||
"apt", errors.New("apt write failed: permission denied"))
|
||||
close(failure)
|
||||
model, _ = applyOperationCommand(t, model, command)
|
||||
if model.selectedTargetID != "apt" {
|
||||
@@ -115,19 +116,57 @@ func TestCompleteModelPreservesExpandableCurrentErrorAndFocusesFirstFailedTarget
|
||||
|
||||
updated, command = model.Update(keyMessage("r"))
|
||||
model = updated.(Model)
|
||||
refresh <- app.OperationUpdate{Snapshot: failedSnapshot, Phase: app.OperationSucceeded,
|
||||
Status: app.OperationStatus{Kind: app.StatusSuccess, Summary: "Target checks completed"}}
|
||||
refresh <- app.SucceededOperationUpdate(failedSnapshot,
|
||||
app.OperationStatus{Summary: "Shell Target check completed", TargetID: "shell"}, nil)
|
||||
close(refresh)
|
||||
model, _ = applyOperationCommand(t, model, command)
|
||||
if !strings.Contains(model.View(), "Current error: Active Profile changed with one Target failure") {
|
||||
t.Fatalf("unrelated success replaced current error:\n%s", model.View())
|
||||
}
|
||||
model = updateKey(model, "x")
|
||||
if strings.Contains(model.View(), "Current error:") {
|
||||
if strings.Contains(model.View(), "Current error:") || strings.Contains(model.View(), "Active Profile changed with one Target failure") {
|
||||
t.Fatalf("acknowledged error remained visible:\n%s", model.View())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteModelClearsCurrentErrorForDirectlyRelatedSuccessfulDetection(t *testing.T) {
|
||||
failure := make(chan app.OperationUpdate, 1)
|
||||
resolution := make(chan app.OperationUpdate, 1)
|
||||
module := &interactiveScriptModule{snapshot: operationSnapshot(), streams: []chan app.OperationUpdate{failure, resolution}}
|
||||
model := resizedModel(t, module, themeModeNoColor, 120, 36)
|
||||
|
||||
updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||
model = updated.(Model)
|
||||
failure <- app.FailedOperationUpdate(module.snapshot,
|
||||
app.OperationStatus{Summary: "Shell Target update failed", TargetID: "shell"}, nil, "shell", errors.New("permission denied"))
|
||||
close(failure)
|
||||
model, _ = applyOperationCommand(t, model, command)
|
||||
|
||||
updated, command = model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||
model = updated.(Model)
|
||||
resolution <- app.SucceededOperationUpdate(module.snapshot,
|
||||
app.OperationStatus{Summary: "Shell Target check completed", TargetID: "shell"},
|
||||
[]app.TargetProgress{{TargetID: "shell", Phase: app.TargetProgressSucceeded}})
|
||||
close(resolution)
|
||||
model, _ = applyOperationCommand(t, model, command)
|
||||
if strings.Contains(model.View(), "Current error:") {
|
||||
t.Fatalf("explicit related resolution did not clear the current error:\n%s", model.View())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteModelReportsOperationStreamClosingWithoutFinalResult(t *testing.T) {
|
||||
updates := make(chan app.OperationUpdate)
|
||||
module := &interactiveScriptModule{snapshot: operationSnapshot(), streams: []chan app.OperationUpdate{updates}}
|
||||
model := resizedModel(t, module, themeModeNoColor, 120, 36)
|
||||
updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||
model = updated.(Model)
|
||||
close(updates)
|
||||
model, _ = applyOperationCommand(t, model, command)
|
||||
if !strings.Contains(model.View(), "Current error: Operation ended without a final result") || model.operation.active {
|
||||
t.Fatalf("unexpected stream closure was not deterministic:\n%s", model.View())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteModelUsesStructuredConfirmationAndSafeCancellation(t *testing.T) {
|
||||
confirmation := make(chan app.OperationUpdate, 1)
|
||||
module := &interactiveScriptModule{snapshot: operationSnapshot(), streams: []chan app.OperationUpdate{confirmation}}
|
||||
@@ -135,9 +174,8 @@ func TestCompleteModelUsesStructuredConfirmationAndSafeCancellation(t *testing.T
|
||||
|
||||
updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||
model = updated.(Model)
|
||||
confirmation <- app.OperationUpdate{Snapshot: module.snapshot, Phase: app.OperationAwaitingConfirmation,
|
||||
Status: app.OperationStatus{Kind: app.StatusConfirmation, Summary: "Confirm Docker restart"},
|
||||
Confirmation: &app.Confirmation{Title: "Confirm Docker restart", Summary: "Running work may be disrupted.", ConfirmLabel: "Restart", CancelLabel: "Cancel", HighImpact: true}}
|
||||
confirmation <- app.ConfirmationOperationUpdate(module.snapshot,
|
||||
app.Confirmation{Title: "Confirm Docker restart", Summary: "Running work may be disrupted.", ConfirmLabel: "Restart", CancelLabel: "Cancel", HighImpact: true})
|
||||
close(confirmation)
|
||||
model, _ = applyOperationCommand(t, model, command)
|
||||
view := model.View()
|
||||
@@ -145,8 +183,29 @@ func TestCompleteModelUsesStructuredConfirmationAndSafeCancellation(t *testing.T
|
||||
|
||||
updated, next := model.Update(tea.KeyMsg{Type: tea.KeyEnter})
|
||||
model = updated.(Model)
|
||||
if next != nil || model.targetConfirm != nil || len(module.intents) != 1 {
|
||||
t.Fatalf("safe-default Enter did not cancel: confirmation=%v intents=%#v", model.targetConfirm != nil, module.intents)
|
||||
if next != nil || model.operation.confirmation != nil || len(module.intents) != 1 {
|
||||
t.Fatalf("safe-default Enter did not cancel: confirmation=%v intents=%#v", model.operation.confirmation != nil, module.intents)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteModelExplainsDisabledTargetMutations(t *testing.T) {
|
||||
checking := operationSnapshot()
|
||||
checking.ActiveProfile = ""
|
||||
module := &interactiveScriptModule{snapshot: checking}
|
||||
model := resizedModel(t, module, themeModeNoColor, 120, 36)
|
||||
model.snapshot.Targets[0].DetectionPhase = app.TargetChecking
|
||||
|
||||
updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||
model = updated.(Model)
|
||||
if command != nil || !strings.Contains(model.View(), "disabled while this Target is still being checked") {
|
||||
t.Fatalf("Checking mutation lacked a visible reason:\n%s", model.View())
|
||||
}
|
||||
|
||||
model.snapshot.Targets[0].DetectionPhase = app.TargetReady
|
||||
updated, command = model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||
model = updated.(Model)
|
||||
if command != nil || !strings.Contains(model.View(), "disabled because there is no Active Profile") {
|
||||
t.Fatalf("missing-Profile mutation lacked a visible reason:\n%s", model.View())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +222,16 @@ func TestCompleteModelVisiblyDefersCtrlCFromHelpAndStartsNoNewWork(t *testing.T)
|
||||
if quit != nil || !strings.Contains(model.View(), "Exit requested — waiting for the current mutation to finish safely") {
|
||||
t.Fatalf("deferred exit was not visible from Help:\n%s", model.View())
|
||||
}
|
||||
model = updateKey(model, "q")
|
||||
model = updateKey(model, "down")
|
||||
if model.selectedTargetID != "apt" {
|
||||
t.Fatalf("deferred exit prevented read-only Target browsing: selected=%q", model.selectedTargetID)
|
||||
}
|
||||
model = updateKey(model, "?")
|
||||
if model.page != helpPage {
|
||||
t.Fatal("deferred exit prevented opening read-only Help")
|
||||
}
|
||||
model = updateKey(model, "q")
|
||||
for _, key := range []tea.KeyMsg{tea.KeyMsg{Type: tea.KeySpace}, keyMessage("r"), keyMessage("p")} {
|
||||
updated, command := model.Update(key)
|
||||
model = updated.(Model)
|
||||
@@ -171,8 +240,9 @@ func TestCompleteModelVisiblyDefersCtrlCFromHelpAndStartsNoNewWork(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
updates <- app.OperationUpdate{Snapshot: module.snapshot, Phase: app.OperationSucceeded,
|
||||
Status: app.OperationStatus{Kind: app.StatusSuccess, Summary: "Shell Target is now Enabled", TargetID: "shell"}}
|
||||
updates <- app.SucceededOperationUpdate(module.snapshot,
|
||||
app.OperationStatus{Summary: "Shell Target is now Enabled", TargetID: "shell"},
|
||||
[]app.TargetProgress{{TargetID: "shell", Phase: app.TargetProgressSucceeded}})
|
||||
close(updates)
|
||||
model, quit = applyOperationCommand(t, model, operation)
|
||||
if quit == nil {
|
||||
|
||||
Reference in New Issue
Block a user