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

577 lines
24 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package tui
import (
"context"
"errors"
"fmt"
"strings"
"testing"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"proxyctl/internal/app"
"proxyctl/internal/probe"
"proxyctl/internal/profile"
"proxyctl/internal/target"
)
// These tests deliberately drive the complete production Model. They verify
// user-visible rendering and accepted input, not page/component internals.
func TestCompleteModelFixedSizeCoverageAcrossCompletedWorkflows(t *testing.T) {
tests := []struct {
name string
setup func(t *testing.T, width, height int) Model
want []string
}{
{
name: "startup",
setup: func(t *testing.T, width, height int) Model {
snapshot := hardeningSnapshot()
for i := range snapshot.Targets {
snapshot.Targets[i].DetectionPhase = app.TargetChecking
}
return resizedModel(t, &recordingModule{snapshot: snapshot}, themeModeNoColor, width, height)
},
want: []string{"Profile:", "Checking", "Target availability"},
},
{
name: "refresh",
setup: func(t *testing.T, width, height int) Model {
model := resizedModel(t, &recordingModule{snapshot: hardeningSnapshot()}, themeModeNoColor, width, height)
updated, command := model.Update(keyMessage("r"))
model = updated.(Model)
if command == nil {
t.Fatal("refresh did not start")
}
return model
},
want: []string{"Profile:", "Checking Target availability and state"},
},
{
name: "Target action",
setup: func(t *testing.T, width, height int) Model {
model := resizedModel(t, &recordingModule{snapshot: hardeningSnapshot()}, themeModeNoColor, width, height)
updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace})
model = updated.(Model)
if command == nil {
t.Fatal("Target action did not start")
}
return model
},
want: []string{"Applying", "Starting Target mutation"},
},
{
name: "Active Profile switching",
setup: func(t *testing.T, width, height int) Model {
model := resizedModel(t, &recordingModule{snapshot: hardeningSnapshot()}, themeModeNoColor, width, height)
return updateKey(model, "p")
},
want: []string{"Switch Active Profile", "Search:", "remote"},
},
{
name: "management",
setup: func(t *testing.T, width, height int) Model {
model := resizedModel(t, &recordingModule{snapshot: hardeningSnapshot()}, themeModeNoColor, width, height)
return updateKey(model, "m")
},
want: []string{"Proxy Profile management", "Selected Profile", "n: create"},
},
{
name: "editor",
setup: func(t *testing.T, width, height int) Model {
model := resizedModel(t, &recordingModule{snapshot: hardeningSnapshot()}, themeModeNoColor, width, height)
model = updateKey(model, "m")
return updateKey(model, "e")
},
want: []string{"Edit Proxy Profile", "[focused]", "Ctrl+S: save"},
},
{
name: "Protocol Probe",
setup: func(t *testing.T, width, height int) Model {
model := resizedModel(t, &recordingModule{snapshot: hardeningSnapshot()}, themeModeNoColor, width, height)
model.detect = func(_ context.Context, _ string) (probe.Capabilities, error) {
return probe.Capabilities{HTTP: true}, nil
}
model = updateKey(model, "m")
model = updateKey(model, "e")
model = updateKey(model, "tab")
updated, command := model.Update(tea.KeyMsg{Type: tea.KeyCtrlD})
model = updated.(Model)
if command == nil {
t.Fatal("Protocol Probe did not start")
}
return model
},
want: []string{"Protocol Probe", "checking proxy.example:8080", "in progress"},
},
{
name: "Help",
setup: func(t *testing.T, width, height int) Model {
model := resizedModel(t, &recordingModule{snapshot: hardeningSnapshot()}, themeModeNoColor, width, height)
return updateKey(model, "?")
},
want: []string{"Contextual Help", "q/Esc: close Help"},
},
{
name: "Drift Resolution",
setup: func(t *testing.T, width, height int) Model {
model := resizedModel(t, &recordingModule{snapshot: hardeningSnapshot()}, themeModeNoColor, width, height)
model = updateKey(model, "down")
return updateKey(model, " ")
},
want: []string{"Drift Resolution", "Reapply Active Profile", "Restore Original"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
for _, size := range []struct{ width, height int }{{120, 36}, {80, 24}} {
model := test.setup(t, size.width, size.height)
assertCellBoundedFrame(t, model.View(), size.width, size.height, test.want...)
}
model := test.setup(t, 80, 24)
model = resize(model, 59, 15)
assertCellBoundedFrame(t, model.View(), 59, 15, "Terminal too small", "59×15")
model = resize(model, 80, 24)
assertCellBoundedFrame(t, model.View(), 80, 24, test.want...)
})
}
}
func TestCompleteModelLightDarkAndNoColorRetainEquivalentSemantics(t *testing.T) {
views := make(map[themeMode]string)
for _, mode := range []themeMode{themeModeDark, themeModeLight, themeModeNoColor} {
module := &targetModule{
snapshot: hardeningSnapshot(),
result: app.Result{Snapshot: hardeningSnapshot(), Err: errors.New("runner failed https://alice:theme-secret@proxy.example:8080")},
}
model := resizedModel(t, module, mode, 80, 24)
updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace})
model = updated.(Model)
model = updateKey(model, "tab")
progressFrame := model.View()
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", "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])
}
}
}
plain := views[themeModeNoColor]
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)
}
}
for _, r := range plain {
if (r >= '\ue000' && r <= '\uf8ff') || (r >= '\U000f0000' && r <= '\U000ffffd') || (r >= '\U00100000' && r <= '\U0010fffd') {
t.Fatalf("rendering depends on a private-use/Nerd Font glyph %U", r)
}
}
for _, mode := range []themeMode{themeModeDark, themeModeLight} {
if got := ansiPattern.ReplaceAllString(views[mode], ""); got != plain {
t.Fatalf("theme %d changed semantic layout\ncolored stripped:\n%s\nNO_COLOR:\n%s", mode, got, plain)
}
}
}
func TestCompleteModelBoundsUnicodeAndKeepsLongContentReachable(t *testing.T) {
longName := strings.Repeat("東京界", 50) + " TARGET-END"
longProfile := strings.Repeat("支店", 70) + " PROFILE-END"
snapshot := hardeningSnapshot()
snapshot.ActiveProfile = longProfile
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].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")
model = updateKey(model, "tab")
for range 120 {
model = updateKey(model, "pgdown")
}
assertCellBoundedFrame(t, model.View(), 80, 24, "EVIDENCE-END")
model = updateKey(model, "m")
for range 60 {
model = updateKey(model, "pgdown")
}
assertCellBoundedFrame(t, model.View(), 80, 24, "PROFILE-END")
model = updateKey(model, "d")
for _, size := range []struct{ width, height int }{{80, 24}, {120, 36}, {59, 15}, {80, 24}} {
model = resize(model, size.width, size.height)
if size.width < minimumWidth {
assertCellBoundedFrame(t, model.View(), size.width, size.height, "Terminal too small")
continue
}
assertCellBoundedFrame(t, model.View(), size.width, size.height, "Delete Proxy Profile", "PROFILE-END", "> Cancel")
}
}
func TestCompleteModelLongFieldsAndDialogsRemainReachableAcrossResizeAtNonterminalOffsets(t *testing.T) {
t.Run("long URL field and Bypass preview", func(t *testing.T) {
snapshot := hardeningSnapshot()
snapshot.Profiles[0].SingleURL = "http://start.example:8080/" + strings.Repeat("a", 90) + " URL-MIDDLE " + strings.Repeat("z", 90) + "URL-END"
bypass := make([]string, 40)
for i := range bypass {
bypass[i] = fmt.Sprintf("host-%02d.example", i+1)
}
snapshot.Profiles[0].Bypass = bypass
model := resizedModel(t, &recordingModule{snapshot: snapshot}, themeModeNoColor, 80, 24)
model = updateKey(model, "m")
model = updateKey(model, "e")
model = updateKey(model, "tab")
for range 115 {
model = updateKey(model, "left")
}
assertCellBoundedFrame(t, model.View(), 80, 24, "URL-MIDDLE")
model = resize(model, 120, 36)
assertCellBoundedFrame(t, model.View(), 120, 36, "URL-MIDDLE")
model = resize(model, 80, 24)
model = updateKey(model, "tab")
model = updateKey(model, "home")
model = updateKey(model, "pgdown")
model = updateKey(model, "pgdown")
view := model.View()
assertCellBoundedFrame(t, view, 80, 24, "host-10.example")
if strings.Contains(view, "host-40.example") {
t.Fatalf("Bypass preview unexpectedly jumped to its terminal offset:\n%s", view)
}
model = resize(model, 120, 36)
assertCellBoundedFrame(t, model.View(), 120, 36, "host-10.example")
})
t.Run("expanded error", func(t *testing.T) {
snapshot := hardeningSnapshot()
lines := make([]string, 60)
for i := range lines {
lines[i] = fmt.Sprintf("ERROR-LINE-%02d", i+1)
}
module := &targetModule{snapshot: snapshot, result: app.Result{Snapshot: snapshot, Err: errors.New(strings.Join(lines, "\n"))}}
model := resizedModel(t, module, themeModeNoColor, 80, 24)
updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace})
model = updated.(Model)
model, _ = applyOperationCommand(t, model, command)
model = updateKey(model, "e")
model = updateKey(model, "pgdown")
model = updateKey(model, "pgdown")
view := model.View()
assertCellBoundedFrame(t, view, 80, 24, "ERROR-LINE-05")
if strings.Contains(view, "ERROR-LINE-60") {
t.Fatalf("expanded error unexpectedly jumped to its terminal offset:\n%s", view)
}
model = resize(model, 120, 36)
assertCellBoundedFrame(t, model.View(), 120, 36, "ERROR-LINE-10")
})
t.Run("arbitrary confirmation details and labels", func(t *testing.T) {
snapshot := hardeningSnapshot()
updates := make(chan app.OperationUpdate, 1)
module := &interactiveScriptModule{snapshot: snapshot, streams: []chan app.OperationUpdate{updates}}
model := resizedModel(t, module, themeModeNoColor, 80, 24)
updated, command := model.Update(keyMessage("r"))
model = updated.(Model)
details := make([]string, 45)
for i := range details {
details[i] = fmt.Sprintf("CONFIRM-DETAIL-%02d", i+1)
}
updates <- app.ConfirmationOperationUpdate(snapshot, app.Confirmation{
Title: "Long confirmation", Summary: "Review every detail", Details: details,
CancelLabel: "CANCEL-START " + strings.Repeat("cancel ", 50) + "CANCEL-MIDDLE " + strings.Repeat("later ", 50) + "CANCEL-END",
ConfirmLabel: "CONFIRM-START " + strings.Repeat("confirm ", 50) + "CONFIRM-MIDDLE " + strings.Repeat("later ", 50) + "CONFIRM-END",
})
model, _ = applyOperationCommand(t, model, command)
model = updateKey(model, "pgdown")
model = updateKey(model, "pgdown")
view := model.View()
assertCellBoundedFrame(t, view, 80, 24, "CONFIRM-DETAIL-10")
if strings.Contains(view, "CONFIRM-DETAIL-45") {
t.Fatalf("confirmation unexpectedly jumped to its terminal offset:\n%s", view)
}
model = resize(model, 120, 36)
assertCellBoundedFrame(t, model.View(), 120, 36, "CONFIRM-DETAIL-10")
model = resize(model, 80, 24)
cancelMiddleAtNonterminal, confirmMiddleAtNonterminal := false, false
for range 20 {
model = updateKey(model, "pgdown")
view = model.View()
if strings.Contains(view, "CANCEL-MIDDLE") && !strings.Contains(view, "CANCEL-END") {
cancelMiddleAtNonterminal = true
}
if strings.Contains(view, "CONFIRM-MIDDLE") && !strings.Contains(view, "CONFIRM-END") {
confirmMiddleAtNonterminal = true
}
}
if !cancelMiddleAtNonterminal || !confirmMiddleAtNonterminal {
t.Fatalf("long confirmation labels were not reachable before their terminal offsets: cancel=%t confirm=%t\n%s", cancelMiddleAtNonterminal, confirmMiddleAtNonterminal, model.View())
}
})
}
func TestCompleteModelResizeStressPreservesDialogFocusSelectionViewportInFlightAndDeferredExit(t *testing.T) {
snapshot := hardeningSnapshot()
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)
updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace})
model = updated.(Model)
updates <- app.RunningOperationUpdate(snapshot,
app.OperationStatus{Summary: "Writing Shell configuration", TargetID: "shell"},
[]app.TargetProgress{{TargetID: "shell", Phase: app.TargetProgressRunning}})
model, command = applyOperationCommand(t, model, command)
model = updateKey(model, "down")
model = updateKey(model, "tab")
for range 120 {
model = updateKey(model, "pgdown")
}
model = updateKey(model, "?")
model = updateKey(model, "end")
updated, quit := model.Update(tea.KeyMsg{Type: tea.KeyCtrlC})
model = updated.(Model)
if quit != nil {
t.Fatal("exit was not deferred during the in-flight mutation")
}
for _, size := range []struct{ width, height int }{{80, 24}, {59, 15}, {120, 36}, {80, 24}} {
model = resize(model, size.width, size.height)
if size.width < minimumWidth {
assertCellBoundedFrame(t, model.View(), size.width, size.height, "Terminal too small")
continue
}
assertCellBoundedFrame(t, model.View(), size.width, size.height,
"Global exit safety", "q/Esc: close Help", "Exit pending until safe completion")
}
model = updateKey(model, "q")
assertCellBoundedFrame(t, model.View(), 80, 24,
"> ! 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"},
[]app.TargetProgress{{TargetID: "shell", Phase: app.TargetProgressSucceeded}})
close(updates)
model, quit = applyOperationCommand(t, model, command)
if quit == nil {
t.Fatal("safe completion did not retain deferred exit intent")
}
if _, ok := quit().(tea.QuitMsg); !ok {
t.Fatal("safe completion did not emit Bubble Tea quit")
}
}
func TestCompleteModelStripsThemeAllowlistedANSIAndTerminalControlsBeforeSemanticStyling(t *testing.T) {
const plain = "INJECTED"
attack := "\x1b[38;5;75m" + plain + "\x1b[0m\x1b[1;38;5;160m\x1b]8;;https://attacker.invalid\x07\x9b\x00\r"
for _, mode := range []themeMode{themeModeDark, themeModeLight, themeModeNoColor} {
t.Run(fmt.Sprintf("theme-%d", mode), func(t *testing.T) {
got := completeModelInjectionFrames(t, mode, attack)
want := completeModelInjectionFrames(t, mode, plain)
if got != want {
t.Fatalf("untrusted presentation text affected rendered styling\n--- hostile ---\n%q\n--- clean ---\n%q", got, want)
}
if strings.ContainsAny(got, "\x00\x07\r\x9b") || strings.Contains(got, "attacker.invalid") {
t.Fatalf("terminal control payload survived complete-Model rendering: %q", got)
}
})
}
}
func completeModelInjectionFrames(t *testing.T, mode themeMode, value string) string {
t.Helper()
snapshot := hardeningSnapshot()
snapshot.ActiveProfile = value
snapshot.Profiles[0] = profile.Profile{Name: value, Mode: profile.SingleMode, SingleURL: value, Bypass: []string{value}}
snapshot.Targets[0].Description = target.Description{
Name: value, Summary: value, Details: value, Scope: value,
Permissions: value, Ownership: value, RestartRisk: value,
}
snapshot.Targets[0].Evidence = value
snapshot.Targets[0].AvailabilityEvidence = value
snapshot.Targets[0].PrimaryAction.DisabledReason = value
updates := make(chan app.OperationUpdate, 3)
confirmations := make(chan app.OperationUpdate, 1)
module := &interactiveScriptModule{snapshot: snapshot, streams: []chan app.OperationUpdate{updates, confirmations}}
model := resizedModel(t, module, mode, 120, 36)
frames := []string{model.View()}
model = updateKey(model, "?")
frames = append(frames, model.View())
model = updateKey(model, "q")
model = updateKey(model, "p")
updated, _ := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(value)})
model = updated.(Model)
frames = append(frames, model.View())
model = updateKey(model, "esc")
model = updateKey(model, "m")
frames = append(frames, model.View())
model = updateKey(model, "e")
frames = append(frames, model.View())
model = updateKey(model, "q")
model = updateKey(model, "q")
updated, command := model.Update(keyMessage("r"))
model = updated.(Model)
updates <- app.RunningOperationUpdate(snapshot,
app.OperationStatus{Summary: value, TargetID: "shell"},
[]app.TargetProgress{{TargetID: "shell", Phase: app.TargetProgressRunning, Evidence: value, Err: errors.New(value)}})
model, command = applyOperationCommand(t, model, command)
frames = append(frames, model.View())
updates <- app.FailedOperationUpdate(snapshot,
app.OperationStatus{Summary: value, TargetID: "shell"},
[]app.TargetProgress{{TargetID: "shell", Phase: app.TargetProgressFailed, Evidence: value, Err: errors.New(value)}},
"shell", errors.New(value))
close(updates)
model, _ = applyOperationCommand(t, model, command)
model = updateKey(model, "e")
frames = append(frames, model.View())
updated, command = model.Update(keyMessage("r"))
model = updated.(Model)
confirmations <- app.ConfirmationOperationUpdate(snapshot, app.Confirmation{
Title: value, Summary: value, Details: []string{value}, ConfirmLabel: value, CancelLabel: value,
})
model, _ = applyOperationCommand(t, model, command)
frames = append(frames, model.View())
return strings.Join(frames, "\n--- frame ---\n")
}
func TestCompleteModelRedactsMalformedSecretMatrixFromEveryRenderedIngress(t *testing.T) {
tests := []struct {
name string
value string
secrets []string
}{
{"valid URL", "https://alice:valid-secret@proxy.example:8080", []string{"valid-secret"}},
{"unsupported scheme CRLF spaces and slash", "odd+scheme://alice:line one/part\r\ntwo@proxy.example:8080", []string{"line one/part\r\ntwo"}},
{"bare malformed userinfo", "alice:bare secret/part@proxy.example:8080", []string{"bare secret/part"}},
{"multiple at signs", "http://alice:sec@ret@proxy.example:8080", []string{"sec@ret"}},
{"multiple URLs", "first http://alice:first-secret@one.example:1 then invalid://bob:second secret@two.example:2", []string{"first-secret", "second secret"}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
frames := completeModelSecretFrames(t, test.value)
for _, secret := range test.secrets {
assertSecretAbsent(t, frames, secret)
}
for _, surrounding := range []string{"before diagnostic", "after diagnostic"} {
if !strings.Contains(frames, surrounding) {
t.Fatalf("redaction swallowed surrounding diagnostics %q:\n%s", surrounding, frames)
}
}
})
}
}
func completeModelSecretFrames(t *testing.T, credential string) string {
t.Helper()
hostile := "before diagnostic \x1b]2;hostile-title\x07\x1b[8m" + credential + "\r after diagnostic"
snapshot := hardeningSnapshot()
snapshot.ActiveProfile = hostile
snapshot.Profiles[0] = profile.Profile{Name: credential, Mode: profile.SingleMode, SingleURL: credential, Bypass: []string{credential}}
snapshot.Targets[0].Description = target.Description{
Name: credential, Summary: credential, Details: credential, Scope: credential,
Permissions: credential, Ownership: credential, RestartRisk: credential,
}
snapshot.Targets[0].Evidence = hostile
snapshot.Targets[0].AvailabilityEvidence = hostile
snapshot.Targets[0].PrimaryAction.DisabledReason = hostile
updates := make(chan app.OperationUpdate, 3)
confirmation := make(chan app.OperationUpdate, 1)
module := &interactiveScriptModule{snapshot: snapshot, streams: []chan app.OperationUpdate{updates, confirmation}}
model := resizedModel(t, module, themeModeNoColor, 120, 36)
frames := []string{model.View()}
model = updateKey(model, "?")
frames = append(frames, model.View())
model = updateKey(model, "q")
model = updateKey(model, "p")
frames = append(frames, model.View())
model = updateKey(model, "esc")
model = updateKey(model, "m")
frames = append(frames, model.View())
model = updateKey(model, "e")
frames = append(frames, model.View())
model = updateKey(model, "q")
model = updateKey(model, "q")
updated, command := model.Update(keyMessage("r"))
model = updated.(Model)
updates <- app.RunningOperationUpdate(snapshot,
app.OperationStatus{Summary: hostile, TargetID: "shell"},
[]app.TargetProgress{{TargetID: "shell", Phase: app.TargetProgressRunning, Evidence: hostile, Err: errors.New(hostile)}})
model, command = applyOperationCommand(t, model, command)
frames = append(frames, model.View())
updates <- app.FailedOperationUpdate(snapshot,
app.OperationStatus{Summary: hostile, TargetID: "shell"},
[]app.TargetProgress{{TargetID: "shell", Phase: app.TargetProgressFailed, Evidence: hostile, Err: errors.New(hostile)}},
"shell", errors.New(hostile))
close(updates)
model, _ = applyOperationCommand(t, model, command)
model = updateKey(model, "e")
frames = append(frames, model.View())
updated, command = model.Update(keyMessage("r"))
model = updated.(Model)
confirmation <- app.ConfirmationOperationUpdate(snapshot, app.Confirmation{
Title: hostile, Summary: hostile, Details: []string{hostile}, ConfirmLabel: hostile, CancelLabel: hostile,
})
model, _ = applyOperationCommand(t, model, command)
frames = append(frames, model.View())
return strings.Join(frames, "\n--- frame ---\n")
}
func hardeningSnapshot() app.Snapshot {
return app.Snapshot{
ActiveProfile: "office",
Profiles: []profile.Profile{
{Name: "office", Mode: profile.SingleMode, SingleURL: "http://proxy.example:8080", Bypass: []string{"localhost", ".example.com"}},
{Name: "remote", Mode: profile.SingleMode, SingleURL: "socks5h://remote.example:1080"},
},
Targets: []app.TargetSnapshot{
{ID: "shell", Description: target.Description{Name: "Shell", Summary: "Shell summary", Details: "Shell details"}, Available: true, State: target.Disabled, Evidence: "fragment absent", PrimaryAction: app.TargetAction{Kind: app.EnableTargetAction, Available: true}},
{ID: "apt", Description: target.Description{Name: "APT", Summary: "APT summary", Details: "APT details"}, Available: true, State: target.Drifted, Evidence: "managed values differ", PrimaryAction: app.TargetAction{Kind: app.ResolveDriftTargetAction, Available: true}, DriftResolutionActions: []app.TargetAction{{Kind: app.ReapplyActiveProfileAction, Available: true}, {Kind: app.RestoreOriginalAction, Available: true}}},
},
}
}
func assertCellBoundedFrame(t *testing.T, view string, width, height int, semantics ...string) {
t.Helper()
plain := ansiPattern.ReplaceAllString(view, "")
lines := strings.Split(strings.TrimSuffix(plain, "\n"), "\n")
if len(lines) > height {
t.Fatalf("frame has %d lines, terminal height is %d:\n%s", len(lines), height, view)
}
for _, line := range lines {
if got := lipgloss.Width(line); got > width {
t.Fatalf("frame line is %d cells, terminal width is %d: %q", got, width, line)
}
}
for _, semantic := range semantics {
if !strings.Contains(plain, semantic) {
t.Fatalf("frame missing %q:\n%s", semantic, view)
}
}
}
func assertSecretAbsent(t *testing.T, frame, secret string) {
t.Helper()
if strings.Contains(frame, secret) {
t.Fatalf("rendered frame exposed secret %q:\n%s", secret, frame)
}
if strings.ContainsAny(frame, "\x1b\x07\r") {
t.Fatalf("rendered frame retained hostile terminal control bytes: %q", frame)
}
}