fix: simplify dashboard and disable unauthorized actions
hermetic-ci / test (push) Canceled after 0s
hermetic-ci / test (push) Canceled after 0s
This commit is contained in:
@@ -97,8 +97,8 @@ func TestDefaultInvocationLoadsPersistedActiveProfile(t *testing.T) {
|
||||
if err := runWithSystemTargets(nil, strings.NewReader("q"), &output, []target.Target{}); err != nil {
|
||||
t.Fatalf("run proxyctl: %v", err)
|
||||
}
|
||||
if !strings.Contains(output.String(), "Active Profile: office") {
|
||||
t.Fatalf("default TUI output missing Active Profile: %q", output.String())
|
||||
if !strings.Contains(output.String(), "Profile: office") {
|
||||
t.Fatalf("default TUI output missing Profile: %q", output.String())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -247,19 +247,17 @@ func TestCommandsRunAsRealProcessesInsideIsolatedUserState(t *testing.T) {
|
||||
}
|
||||
defaultView := string(defaultOutput)
|
||||
for _, expected := range []string{
|
||||
"proxyctl Operations Console",
|
||||
"Active Profile: No Active Profile",
|
||||
"Targets: ● Enabled 0 ○ Disabled 1 ◇ External 0 ! Drifted 1",
|
||||
"proxyctl Profile: No Active Profile",
|
||||
"▶ Targets [focused]",
|
||||
"> ○ Disabled Shell",
|
||||
"! Drifted Docker daemon",
|
||||
"Target details",
|
||||
"State: ○ Disabled",
|
||||
"Evidence: no recognized Shell proxy configuration detected",
|
||||
"Tab: focus details",
|
||||
"○ Disabled",
|
||||
"no recognized Shell proxy configuration detected",
|
||||
"Tab: Details",
|
||||
} {
|
||||
if !strings.Contains(defaultView, expected) {
|
||||
t.Errorf("default Operations Console missing %q; output=%q", expected, defaultOutput)
|
||||
t.Errorf("default dashboard missing %q; output=%q", expected, defaultOutput)
|
||||
}
|
||||
}
|
||||
versionOutput, err := invoke("", "version", "--short")
|
||||
|
||||
+36
-5
@@ -164,6 +164,7 @@ type Application struct {
|
||||
targetDescriptions []target.Description
|
||||
availabilities map[string]target.Availability
|
||||
inspections map[string]target.Inspection
|
||||
mutationBlockReasons map[string]string
|
||||
detectionPhase map[string]TargetDetectionPhase
|
||||
detectionErr map[string]error
|
||||
detectionGeneration uint64
|
||||
@@ -202,6 +203,7 @@ func NewWithTargetsAndRuntime(store profile.Store, targets []target.Target, muta
|
||||
targets: append([]target.Target(nil), targets...),
|
||||
availabilities: make(map[string]target.Availability),
|
||||
inspections: make(map[string]target.Inspection),
|
||||
mutationBlockReasons: make(map[string]string),
|
||||
detectionPhase: make(map[string]TargetDetectionPhase),
|
||||
detectionErr: make(map[string]error),
|
||||
mutationLock: mutationLock,
|
||||
@@ -229,7 +231,7 @@ func (a *Application) snapshotLocked() Snapshot {
|
||||
availability := a.availabilities[id]
|
||||
inspection := a.inspections[id]
|
||||
phase := a.detectionPhase[id]
|
||||
primary, resolution := contextualTargetActions(phase, availability.Available, inspection.State, state.ActiveProfile != "")
|
||||
primary, resolution := contextualTargetActions(phase, availability.Available, inspection.State, state.ActiveProfile != "", a.mutationBlockReasons[id])
|
||||
snapshot.Targets = append(snapshot.Targets, TargetSnapshot{
|
||||
ID: id, Description: a.targetDescriptions[i], DetectionPhase: phase, DetectionErr: a.detectionErr[id],
|
||||
Available: availability.Available, AvailabilityEvidence: availability.Evidence, State: inspection.State, Evidence: inspection.Evidence,
|
||||
@@ -239,7 +241,7 @@ func (a *Application) snapshotLocked() Snapshot {
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func contextualTargetActions(phase TargetDetectionPhase, available bool, state target.State, hasActiveProfile bool) (TargetAction, []TargetAction) {
|
||||
func contextualTargetActions(phase TargetDetectionPhase, available bool, state target.State, hasActiveProfile bool, mutationBlockReason string) (TargetAction, []TargetAction) {
|
||||
primaryKind := primaryTargetActionKind(state)
|
||||
primary := TargetAction{Kind: primaryKind, Available: primaryKind != 0}
|
||||
if primaryKind == 0 {
|
||||
@@ -253,6 +255,10 @@ func contextualTargetActions(phase TargetDetectionPhase, available bool, state t
|
||||
primary.Available = false
|
||||
primary.DisabledReason = "This action is unavailable because there is no Active Profile; open Profile management first"
|
||||
}
|
||||
if primary.Available && mutationBlockReason != "" {
|
||||
primary.Available = false
|
||||
primary.DisabledReason = mutationBlockReason
|
||||
}
|
||||
if state != target.Drifted {
|
||||
return primary, nil
|
||||
}
|
||||
@@ -271,6 +277,10 @@ func contextualTargetActions(phase TargetDetectionPhase, available bool, state t
|
||||
action.Available = false
|
||||
action.DisabledReason = reason
|
||||
}
|
||||
if action.Available && mutationBlockReason != "" {
|
||||
action.Available = false
|
||||
action.DisabledReason = mutationBlockReason
|
||||
}
|
||||
resolution = append(resolution, action)
|
||||
}
|
||||
return primary, resolution
|
||||
@@ -302,6 +312,7 @@ func targetActionDisabledReason(phase TargetDetectionPhase, available bool) stri
|
||||
type targetDetectionOutcome struct {
|
||||
targetID string
|
||||
detection target.Detection
|
||||
mutationBlockReason string
|
||||
}
|
||||
|
||||
type targetDetectionUpdate struct {
|
||||
@@ -327,7 +338,10 @@ func (a *Application) beginTargetDetection(ctx context.Context) (Snapshot, <-cha
|
||||
for i, adapter := range a.targets {
|
||||
adapter, id := adapter, a.targetIDs[i]
|
||||
go func() {
|
||||
outcomes <- targetDetectionOutcome{targetID: id, detection: detectTarget(ctx, adapter, active)}
|
||||
outcomes <- targetDetectionOutcome{
|
||||
targetID: id, detection: detectTarget(ctx, adapter, active),
|
||||
mutationBlockReason: mutationBlockReason(ctx, adapter),
|
||||
}
|
||||
}()
|
||||
}
|
||||
go func() {
|
||||
@@ -345,6 +359,7 @@ func (a *Application) beginTargetDetection(ctx context.Context) (Snapshot, <-cha
|
||||
}
|
||||
a.availabilities[outcome.targetID] = outcome.detection.Availability
|
||||
a.inspections[outcome.targetID] = outcome.detection.Inspection
|
||||
a.mutationBlockReasons[outcome.targetID] = outcome.mutationBlockReason
|
||||
a.detectionPhase[outcome.targetID] = phase
|
||||
a.detectionErr[outcome.targetID] = outcome.detection.Err
|
||||
update := targetDetectionUpdate{snapshot: a.snapshotLocked(), targetID: outcome.targetID}
|
||||
@@ -372,6 +387,21 @@ func detectTarget(ctx context.Context, adapter target.Target, active *profile.Pr
|
||||
}
|
||||
}
|
||||
|
||||
func mutationBlockReason(ctx context.Context, adapter target.Target) string {
|
||||
authorizer, ok := adapter.(target.MutationAuthorizer)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
check := authorizer.MutationAuthority(ctx)
|
||||
if check.OK {
|
||||
return ""
|
||||
}
|
||||
if check.Evidence == "" {
|
||||
return "The current process does not have authority to change this Target"
|
||||
}
|
||||
return check.Evidence
|
||||
}
|
||||
|
||||
func (a *Application) Dispatch(intent Intent) Result {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), operationTimeout)
|
||||
defer cancel()
|
||||
@@ -642,12 +672,13 @@ func (a *Application) replaceInspection(id string, inspection target.Inspection)
|
||||
// publishTargetDetection atomically replaces all availability, inspection, and
|
||||
// failure fields from one targeted check. Detect runs before this method, so no
|
||||
// Application state mutex is held while adapter code executes.
|
||||
func (a *Application) publishTargetDetection(id string, detection target.Detection) Snapshot {
|
||||
func (a *Application) publishTargetDetection(id string, detection target.Detection, mutationBlockReason string) Snapshot {
|
||||
a.stateMu.Lock()
|
||||
defer a.stateMu.Unlock()
|
||||
a.detectionGeneration++
|
||||
a.availabilities[id] = detection.Availability
|
||||
a.inspections[id] = detection.Inspection
|
||||
a.mutationBlockReasons[id] = mutationBlockReason
|
||||
a.detectionErr[id] = detection.Err
|
||||
a.detectionPhase[id] = TargetReady
|
||||
if detection.Err != nil {
|
||||
@@ -806,7 +837,7 @@ func (a *Application) setTargetDesired(ctx context.Context, intent Intent, publi
|
||||
// before action validation. Adapter calls happen without stateMu; the full
|
||||
// outcome is then published under one lock before policy is derived.
|
||||
detection := detectTarget(ctx, selected, active)
|
||||
snapshot := a.publishTargetDetection(intent.TargetID, detection)
|
||||
snapshot := a.publishTargetDetection(intent.TargetID, detection, mutationBlockReason(ctx, selected))
|
||||
selectedSnapshot := snapshot.Targets[selectedIndex]
|
||||
policy, err := validateTargetAction(intent, selectedSnapshot)
|
||||
if err != nil {
|
||||
|
||||
@@ -103,6 +103,35 @@ func TestApplicationRequiresTargetConfirmationBeforeMutation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type authorityTarget struct {
|
||||
*fakeTarget
|
||||
allowed bool
|
||||
reason string
|
||||
}
|
||||
|
||||
func (t *authorityTarget) MutationAuthority(context.Context) target.Check {
|
||||
return target.Check{Name: "mutation authority", OK: t.allowed, Evidence: t.reason}
|
||||
}
|
||||
|
||||
func TestApplicationDisablesMutationWhenCurrentProcessLacksAuthority(t *testing.T) {
|
||||
adapter := &authorityTarget{fakeTarget: &fakeTarget{available: true, state: target.Disabled}, reason: "requires elevated authority"}
|
||||
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
createAndActivate(t, application)
|
||||
refreshApplicationTargets(t, application)
|
||||
|
||||
snapshot := application.Snapshot()
|
||||
if snapshot.Targets[0].PrimaryAction.Available || snapshot.Targets[0].PrimaryAction.DisabledReason != adapter.reason {
|
||||
t.Fatalf("authority policy = %#v", snapshot.Targets[0].PrimaryAction)
|
||||
}
|
||||
result := application.Dispatch(app.Intent{Kind: app.EnableTarget, TargetID: adapter.ID()})
|
||||
if result.Err == nil || !strings.Contains(result.Err.Error(), adapter.reason) || len(adapter.calls) != 0 {
|
||||
t.Fatalf("blocked mutation result = %#v calls=%v", result, adapter.calls)
|
||||
}
|
||||
}
|
||||
|
||||
type targetWithID struct {
|
||||
*fakeTarget
|
||||
id string
|
||||
|
||||
@@ -130,6 +130,14 @@ func (a *Adapter) Confirmation(bool) string { return "" }
|
||||
|
||||
// DiagnosticChecks observes the APT command, configuration schema, and current
|
||||
// filesystem authority without changing the overlay or effective settings.
|
||||
func (a *Adapter) MutationAuthority(context.Context) target.Check {
|
||||
check := target.ReadOnlyWriteAuthority(a.configuration.OverlayFile)
|
||||
if !check.OK {
|
||||
check.Evidence += "; run proxyctl with sufficient authority (normally sudo proxyctl)"
|
||||
}
|
||||
return check
|
||||
}
|
||||
|
||||
func (a *Adapter) DiagnosticChecks(ctx context.Context, _ *profile.Profile) []target.Check {
|
||||
commandCheck := target.Check{Name: "required command", OK: true, Evidence: "apt-config is available"}
|
||||
command, err := a.runner.LookPath("apt-config")
|
||||
|
||||
@@ -132,6 +132,15 @@ func (a *Adapter) Confirmation(bool) string {
|
||||
|
||||
// DiagnosticChecks performs only systemctl show and filesystem reads/metadata
|
||||
// probes; service reload/restart and all configuration mutations are excluded.
|
||||
func (a *Adapter) MutationAuthority(context.Context) target.Check {
|
||||
if a.authority.EffectiveUID() != 0 {
|
||||
return target.Check{Name: "mutation authority", Evidence: "Docker daemon changes require root authority; restart with sudo proxyctl"}
|
||||
}
|
||||
check := target.ReadOnlyWriteAuthority(a.configuration.DaemonConfigPath, a.configuration.RecoveryPath)
|
||||
check.Name = "mutation authority"
|
||||
return check
|
||||
}
|
||||
|
||||
func (a *Adapter) DiagnosticChecks(ctx context.Context, _ *profile.Profile) []target.Check {
|
||||
commandCheck := target.Check{Name: "required command and service", OK: true, Evidence: "systemctl can inspect the loaded docker.service"}
|
||||
if availability, err := a.detect(ctx); err != nil || !availability.Available {
|
||||
|
||||
@@ -78,6 +78,12 @@ type Diagnosable interface {
|
||||
DiagnosticChecks(context.Context, *profile.Profile) []Check
|
||||
}
|
||||
|
||||
// MutationAuthorizer optionally explains why a detected Target cannot be
|
||||
// changed by the current process. It is read-only and presentation-independent.
|
||||
type MutationAuthorizer interface {
|
||||
MutationAuthority(context.Context) Check
|
||||
}
|
||||
|
||||
// Target hides platform paths and commands from the application and TUI.
|
||||
type Target interface {
|
||||
ID() string
|
||||
|
||||
@@ -180,7 +180,7 @@ func TestCompleteModelPickerConfirmedActivationContinuesThroughUnifiedOperation(
|
||||
completion <- app.SucceededOperationUpdate(completed, app.OperationStatus{Summary: `Activated Profile "new"`}, nil)
|
||||
close(completion)
|
||||
model, _ = applyOperationCommand(t, model, command)
|
||||
if view := model.View(); !strings.Contains(view, "Active Profile: new") || !strings.Contains(view, `Activated Profile "new"`) {
|
||||
if view := model.View(); !strings.Contains(view, "Profile: new") || !strings.Contains(view, `Activated Profile "new"`) {
|
||||
t.Fatalf("confirmed activation did not finish on the dashboard:\n%s", view)
|
||||
}
|
||||
}
|
||||
@@ -213,7 +213,7 @@ func TestCompleteModelPickerPartialFailureKeepsProgressClosesPickerAndFocusesFai
|
||||
close(updates)
|
||||
model, _ = applyOperationCommand(t, model, command)
|
||||
view := model.View()
|
||||
if model.selectedTargetID != "docker" || !strings.Contains(view, "> ! Drifted ! Failed Docker daemon") || !strings.Contains(view, "Current error: Activated Profile new with 1 Target update failure") || !strings.Contains(view, "Active Profile: new") {
|
||||
if model.selectedTargetID != "docker" || !strings.Contains(view, "> ! Drifted ! Failed Docker daemon") || !strings.Contains(view, "Current error: Activated Profile new with 1 Target update failure") || !strings.Contains(view, "Profile: new") {
|
||||
t.Fatalf("partial failure was not retained and focused: selected=%q\n%s", model.selectedTargetID, view)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,12 +35,9 @@ func TestCompleteModelRendersAndDispatchesContextualPrimaryActions(t *testing.T)
|
||||
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, "Action: "+test.label) || !strings.Contains(view, "Space: "+test.label) {
|
||||
if !strings.Contains(view, "Space: "+test.label) {
|
||||
t.Fatalf("contextual action not rendered:\n%s", view)
|
||||
}
|
||||
if test.state == target.External && (!strings.Contains(view, "proxyctl will begin managing this configuration") || !strings.Contains(view, "while preserving restorable state")) {
|
||||
t.Fatalf("Apply Profile ownership explanation missing:\n%s", view)
|
||||
}
|
||||
|
||||
updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||
model = updated.(Model)
|
||||
@@ -91,7 +88,7 @@ func TestCompleteModelRendersUnknownTargetStateAsStructurallyUnavailable(t *test
|
||||
model := resizedModel(t, module, themeModeNoColor, 120, 36)
|
||||
|
||||
view := model.View()
|
||||
if !strings.Contains(view, "Action: Unavailable") || !strings.Contains(view, "Action unavailable:") || !strings.Contains(view, "state is unknown") || !strings.Contains(view, "Space: Unavailable disabled") {
|
||||
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})
|
||||
@@ -108,7 +105,7 @@ func TestCompleteModelUsesStructuredDisabledReasonWithoutInferringFromState(t *t
|
||||
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 disabled") {
|
||||
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})
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"proxyctl/internal/app"
|
||||
"proxyctl/internal/target"
|
||||
)
|
||||
import "proxyctl/internal/app"
|
||||
|
||||
func targetActionLabel(kind app.TargetActionKind) string {
|
||||
switch kind {
|
||||
@@ -101,40 +98,3 @@ func (c targetCatalog) retain(id string, formerIndex int) string {
|
||||
}
|
||||
return c.items[max(0, min(len(c.items)-1, formerIndex))].ID
|
||||
}
|
||||
|
||||
type targetStateCounts struct {
|
||||
enabled int
|
||||
disabled int
|
||||
external int
|
||||
drifted int
|
||||
checking int
|
||||
failed int
|
||||
}
|
||||
|
||||
func countAvailableTargetStates(snapshot app.Snapshot) targetStateCounts {
|
||||
var counts targetStateCounts
|
||||
for _, item := range snapshot.Targets {
|
||||
switch item.DetectionPhase {
|
||||
case app.TargetChecking:
|
||||
counts.checking++
|
||||
continue
|
||||
case app.TargetDetectionFailed:
|
||||
counts.failed++
|
||||
continue
|
||||
}
|
||||
if !item.Available {
|
||||
continue
|
||||
}
|
||||
switch item.State {
|
||||
case target.Enabled:
|
||||
counts.enabled++
|
||||
case target.Disabled:
|
||||
counts.disabled++
|
||||
case target.External:
|
||||
counts.external++
|
||||
case target.Drifted:
|
||||
counts.drifted++
|
||||
}
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
@@ -23,9 +23,8 @@ func TestCompleteModelWideOperationsConsoleShowsStableAvailableTargetDashboard(t
|
||||
|
||||
view := resizedModel(t, module, themeModeNoColor, 120, 36).View()
|
||||
assertUsableFrame(t, view, 120, 36,
|
||||
"proxyctl", "Active Profile: office",
|
||||
"● Enabled 1", "○ Disabled 1", "◇ External 1", "! Drifted 1",
|
||||
"> ● Enabled", "Shell", "State: ● Enabled", "Evidence: matches Active Profile",
|
||||
"proxyctl", "Profile: office",
|
||||
"> ● Enabled", "Shell", "matches Active Profile",
|
||||
)
|
||||
if strings.Contains(view, "APT") || strings.Contains(view, "apt-config was not detected") {
|
||||
t.Fatalf("default dashboard included an unavailable Target:\n%s", view)
|
||||
@@ -71,12 +70,8 @@ func TestCompleteModelDashboardRevealsUnavailableEvidenceAndStructuredDetails(t
|
||||
|
||||
view := model.View()
|
||||
assertUsableFrame(t, view, 120, 36,
|
||||
"> ○ Unavailable APT", "State: ○ Unavailable", "Evidence: apt-config was not detected",
|
||||
"Affects: package operations across privilege boundaries",
|
||||
"Permissions: pre-existing root write authority",
|
||||
"Ownership & restoration: isolated overlay; disabling reveals the", "previous configuration",
|
||||
"Restart risk: none", "Persistent package proxy.", "Longer APT explanation.",
|
||||
"u: hide unavailable",
|
||||
"> ○ Unavailable APT", "○ Unavailable", "apt-config was not detected",
|
||||
"Persistent package proxy.", "u: Hide unavailable",
|
||||
)
|
||||
if !(strings.Index(view, "Shell") < strings.Index(view, "APT") && strings.Index(view, "APT") < strings.Index(view, "Docker daemon")) {
|
||||
t.Fatalf("revealing unavailable Targets changed product order:\n%s", view)
|
||||
@@ -95,7 +90,7 @@ func TestCompleteModelNarrowDashboardFocusAndIndependentScrollingRetainTargetIde
|
||||
}
|
||||
}
|
||||
targets[22].Description.Summary = "Selected identity summary"
|
||||
targets[22].Description.Details = strings.Repeat("Scrollable detail line. ", 45) + "END OF TARGET DETAILS"
|
||||
targets[22].Evidence = strings.Repeat("Scrollable evidence line. ", 45) + "END OF TARGET EVIDENCE"
|
||||
module := &recordingModule{snapshot: app.Snapshot{ActiveProfile: "office", Targets: targets}}
|
||||
model := resizedModel(t, module, themeModeNoColor, 80, 24)
|
||||
for range 22 {
|
||||
@@ -103,7 +98,7 @@ func TestCompleteModelNarrowDashboardFocusAndIndependentScrollingRetainTargetIde
|
||||
}
|
||||
|
||||
view := model.View()
|
||||
assertUsableFrame(t, view, 80, 24, "> ○ Disabled Product Target 22", "Selected identity summary", "▶ Targets [focused]")
|
||||
assertUsableFrame(t, view, 80, 24, "> ○ Disabled Product Target 22", "▶ Targets [focused]")
|
||||
|
||||
model = updateKey(model, "tab")
|
||||
for range 70 {
|
||||
@@ -111,16 +106,16 @@ func TestCompleteModelNarrowDashboardFocusAndIndependentScrollingRetainTargetIde
|
||||
}
|
||||
view = model.View()
|
||||
assertUsableFrame(t, view, 80, 24,
|
||||
"> ○ Disabled Product Target 22", "▶ Target details [focused]", "END OF", "TARGET DETAILS",
|
||||
"↑/k ↓/j PgUp/PgDn: scroll", "Tab: focus Targets",
|
||||
"> ○ Disabled Product Target 22", "▶ Target details [focused]", "END OF", "TARGET EVIDENCE",
|
||||
"↑/↓: Scroll", "Tab: Targets",
|
||||
)
|
||||
|
||||
model = resize(model, 120, 36)
|
||||
view = model.View()
|
||||
assertUsableFrame(t, view, 120, 36, "> ○ Disabled Product Target 22", "END OF TARGET DETAILS", "▶ Target details [focused]")
|
||||
assertUsableFrame(t, view, 120, 36, "> ○ Disabled Product Target 22", "END OF TARGET", "EVIDENCE", "▶ Target details [focused]")
|
||||
if !sameLine(view, "Targets", "Target details") {
|
||||
t.Fatalf("resized dashboard did not become side by side:\n%s", view)
|
||||
}
|
||||
model = resize(model, 80, 24)
|
||||
assertUsableFrame(t, model.View(), 80, 24, "> ○ Disabled Product Target 22", "END OF", "TARGET DETAILS", "▶ Target details [focused]")
|
||||
assertUsableFrame(t, model.View(), 80, 24, "> ○ Disabled Product Target 22", "END OF", "TARGET EVIDENCE", "▶ Target details [focused]")
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"proxyctl/internal/app"
|
||||
"proxyctl/internal/target"
|
||||
)
|
||||
|
||||
type dashboardRenderState struct {
|
||||
@@ -149,55 +148,18 @@ func (p terminalPresentation) targetDetails(item app.TargetSnapshot, progress ap
|
||||
|
||||
var details strings.Builder
|
||||
details.WriteString(p.theme.section.Render(item.Description.Name))
|
||||
fmt.Fprintf(&details, "\nState: %s\nEvidence: %s", state, evidence)
|
||||
fmt.Fprintf(&details, "\nAction: %s", targetActionLabel(item.PrimaryAction.Kind))
|
||||
if !item.PrimaryAction.Available && item.PrimaryAction.DisabledReason != "" {
|
||||
fmt.Fprintf(&details, "\nAction unavailable: %s", item.PrimaryAction.DisabledReason)
|
||||
}
|
||||
switch item.PrimaryAction.Kind {
|
||||
case app.ApplyProfileTargetAction:
|
||||
details.WriteString("\nApply Profile means proxyctl will begin managing this configuration while preserving restorable state.")
|
||||
case app.ResolveDriftTargetAction:
|
||||
details.WriteString("\nResolve opens Drift Resolution without choosing a recovery direction for you.")
|
||||
fmt.Fprintf(&details, "\n%s\n%s", state, evidence)
|
||||
action := targetActionLabel(item.PrimaryAction.Kind)
|
||||
if item.PrimaryAction.Available {
|
||||
fmt.Fprintf(&details, "\n\nSpace: %s", action)
|
||||
} else if item.PrimaryAction.DisabledReason != "" {
|
||||
fmt.Fprintf(&details, "\n\nUnavailable: %s", item.PrimaryAction.DisabledReason)
|
||||
}
|
||||
if item.DetectionPhase == app.TargetDetectionFailed && item.DetectionErr != nil {
|
||||
fmt.Fprintf(&details, "\nCheck error: %s", item.DetectionErr)
|
||||
}
|
||||
facts := []struct {
|
||||
label string
|
||||
value string
|
||||
}{
|
||||
{"Affects", item.Description.Scope},
|
||||
{"Permissions", item.Description.Permissions},
|
||||
{"Ownership & restoration", item.Description.Ownership},
|
||||
{"Restart risk", item.Description.RestartRisk},
|
||||
}
|
||||
for _, fact := range facts {
|
||||
if fact.value != "" {
|
||||
fmt.Fprintf(&details, "\n%s: %s", fact.label, fact.value)
|
||||
}
|
||||
fmt.Fprintf(&details, "\nCheck failed: %s", item.DetectionErr)
|
||||
}
|
||||
if item.Description.Summary != "" {
|
||||
fmt.Fprintf(&details, "\n\n%s", item.Description.Summary)
|
||||
}
|
||||
if item.Description.Details != "" {
|
||||
fmt.Fprintf(&details, "\n\n%s", item.Description.Details)
|
||||
}
|
||||
return protectRenderedText(details.String())
|
||||
}
|
||||
|
||||
func (p terminalPresentation) stateCountSummary(snapshot app.Snapshot) string {
|
||||
counts := countAvailableTargetStates(snapshot)
|
||||
summary := fmt.Sprintf("%s %d %s %d %s %d %s %d",
|
||||
p.theme.renderTargetState(target.Enabled, false), counts.enabled,
|
||||
p.theme.renderTargetState(target.Disabled, false), counts.disabled,
|
||||
p.theme.renderTargetState(target.External, false), counts.external,
|
||||
p.theme.renderTargetState(target.Drifted, false), counts.drifted)
|
||||
if counts.checking > 0 {
|
||||
summary += fmt.Sprintf(" %s %d", p.theme.checking.Render("… Checking"), counts.checking)
|
||||
}
|
||||
if counts.failed > 0 {
|
||||
summary += fmt.Sprintf(" %s %d", p.theme.checkFailed.Render("! Check failed"), counts.failed)
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestCompleteModelRendersResponsiveTerminalSizeClasses(t *testing.T) {
|
||||
if !sameLine(view, "Targets", "Target details") {
|
||||
t.Fatalf("wide layout did not place list and details side by side:\n%s", view)
|
||||
}
|
||||
assertUsableFrame(t, view, 120, 36, "Shell", "Shell summary", "Active Profile: office")
|
||||
assertUsableFrame(t, view, 120, 36, "Shell", "Shell summary", "Profile: office")
|
||||
})
|
||||
|
||||
t.Run("narrow 80x24", func(t *testing.T) {
|
||||
@@ -33,7 +33,7 @@ func TestCompleteModelRendersResponsiveTerminalSizeClasses(t *testing.T) {
|
||||
if sameLine(view, "Targets", "Target details") || strings.Index(view, "Target details") < strings.Index(view, "Shell") {
|
||||
t.Fatalf("narrow layout did not stack list above details:\n%s", view)
|
||||
}
|
||||
assertUsableFrame(t, view, 80, 24, "Shell", "Shell summary", "Active Profile: office")
|
||||
assertUsableFrame(t, view, 80, 24, "Shell", "Shell summary", "Profile: office")
|
||||
})
|
||||
|
||||
t.Run("below minimum 59x15", func(t *testing.T) {
|
||||
@@ -82,11 +82,11 @@ func TestCompleteModelResizePreservesValidSelectionAndFormFocus(t *testing.T) {
|
||||
model := resizedModel(t, module, themeModeNoColor, 120, 36)
|
||||
model = updateKey(model, "down")
|
||||
model = resize(model, 80, 24)
|
||||
if view := model.View(); !strings.Contains(view, "> ◇ External APT") || !strings.Contains(view, "Action: Apply Profile") {
|
||||
if view := model.View(); !strings.Contains(view, "> ◇ External APT") || !strings.Contains(view, "Space: Apply Profile") {
|
||||
t.Fatalf("Target selection did not survive wide-to-narrow resize:\n%s", view)
|
||||
}
|
||||
model = resize(model, 120, 36)
|
||||
if view := model.View(); !strings.Contains(view, "> ◇ External APT") || !strings.Contains(view, "Action: Apply Profile") {
|
||||
if view := model.View(); !strings.Contains(view, "> ◇ External APT") || !strings.Contains(view, "Space: Apply Profile") {
|
||||
t.Fatalf("Target selection did not survive narrow-to-wide resize:\n%s", view)
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ func TestCompleteModelSemanticThemeSupportsDarkLightAndNoColor(t *testing.T) {
|
||||
plain := resizedModel(t, module, themeModeNoColor, 80, 24).View()
|
||||
|
||||
for name, view := range map[string]string{"dark": dark, "light": light, "NO_COLOR": plain} {
|
||||
for _, semantic := range []string{"Active Profile: office", "Disabled", "External", ">"} {
|
||||
for _, semantic := range []string{"Profile: office", "Disabled", "External", ">"} {
|
||||
if !strings.Contains(view, semantic) {
|
||||
t.Fatalf("%s theme lost non-color semantic %q:\n%s", name, semantic, view)
|
||||
}
|
||||
@@ -134,7 +134,7 @@ func TestNewModelUsesInjectedBubbleTeaOutputForTerminalCapabilities(t *testing.T
|
||||
if ansiPattern.MatchString(view) {
|
||||
t.Fatalf("non-terminal Bubble Tea output received ANSI styling: %q", view)
|
||||
}
|
||||
for _, semantic := range []string{"Active Profile: office", "Disabled", "External", ">"} {
|
||||
for _, semantic := range []string{"Profile: office", "Disabled", "External", ">"} {
|
||||
if !strings.Contains(view, semantic) {
|
||||
t.Fatalf("injected-output rendering lost semantic %q:\n%s", semantic, view)
|
||||
}
|
||||
@@ -231,19 +231,19 @@ func TestCompleteModelNarrowShellKeepsLastProductionTargetStatusAndActionsVisibl
|
||||
|
||||
assertUsableFrame(t, model.View(), 80, 24,
|
||||
"> ! Drifted Docker daemon",
|
||||
"State: ! Drifted",
|
||||
"! Drifted",
|
||||
"Docker daemon selected evidence.",
|
||||
"Status: Refresh completed; selected Target remains actionable.",
|
||||
"Space: Resolve",
|
||||
"p: switch Active Profile",
|
||||
"m: manage Proxy Profiles",
|
||||
"p: Profile",
|
||||
"m: Profiles",
|
||||
)
|
||||
|
||||
model = resize(model, 120, 36)
|
||||
model = resize(model, 80, 24)
|
||||
assertUsableFrame(t, model.View(), 80, 24,
|
||||
"> ! Drifted Docker daemon",
|
||||
"State: ! Drifted",
|
||||
"! Drifted",
|
||||
"Docker daemon selected evidence.",
|
||||
"Space: Resolve",
|
||||
)
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestCompleteModelContextualHelpOpensOverEveryPageAndRestoresPageState(t *te
|
||||
model = updateKey(model, "down")
|
||||
model = updateKey(model, "?")
|
||||
view := model.View()
|
||||
assertUsableFrame(t, view, 120, 36, "Operations Console", "Contextual Help", "Dashboard", "Target details focus", "APT", "Disable")
|
||||
assertUsableFrame(t, view, 120, 36, "Profile: office", "Contextual Help", "Dashboard", "Target details focus", "APT", "Disable")
|
||||
|
||||
model = updateKey(model, " ")
|
||||
if len(module.intents) != 0 {
|
||||
@@ -259,9 +259,9 @@ func TestCompleteModelFooterHintsMatchContextAndAcceptedKeys(t *testing.T) {
|
||||
|
||||
t.Run("dashboard and busy details", func(t *testing.T) {
|
||||
model := resizedModel(t, module, themeModeNoColor, 120, 36)
|
||||
assertUsableFrame(t, model.View(), 120, 36, "Space: Enable", "u: show unavailable", "r: refresh", "q/Ctrl+C: quit")
|
||||
assertUsableFrame(t, model.View(), 120, 36, "Space: Enable", "u: Show hidden", "r: Refresh", "q: Quit")
|
||||
model = updateKey(model, "tab")
|
||||
assertUsableFrame(t, model.View(), 120, 36, "PgUp/PgDn: scroll", "u: show unavailable", "q/Ctrl+C: quit")
|
||||
assertUsableFrame(t, model.View(), 120, 36, "↑/↓: Scroll", "u: Show hidden", "q: Quit")
|
||||
|
||||
updates := make(chan app.OperationUpdate)
|
||||
busyModule := &interactiveScriptModule{snapshot: operationSnapshot(), streams: []chan app.OperationUpdate{updates}}
|
||||
@@ -269,9 +269,12 @@ func TestCompleteModelFooterHintsMatchContextAndAcceptedKeys(t *testing.T) {
|
||||
model = updateKey(model, "tab")
|
||||
updated, _ := model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||
model = updated.(Model)
|
||||
assertUsableFrame(t, model.View(), 120, 36, "u: show unavailable", "q/Ctrl+C: defer safe exit", "mutations disabled")
|
||||
assertUsableFrame(t, model.View(), 120, 36, "q: Stop")
|
||||
model = updateKey(model, "u")
|
||||
assertUsableFrame(t, model.View(), 120, 36, "u: hide unavailable")
|
||||
if !model.showUnavailable {
|
||||
t.Fatal("u did not toggle unavailable Targets while busy")
|
||||
}
|
||||
assertUsableFrame(t, model.View(), 120, 36, "q: Stop")
|
||||
close(updates)
|
||||
})
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ func TestMainAndProfilePagesMapKeysAndObscurePasswords(t *testing.T) {
|
||||
}},
|
||||
}}
|
||||
model := NewModel(module)
|
||||
if view := model.View(); !strings.Contains(view, "Active Profile: office") {
|
||||
if view := model.View(); !strings.Contains(view, "Profile: office") {
|
||||
t.Fatalf("main view missing Active Profile:\n%s", view)
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ func TestRenderedMalformedProxyURLStillObscuresPassword(t *testing.T) {
|
||||
func TestNoProfileStateStaysOnAccessibleMainPage(t *testing.T) {
|
||||
model := NewModel(&recordingModule{})
|
||||
view := model.View()
|
||||
if !strings.Contains(view, "No Active Profile") || !strings.Contains(view, "m: manage Proxy Profiles") {
|
||||
if !strings.Contains(view, "No Active Profile") || !strings.Contains(view, "m: Profiles") {
|
||||
t.Fatalf("no-Profile main view is not actionable:\n%s", view)
|
||||
}
|
||||
}
|
||||
|
||||
+15
-52
@@ -14,12 +14,7 @@ func (p terminalPresentation) mainView(model presentationState, width, height in
|
||||
if activeProfile == "" {
|
||||
activeProfile = "No Active Profile"
|
||||
}
|
||||
header := fmt.Sprintf("%s Operations Console\nActive Profile: %s Targets: %s",
|
||||
p.theme.title.Render("proxyctl"), activeProfile, p.stateCountSummary(model.snapshot))
|
||||
if width < wideWidth {
|
||||
header = fmt.Sprintf("%s Operations Console\nActive Profile: %s\nTargets: %s",
|
||||
p.theme.title.Render("proxyctl"), activeProfile, p.stateCountSummary(model.snapshot))
|
||||
}
|
||||
header := fmt.Sprintf("%s Profile: %s", p.theme.title.Render("proxyctl"), activeProfile)
|
||||
|
||||
catalog := newTargetCatalog(model.snapshot, dashboard.showUnavailable, dashboard.selectedTargetID)
|
||||
footer := p.dashboardFooter(model, catalog)
|
||||
@@ -97,62 +92,30 @@ func (p terminalPresentation) driftResolutionView(selected app.TargetSnapshot) (
|
||||
}
|
||||
|
||||
func (p terminalPresentation) dashboardFooter(model presentationState, catalog targetCatalog) string {
|
||||
dashboard, operation, width := model.dashboard, model.operation, model.terminal.width
|
||||
visibility := "u: show unavailable"
|
||||
dashboard, operation := model.dashboard, model.operation
|
||||
visibility := "u: Show hidden"
|
||||
if dashboard.showUnavailable {
|
||||
visibility = "u: hide unavailable"
|
||||
visibility = "u: Hide unavailable"
|
||||
}
|
||||
if operation.exitDeferred {
|
||||
return "Operation stopping; changes disabled ?: Help"
|
||||
}
|
||||
if dashboard.focus == targetDetailsPane {
|
||||
if operation.exitDeferred {
|
||||
return "↑/k ↓/j PgUp/PgDn: browse Tab: focus Targets " + visibility + " ?: help\nExit pending; new work disabled until safe completion"
|
||||
}
|
||||
if width < wideWidth {
|
||||
if operation.active {
|
||||
return "↑/k ↓/j PgUp/PgDn: browse Tab: focus Targets\n?: help q/Ctrl+C: defer exit; mutations disabled"
|
||||
return "↑/↓: Scroll Tab: Targets ?: Help q: Stop"
|
||||
}
|
||||
if width < 70 {
|
||||
return "↑/k ↓/j PgUp/PgDn: scroll Tab: focus Targets\np: switch Profile m: manage ?: help q: quit"
|
||||
}
|
||||
return "↑/k ↓/j PgUp/PgDn: scroll Tab: focus Targets\np: switch Active Profile m: manage Proxy Profiles ?: help q: quit"
|
||||
}
|
||||
footer := "↑/k ↓/j PgUp/PgDn: scroll Tab: focus Targets " + visibility + "\np: switch Active Profile m: manage Proxy Profiles ?: help q/Ctrl+C: quit"
|
||||
if operation.active {
|
||||
footer = "↑/k ↓/j PgUp/PgDn: scroll Tab: focus Targets " + visibility + " ?: help\nq/Ctrl+C: defer safe exit; mutations disabled"
|
||||
}
|
||||
return footer
|
||||
}
|
||||
actions := "↑/k ↓/j: select Tab: focus details " + visibility
|
||||
if operation.exitDeferred {
|
||||
return actions + " ?: help\nExit pending; new work disabled until safe completion"
|
||||
return "↑/↓: Scroll Tab: Targets " + visibility + "\np: Profile m: Profiles r: Refresh ?: Help q: Quit"
|
||||
}
|
||||
selected, ok := catalog.selected(dashboard.selectedTargetID)
|
||||
availability := targetMutationAvailability(selected, ok, operation.active || operation.exitDeferred)
|
||||
label := targetActionLabel(selected.PrimaryAction.Kind)
|
||||
if availability.available {
|
||||
actions = "Space: " + label + " " + actions
|
||||
} else {
|
||||
actions = "Space: " + label + " disabled (" + availability.compactReason + ") " + actions
|
||||
availability := targetMutationAvailability(selected, ok, operation.active)
|
||||
action := "Space: " + targetActionLabel(selected.PrimaryAction.Kind)
|
||||
if !availability.available {
|
||||
action += " unavailable"
|
||||
}
|
||||
if width < wideWidth {
|
||||
if availability.available {
|
||||
actions = "Space: " + label + " ↑/k ↓/j: select Tab: focus details"
|
||||
} else {
|
||||
actions = "Space: " + label + " disabled ↑/k ↓/j: select Tab: focus details"
|
||||
}
|
||||
|
||||
if operation.active {
|
||||
return actions + "\n?: help q/Ctrl+C: defer exit; mutations disabled"
|
||||
return action + " ↑/↓: Select Tab: Details\n?: Help q: Stop"
|
||||
}
|
||||
if width < 70 {
|
||||
return actions + "\np: switch Profile m: manage ?: help q: quit"
|
||||
}
|
||||
return actions + "\np: switch Active Profile m: manage Proxy Profiles ?: help q: quit"
|
||||
}
|
||||
footer := actions + "\np: switch Active Profile m: manage Proxy Profiles r: refresh ?: help q/Ctrl+C: quit"
|
||||
if operation.active {
|
||||
footer = actions + "\n?: help q: defer safe exit; new mutations disabled"
|
||||
}
|
||||
return footer
|
||||
return action + " ↑/↓: Select Tab: Details " + visibility + "\np: Profile m: Profiles r: Refresh ?: Help q: Quit"
|
||||
}
|
||||
|
||||
func (p terminalPresentation) profilesView(model presentationState) terminalPage {
|
||||
|
||||
@@ -219,7 +219,7 @@ func TestCompleteModelProfileManagementNavigationAndBusyReasonsAreConsistent(t *
|
||||
model = updateKey(model, "m")
|
||||
updated, command := model.Update(back)
|
||||
model = updated.(Model)
|
||||
if command != nil || !strings.Contains(model.View(), "Operations Console") {
|
||||
if command != nil || !strings.Contains(model.View(), "Profile:") {
|
||||
t.Fatalf("%q did not return to the dashboard:\n%s", back.String(), model.View())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ func TestCompleteModelReconcilesAuthoritativeSnapshotWhenDetectionGenerationClos
|
||||
if next != nil || model.operation.active {
|
||||
t.Fatalf("stale closure left detection active: next=%v active=%v", next != nil, model.operation.active)
|
||||
}
|
||||
assertUsableFrame(t, model.View(), 80, 24, "> ● Enabled Shell", "Evidence: shell enabled")
|
||||
assertUsableFrame(t, model.View(), 80, 24, "> ● Enabled Shell", "shell enabled")
|
||||
}
|
||||
|
||||
func (m *progressiveDetectionModule) detectionUpdate(id string, phase app.TargetDetectionPhase, available bool, state target.State, evidence string, err error) app.OperationUpdate {
|
||||
|
||||
@@ -33,7 +33,7 @@ func TestCompleteModelFixedSizeCoverageAcrossCompletedWorkflows(t *testing.T) {
|
||||
}
|
||||
return resizedModel(t, &recordingModule{snapshot: snapshot}, themeModeNoColor, width, height)
|
||||
},
|
||||
want: []string{"Operations Console", "Checking", "Target availability"},
|
||||
want: []string{"Profile:", "Checking", "Target availability"},
|
||||
},
|
||||
{
|
||||
name: "refresh",
|
||||
@@ -46,7 +46,7 @@ func TestCompleteModelFixedSizeCoverageAcrossCompletedWorkflows(t *testing.T) {
|
||||
}
|
||||
return model
|
||||
},
|
||||
want: []string{"Operations Console", "Checking Target availability and state"},
|
||||
want: []string{"Profile:", "Checking Target availability and state"},
|
||||
},
|
||||
{
|
||||
name: "Target action",
|
||||
@@ -155,7 +155,7 @@ func TestCompleteModelLightDarkAndNoColorRetainEquivalentSemantics(t *testing.T)
|
||||
model, _ = applyOperationCommand(t, model, command)
|
||||
model = updateKey(model, "e")
|
||||
views[mode] = progressFrame + "--- operation completed ---\n" + model.View()
|
||||
for _, semantic := range []string{"[focused]", "Disabled", "Drifted", "Applying", "mutations disabled", "Current error:", "REDACTED", "Action: Enable"} {
|
||||
for _, semantic := range []string{"[focused]", "Disabled", "Drifted", "Applying", "q: Stop", "Current error:", "REDACTED", "Space: Enable"} {
|
||||
if !strings.Contains(views[mode], semantic) {
|
||||
t.Fatalf("theme %d lost %q:\n%s", mode, semantic, views[mode])
|
||||
}
|
||||
@@ -163,7 +163,7 @@ func TestCompleteModelLightDarkAndNoColorRetainEquivalentSemantics(t *testing.T)
|
||||
}
|
||||
|
||||
plain := views[themeModeNoColor]
|
||||
for _, symbol := range []string{"● Enabled", "○ Disabled", "! Drifted", "▶ Target details [focused]"} {
|
||||
for _, symbol := range []string{"○ Disabled", "! Drifted", "▶ Target details [focused]"} {
|
||||
if !strings.Contains(plain, symbol) {
|
||||
t.Fatalf("standard Unicode/ASCII status strategy lost %q:\n%s", symbol, plain)
|
||||
}
|
||||
@@ -188,7 +188,7 @@ func TestCompleteModelBoundsUnicodeAndKeepsLongContentReachable(t *testing.T) {
|
||||
snapshot.Profiles[0].Name = longProfile
|
||||
snapshot.Targets[0].Description.Name = longName
|
||||
snapshot.Targets[0].Evidence = strings.Repeat("証拠 and wrapped evidence ", 80) + " EVIDENCE-END"
|
||||
snapshot.Targets[0].Description.Details = strings.Repeat("詳細 line ", 100) + " DETAILS-END"
|
||||
snapshot.Targets[0].Evidence = strings.Repeat("証拠 and wrapped evidence ", 120) + " EVIDENCE-END"
|
||||
model := resizedModel(t, &recordingModule{snapshot: snapshot}, themeModeNoColor, 80, 24)
|
||||
|
||||
assertCellBoundedFrame(t, model.View(), 80, 24, "Disabled")
|
||||
@@ -196,7 +196,7 @@ func TestCompleteModelBoundsUnicodeAndKeepsLongContentReachable(t *testing.T) {
|
||||
for range 120 {
|
||||
model = updateKey(model, "pgdown")
|
||||
}
|
||||
assertCellBoundedFrame(t, model.View(), 80, 24, "DETAILS-END")
|
||||
assertCellBoundedFrame(t, model.View(), 80, 24, "EVIDENCE-END")
|
||||
|
||||
model = updateKey(model, "m")
|
||||
for range 60 {
|
||||
@@ -318,7 +318,7 @@ func TestCompleteModelLongFieldsAndDialogsRemainReachableAcrossResizeAtNontermin
|
||||
|
||||
func TestCompleteModelResizeStressPreservesDialogFocusSelectionViewportInFlightAndDeferredExit(t *testing.T) {
|
||||
snapshot := hardeningSnapshot()
|
||||
snapshot.Targets[1].Description.Details = strings.Repeat("APT scrollable detail. ", 90) + " APT-VIEWPORT-END"
|
||||
snapshot.Targets[1].Evidence = strings.Repeat("APT scrollable evidence. ", 90) + " APT-VIEWPORT-END"
|
||||
updates := make(chan app.OperationUpdate, 2)
|
||||
module := &interactiveScriptModule{snapshot: snapshot, streams: []chan app.OperationUpdate{updates}}
|
||||
model := resizedModel(t, module, themeModeNoColor, 120, 36)
|
||||
@@ -354,8 +354,8 @@ func TestCompleteModelResizeStressPreservesDialogFocusSelectionViewportInFlightA
|
||||
|
||||
model = updateKey(model, "q")
|
||||
assertCellBoundedFrame(t, model.View(), 80, 24,
|
||||
"> ! Drifted APT", "▶ Target details [focused]", "APT-VIEWPORT-", "END",
|
||||
"Applying", "Exit requested", "new work disabled")
|
||||
"> ! Drifted APT", "▶ Target details [focused]", "VIEWPORT-END",
|
||||
"Applying", "Exit requested", "changes disabled")
|
||||
|
||||
updates <- app.SucceededOperationUpdate(snapshot,
|
||||
app.OperationStatus{Summary: "Shell Target is now Enabled", TargetID: "shell"},
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestTargetPanelShowsOnlyAvailableTargetsAndAppliesAsynchronously(t *testing
|
||||
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") {
|
||||
if !strings.Contains(view, "Shell") || strings.Contains(view, "APT") || !strings.Contains(view, "Space: Apply Profile") {
|
||||
t.Fatalf("Target panel:\n%s", view)
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ func TestHelpListsEveryTargetAndMainListsOnlyAvailableTargets(t *testing.T) {
|
||||
}}}
|
||||
model := NewModel(module)
|
||||
view := model.View()
|
||||
if strings.Contains(view, "APT") || !strings.Contains(view, "Action: Enable") || !strings.Contains(view, "integration installed") {
|
||||
if strings.Contains(view, "APT") || !strings.Contains(view, "Unavailable:") || !strings.Contains(view, "integration installed") {
|
||||
t.Fatalf("main Target panel:\n%s", view)
|
||||
}
|
||||
model = updateKey(model, "?")
|
||||
@@ -123,7 +123,7 @@ func TestHelpListsEveryTargetAndMainListsOnlyAvailableTargets(t *testing.T) {
|
||||
t.Fatalf("scrollable Target reference:\n%s", view)
|
||||
}
|
||||
model = updateKey(model, "esc")
|
||||
if !strings.Contains(model.View(), "Operations Console") {
|
||||
if !strings.Contains(model.View(), "Profile:") {
|
||||
t.Fatalf("Escape did not return from help:\n%s", model.View())
|
||||
}
|
||||
}
|
||||
@@ -279,12 +279,12 @@ func TestQAndEscapeReturnFromNestedPages(t *testing.T) {
|
||||
for _, key := range []tea.KeyMsg{{Type: tea.KeyRunes, Runes: []rune{'q'}}, {Type: tea.KeyEsc}} {
|
||||
model := updateKey(NewModel(module), "?")
|
||||
updated, command := model.Update(key)
|
||||
if command != nil || !strings.Contains(updated.(Model).View(), "Operations Console") {
|
||||
t.Fatalf("help key %q did not return to Operations Console", key.String())
|
||||
if command != nil || !strings.Contains(updated.(Model).View(), "Profile:") {
|
||||
t.Fatalf("help key %q did not return to dashboard", key.String())
|
||||
}
|
||||
model = updateKey(NewModel(module), "m")
|
||||
updated, command = model.Update(key)
|
||||
if command != nil || !strings.Contains(updated.(Model).View(), "Operations Console") {
|
||||
if command != nil || !strings.Contains(updated.(Model).View(), "Profile:") {
|
||||
t.Fatalf("Profile key %q did not return to Target panel", key.String())
|
||||
}
|
||||
}
|
||||
@@ -315,7 +315,7 @@ func TestActiveProfileTransitionRendersPerTargetProgress(t *testing.T) {
|
||||
t.Fatal("Profile transition did not start progressive operation")
|
||||
}
|
||||
model, command = applyOperationCommand(t, model, command)
|
||||
if command == nil || !strings.Contains(model.View(), "Applying") || !strings.Contains(model.View(), "Active Profile: new") {
|
||||
if command == nil || !strings.Contains(model.View(), "Applying") || !strings.Contains(model.View(), "Profile: new") {
|
||||
t.Fatalf("applying progress:\n%s", model.View())
|
||||
}
|
||||
model, command = applyOperationCommand(t, model, command)
|
||||
|
||||
Reference in New Issue
Block a user