Skip to content

Commit 2caa064

Browse files
committed
test(#544,#607): mechanism guard for csiuFileReader Fd() preservation
Adds 3 unit tests that guard the mechanism by which NewCSIuReader avoids breaking Bubble Tea's raw-terminal-mode setup. Tests: - TestCSIuReader_PreservesFdForRawMode: asserts NewCSIuReader(*os.File) returns a value with Fd() equal to the original file's Fd(). - TestCSIuReader_NonFileFallsBackToPlainReader: complementary guard. - TestCSIuReader_UnderscorePreserved: guards the 02-01 fix. Historical context: PR #538 (Apr 8) removed tea.WithInput to fix #544; commit 817a616 (Apr 12) re-added it for 02-01 with csiuFileReader embedding *os.File so Fd() promotes through Bubble Tea's type assertion. No existing test guarded the embedding mechanism — integration tests pass in headless tmux while real terminals could break. Closes #544 (mechanism-intact verified). References #607 (separate timing concern not addressed).
1 parent 94a003e commit 2caa064

File tree

1 file changed

+109
-0
lines changed

1 file changed

+109
-0
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package ui
2+
3+
import (
4+
"bytes"
5+
"os"
6+
"testing"
7+
)
8+
9+
// TestCSIuReader_PreservesFdForRawMode is a MECHANISM guard test.
10+
//
11+
// Why this test exists: On 2026-04-08, PR #538 removed
12+
// `tea.WithInput(ui.NewCSIuReader(os.Stdin))` because the wrapper stripped
13+
// the *os.File interface from stdin, preventing Bubble Tea from setting raw
14+
// terminal mode. Arrow keys appeared as literal "^[[A" text (issue #544,
15+
// #539, #607).
16+
//
17+
// On 2026-04-12, commit 817a616 needed the wrapper back to translate CSI u
18+
// sequences for underscore input (issue 02-01). To AVOID re-introducing the
19+
// raw-mode bug, that commit introduced csiuFileReader which embeds *os.File.
20+
// Embedding promotes Fd() / Read / Write / Close to satisfy
21+
// github.com/charmbracelet/x/term.File, which Bubble Tea checks via type
22+
// assertion in tty_unix.go before calling term.MakeRaw.
23+
//
24+
// If a future refactor:
25+
// - Removes the csiuFileReader struct
26+
// - Changes NewCSIuReader to return plain *csiuReader even for *os.File input
27+
// - Removes the *os.File embed from csiuFileReader
28+
//
29+
// then raw terminal mode will silently stop being set, and #544/#607 will
30+
// regress. Every existing integration test (PR #541) may still pass in
31+
// headless-tmux environments but real interactive terminals will break.
32+
//
33+
// This test asserts the MECHANISM (type-assertion chain) directly, not a
34+
// symptom (arrow-key echo), so it catches the reversion regardless of test
35+
// environment.
36+
func TestCSIuReader_PreservesFdForRawMode(t *testing.T) {
37+
// Use a real *os.File so Fd() returns a meaningful descriptor.
38+
// os.Stdin works even in test environments because its Fd() is defined
39+
// (it's the inherited stdin from the go test runner).
40+
f := os.Stdin
41+
wantFd := f.Fd()
42+
43+
got := NewCSIuReader(f)
44+
45+
// The returned reader MUST satisfy the Fd()-having interface so Bubble
46+
// Tea can type-assert and call term.MakeRaw on the descriptor.
47+
fdProvider, ok := got.(interface{ Fd() uintptr })
48+
if !ok {
49+
t.Fatalf("NewCSIuReader(*os.File) returned a reader that does NOT implement Fd() uintptr.\n"+
50+
"Bubble Tea's tty_unix.go checks `p.input.(term.File)` and calls term.MakeRaw on Fd().\n"+
51+
"Without Fd(), raw mode is never set → arrow keys echo as '^[[A' → TUI unusable.\n"+
52+
"Concrete type returned: %T. See keyboard_compat.go and issue #544.", got)
53+
}
54+
55+
if fdProvider.Fd() != wantFd {
56+
t.Errorf("Fd() returned %d, want %d (the original os.Stdin fd). "+
57+
"The returned reader must preserve the original file descriptor so "+
58+
"Bubble Tea sets raw mode on the correct terminal.", fdProvider.Fd(), wantFd)
59+
}
60+
}
61+
62+
// TestCSIuReader_NonFileFallsBackToPlainReader asserts the complementary case:
63+
// when NewCSIuReader is given an io.Reader that is NOT a *os.File (e.g., a
64+
// bytes.Buffer in tests), it returns a plain *csiuReader. This keeps the
65+
// translation behavior intact for test harnesses while not synthesizing a
66+
// fake Fd() that would mislead Bubble Tea.
67+
func TestCSIuReader_NonFileFallsBackToPlainReader(t *testing.T) {
68+
buf := bytes.NewBuffer([]byte{})
69+
70+
got := NewCSIuReader(buf)
71+
72+
// Should NOT satisfy Fd() — we don't want to lie about file descriptors.
73+
if _, ok := got.(interface{ Fd() uintptr }); ok {
74+
t.Errorf("NewCSIuReader(bytes.Buffer) unexpectedly satisfies Fd() uintptr. "+
75+
"Only *os.File input should return a reader with Fd() — otherwise "+
76+
"Bubble Tea would try to call term.MakeRaw on a fake fd. "+
77+
"Concrete type returned: %T", got)
78+
}
79+
}
80+
81+
// TestCSIuReader_UnderscorePreserved guards the fix from commit 817a616
82+
// (issue 02-01): Shift+hyphen sends CSI u sequence \x1b[95;2u in terminals
83+
// with extended-keys (Ghostty, Alacritty, tmux). The reader must translate
84+
// this to a literal '_' byte so TUI text inputs receive the underscore.
85+
//
86+
// If this regresses (e.g., by removing csiuReader entirely), dialog text
87+
// inputs will silently drop underscores. This test, together with
88+
// TestCSIuReader_PreservesFdForRawMode, guards the full fix space: the
89+
// wrapper MUST exist AND MUST preserve Fd() for *os.File input.
90+
func TestCSIuReader_UnderscorePreserved(t *testing.T) {
91+
// CSI u encoding for Shift+hyphen → codepoint 95 ('_') with shift modifier.
92+
// Sequence: ESC [ 95 ; 2 u
93+
input := bytes.NewBuffer([]byte("\x1b[95;2u"))
94+
r := NewCSIuReader(input)
95+
96+
out := make([]byte, 16)
97+
n, err := r.Read(out)
98+
if err != nil && err.Error() != "EOF" {
99+
t.Fatalf("Read failed: %v", err)
100+
}
101+
102+
got := string(out[:n])
103+
if got != "_" {
104+
t.Errorf("expected CSI u underscore sequence to translate to '_', got %q (%d bytes). "+
105+
"If this test fails, TUI dialog text inputs will silently drop underscores "+
106+
"on Ghostty/Alacritty/tmux-extended-keys terminals. See issue 02-01 / commit 817a616.",
107+
got, n)
108+
}
109+
}

0 commit comments

Comments
 (0)