termd/internal/session/vtpin_test.go
2026-07-02 22:13:49 -03:00

156 lines
4.2 KiB
Go

package session
// These tests pin the behavior of the unversioned charmbracelet/x/vt
// dependency. If an upstream bump changes input encoding or mode-callback
// semantics, they fail loudly instead of termd silently emitting wrong bytes.
import (
"io"
"sync"
"testing"
"time"
uv "github.com/charmbracelet/ultraviolet"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/vt"
)
// collector continuously drains the emulator's encoded-input pipe. It must be
// running before any Send* call: the pipe is synchronous, so an unread write
// blocks forever (the daemon's pump goroutine plays this role in production).
type collector struct {
mu sync.Mutex
buf []byte
}
func collect(em *vt.Emulator) *collector {
c := &collector{}
go func() {
b := make([]byte, 4096)
for {
n, err := em.Read(b)
if n > 0 {
c.mu.Lock()
c.buf = append(c.buf, b[:n]...)
c.mu.Unlock()
}
if err != nil {
return
}
}
}()
return c
}
// expect polls until the collected bytes equal want, then resets the buffer.
func (c *collector) expect(t *testing.T, label, want string) {
t.Helper()
deadline := time.Now().Add(2 * time.Second)
for {
c.mu.Lock()
got := string(c.buf)
c.mu.Unlock()
if got == want {
c.mu.Lock()
c.buf = nil
c.mu.Unlock()
return
}
if time.Now().After(deadline) {
t.Fatalf("%s: got %q, want %q", label, got, want)
}
time.Sleep(5 * time.Millisecond)
}
}
// expectNothing asserts no bytes arrive within a settle window.
func (c *collector) expectNothing(t *testing.T, label string) {
t.Helper()
time.Sleep(50 * time.Millisecond)
c.mu.Lock()
got := string(c.buf)
c.mu.Unlock()
if got != "" {
t.Fatalf("%s: got %q, want nothing", label, got)
}
}
func TestVTKeyEncodingRespectsDECCKM(t *testing.T) {
em := vt.NewEmulator(80, 24)
c := collect(em)
em.SendKey(uv.KeyPressEvent{Code: uv.KeyUp})
em.SendKey(uv.KeyPressEvent{Code: 'c', Mod: uv.ModCtrl})
em.SendKey(uv.KeyPressEvent{Code: uv.KeyEnter})
c.expect(t, "normal mode", "\x1b[A\x03\r")
// Application cursor keys (DECSET ?1) switches arrows to SS3.
if _, err := io.WriteString(em, "\x1b[?1h"); err != nil {
t.Fatal(err)
}
em.SendKey(uv.KeyPressEvent{Code: uv.KeyUp})
c.expect(t, "DECCKM mode", "\x1bOA")
}
func TestVTMouseEncoding(t *testing.T) {
em := vt.NewEmulator(80, 24)
c := collect(em)
// No tracking mode: SendMouse must emit nothing (termd's 409 pre-check
// assumes this, but must not depend on it).
em.SendMouse(uv.MouseClickEvent{X: 1, Y: 1, Button: uv.MouseLeft})
c.expectNothing(t, "mouse without tracking mode")
// SGR encoding (?1000 + ?1006): CSI < b;x+1;y+1 M/m, 1-based cells.
if _, err := io.WriteString(em, "\x1b[?1000h\x1b[?1006h"); err != nil {
t.Fatal(err)
}
em.SendMouse(uv.MouseClickEvent{X: 10, Y: 5, Button: uv.MouseLeft})
em.SendMouse(uv.MouseReleaseEvent{X: 10, Y: 5, Button: uv.MouseLeft})
c.expect(t, "SGR click", "\x1b[<0;11;6M\x1b[<0;11;6m")
em.SendMouse(uv.MouseWheelEvent{X: 0, Y: 0, Button: uv.MouseWheelUp})
c.expect(t, "SGR wheel", "\x1b[<64;1;1M")
// Legacy X10 encoding when ?1006 is off.
if _, err := io.WriteString(em, "\x1b[?1006l"); err != nil {
t.Fatal(err)
}
em.SendMouse(uv.MouseClickEvent{X: 2, Y: 3, Button: uv.MouseLeft})
c.expect(t, "X10 click", "\x1b[M\x20\x23\x24")
}
func TestVTModeCallbacks(t *testing.T) {
em := vt.NewEmulator(80, 24)
var ms modeState
ms.cursorVisible = true
altScreen := false
em.SetCallbacks(vt.Callbacks{
EnableMode: func(m ansi.Mode) { ms.set(m, true) },
DisableMode: func(m ansi.Mode) { ms.set(m, false) },
AltScreen: func(on bool) { altScreen = on },
})
if _, err := io.WriteString(em, "\x1b[?1049h\x1b[?1h\x1b[?1002h\x1b[?1006h\x1b[?2004h"); err != nil {
t.Fatal(err)
}
if !altScreen {
t.Error("alt screen callback did not fire on ?1049h")
}
if got := ms.mouseMode(); got != "button_event" {
t.Errorf("mouse mode = %q, want button_event", got)
}
if !ms.sgrExt || !ms.appCursorKeys || !ms.bracketedPaste {
t.Errorf("modes not all tracked: %+v", ms)
}
if _, err := io.WriteString(em, "\x1b[?1002l\x1b[?1049l"); err != nil {
t.Fatal(err)
}
if altScreen {
t.Error("alt screen still on after ?1049l")
}
if got := ms.mouseMode(); got != "off" {
t.Errorf("mouse mode after disable = %q, want off", got)
}
}