feat: add contextual Target actions

This commit is contained in:
proxyctl coordinator
2026-07-17 23:14:57 +08:00
parent 2ca42bd4bd
commit 2ac088b388
16 changed files with 428 additions and 194 deletions
+15 -9
View File
@@ -18,10 +18,9 @@ type targetModule struct {
intents []app.Intent
}
func (m *targetModule) Snapshot() app.Snapshot { return withTestTargetActions(m.snapshot) }
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.result.Snapshot = withTestTargetActions(m.result.Snapshot)
m.snapshot = m.result.Snapshot
return m.result
}
@@ -44,10 +43,10 @@ func (m *targetModule) StartOperation(intent app.Intent) app.OperationStream {
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: "shell", Description: target.Description{Name: "Shell", Summary: "Exports for Bash/Zsh"}, Available: true, State: target.External, Evidence: "existing proxy", PrimaryAction: app.TargetAction{Kind: app.ApplyProfileTargetAction, Available: true}},
{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"}
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", PrimaryAction: app.TargetAction{Kind: app.DisableTargetAction, Available: true}}}}, Notice: "Shell Target is now Enabled"}
model := NewModel(module)
view := model.View()
if !strings.Contains(view, "Shell") || strings.Contains(view, "APT") || !strings.Contains(view, "Action: Apply Profile") {
@@ -68,7 +67,7 @@ func TestTargetPanelShowsOnlyAvailableTargetsAndAppliesAsynchronously(t *testing
}
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}}}
initial := app.Snapshot{ActiveProfile: "office", Targets: []app.TargetSnapshot{{ID: "docker-daemon", Description: target.Description{Name: "Docker daemon"}, Available: true, State: target.Disabled, PrimaryAction: app.TargetAction{Kind: app.EnableTargetAction, Available: true}}}}
module := &targetModule{snapshot: initial, result: app.Result{Snapshot: initial, ConfirmationRequest: &app.Confirmation{Title: "Confirm Docker restart", Summary: "Docker will restart immediately and may disrupt running work.", ConfirmLabel: "Continue", CancelLabel: "Cancel"}}}
model := NewModel(module)
updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace})
@@ -89,7 +88,7 @@ func TestTargetRestartConfirmationCancelsWithoutConfirmedMutation(t *testing.T)
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}}},
Snapshot: app.Snapshot{ActiveProfile: "office", Targets: []app.TargetSnapshot{{ID: "docker-daemon", Description: target.Description{Name: "Docker daemon"}, Available: true, State: target.Enabled, PrimaryAction: app.TargetAction{Kind: app.DisableTargetAction, Available: true}}}},
Notice: "Docker daemon Target is now Enabled",
}
updated, command = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'y'}})
@@ -105,7 +104,7 @@ func TestTargetRestartConfirmationCancelsWithoutConfirmedMutation(t *testing.T)
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: "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", PrimaryAction: app.TargetAction{Kind: app.EnableTargetAction, Available: false, DisabledReason: "This action is unavailable because there is no Active Profile; open Profile management first"}},
{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)
@@ -348,7 +347,11 @@ func TestTargetPanelRendersEveryTargetState(t *testing.T) {
}
func TestDriftedTargetWithoutActiveProfileRequiresExplicitResolutionDirection(t *testing.T) {
initial := withTestTargetActions(app.Snapshot{Targets: []app.TargetSnapshot{{ID: "shell", Description: target.Description{Name: "Shell"}, Available: true, State: target.Drifted}}})
initial := app.Snapshot{Targets: []app.TargetSnapshot{{
ID: "shell", Description: target.Description{Name: "Shell"}, Available: true, State: target.Drifted,
PrimaryAction: app.TargetAction{Kind: app.ResolveDriftTargetAction, Available: true},
DriftResolutionActions: []app.TargetAction{{Kind: app.ReapplyActiveProfileAction}, {Kind: app.RestoreOriginalAction, Available: true}},
}}}
initial.Targets[0].DriftResolutionActions[0].DisabledReason = "Reapply Active Profile is unavailable because there is no Active Profile"
module := &targetModule{snapshot: initial}
model := NewModel(module)
@@ -360,7 +363,10 @@ func TestDriftedTargetWithoutActiveProfileRequiresExplicitResolutionDirection(t
}
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}}}
initial := app.Snapshot{ActiveProfile: "office", Targets: []app.TargetSnapshot{{
ID: "shell", Description: target.Description{Name: "Shell"}, Available: true, State: target.Enabled,
PrimaryAction: app.TargetAction{Kind: app.DisableTargetAction, Available: true},
}}}
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})