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{"Operations Console", "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{"Operations Console", "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() updated, _ = model.Update(command()) model = updated.(Model) 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"} { 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{"● Enabled", "○ 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].Description.Details = strings.Repeat("詳細 line ", 100) + " DETAILS-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, "DETAILS-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) updated, _ = model.Update(command()) model = updated.(Model) 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].Description.Details = strings.Repeat("APT scrollable detail. ", 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]", "APT-VIEWPORT-", "END", "Applying", "Exit requested", "new work 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) } }