fetch_ml/tests/e2e/tui_ssh_test.go
Jeremie Fraeys b4672a6c25
feat: add TUI SSH usability testing infrastructure
Add comprehensive testing for TUI usability over SSH in production-like environment:

Infrastructure:
- Caddy reverse proxy config for WebSocket and API routing
- Docker Compose with SSH test server container
- TUI test configuration for smoke testing

Test Harness:
- SSH server Go test fixture with container management
- TUI driver with PTY support for automated input/output testing
- 8 E2E tests covering SSH connectivity, TERM propagation,
  API/WebSocket connectivity, and TUI configuration

Scripts:
- SSH key generation for test environment
- Manual testing script with interactive TUI verification

The setup allows automated verification that the BubbleTea TUI works
correctly over SSH with proper terminal handling, alt-screen buffer,
and mouse support through Caddy reverse proxy.
2026-02-18 17:48:02 -05:00

185 lines
5.2 KiB
Go

// Package tests provides E2E tests for TUI SSH usability testing
package tests
import (
"strings"
"testing"
"time"
testfixtures "github.com/jfraeys/fetch_ml/tests/fixtures"
)
// TestTUI_SSHBasicConnectivity verifies SSH connection and basic command execution
func TestTUI_SSHBasicConnectivity(t *testing.T) {
t.Parallel()
server := testfixtures.NewSSHTestServer(t)
// Test basic command execution
output, err := server.Exec("echo 'SSH connection successful'")
if err != nil {
t.Fatalf("Failed to execute command over SSH: %v", err)
}
if !strings.Contains(output, "SSH connection successful") {
t.Errorf("Expected SSH output not found. Got: %s", output)
}
}
// TestTUI_TERMPropagation verifies TERM variable is propagated correctly
func TestTUI_TERMPropagation(t *testing.T) {
t.Parallel()
server := testfixtures.NewSSHTestServer(t)
// Check TERM variable
output, err := server.Exec("echo TERM=$TERM")
if err != nil {
t.Fatalf("Failed to check TERM: %v", err)
}
// Should have a TERM value set
if !strings.Contains(output, "TERM=") {
t.Errorf("TERM not set in SSH session. Got: %s", output)
}
t.Logf("TERM value: %s", strings.TrimSpace(output))
}
// TestTUI_CaddyConnectivity verifies TUI can reach API through Caddy
func TestTUI_CaddyConnectivity(t *testing.T) {
t.Parallel()
server := testfixtures.NewSSHTestServer(t)
// Test API health through Caddy (TUI connects to Caddy on port 80)
output, err := server.Exec("curl -s http://caddy:80/health")
if err != nil {
// Try direct API connection as fallback
output, err = server.Exec("curl -s http://api-server:9101/health")
if err != nil {
t.Skipf("API not reachable from SSH container. Ensure docker-compose services are running: %v", err)
}
}
if !strings.Contains(output, `"status":"healthy"`) && !strings.Contains(output, `healthy`) {
t.Errorf("API health check failed. Got: %s", output)
}
}
// TestTUI_WebSocketViaCaddy verifies WebSocket proxy through Caddy works
func TestTUI_WebSocketViaCaddy(t *testing.T) {
t.Parallel()
server := testfixtures.NewSSHTestServer(t)
// Test WebSocket upgrade works through Caddy
output, err := server.Exec("curl -i -N -H 'Connection: Upgrade' -H 'Upgrade: websocket' http://caddy:80/ws/status 2>&1 | head -20")
if err != nil {
// Direct API test as fallback
output, err = server.Exec("curl -i -N -H 'Connection: Upgrade' -H 'Upgrade: websocket' http://api-server:9101/ws/status 2>&1 | head -20")
if err != nil {
t.Skipf("WebSocket test skipped - services may not be running: %v", err)
}
}
// Should see upgrade headers or connection
if !strings.Contains(output, "Upgrade") && !strings.Contains(output, "101") {
t.Logf("WebSocket response: %s", output)
}
}
// TestTUI_TUIConfigExists verifies TUI config is mounted correctly
func TestTUI_TUIConfigExists(t *testing.T) {
t.Parallel()
server := testfixtures.NewSSHTestServer(t)
// Check TUI config exists
output, err := server.Exec("cat /config/.ml/config.toml")
if err != nil {
t.Fatalf("TUI config not found in SSH container: %v", err)
}
// Verify config content
requiredFields := []string{"worker_host", "worker_port", "worker_user"}
for _, field := range requiredFields {
if !strings.Contains(output, field) {
t.Errorf("Config missing required field: %s", field)
}
}
t.Logf("TUI config:\n%s", output)
}
// TestTUI_TUIRuns verifies TUI binary executes without errors
func TestTUI_TUIRuns(t *testing.T) {
t.Parallel()
server := testfixtures.NewSSHTestServer(t)
// Verify TUI binary exists and is executable
output, err := server.Exec("ls -la /usr/local/bin/tui")
if err != nil {
t.Fatalf("TUI binary not found: %v", err)
}
if !strings.Contains(output, "tui") {
t.Errorf("TUI binary not found. Got: %s", output)
}
// Test TUI can start (help/version check)
// Note: Full TUI test requires PTY which is in integration tests
output, err = server.Exec("file /usr/local/bin/tui")
if err == nil {
t.Logf("TUI binary info: %s", strings.TrimSpace(output))
}
}
// TestTUI_PTYSession tests PTY allocation for interactive TUI
func TestTUI_PTYSession(t *testing.T) {
t.Parallel()
server := testfixtures.NewSSHTestServer(t)
// Create PTY session
session, err := server.ExecWithPTY("echo 'PTY test'", "xterm-256color", 80, 24)
if err != nil {
t.Skipf("PTY session failed - SSH server may not support PTY: %v", err)
}
defer session.Close()
// Wait for command to complete
done := make(chan error, 1)
go func() {
done <- session.Wait()
}()
select {
case err := <-done:
if err != nil {
t.Logf("PTY session completed with error (may be expected): %v", err)
}
case <-time.After(5 * time.Second):
t.Log("PTY session timed out")
}
}
// TestTUI_TerminfoAvailable verifies terminfo database is available
func TestTUI_TerminfoAvailable(t *testing.T) {
t.Parallel()
server := testfixtures.NewSSHTestServer(t)
// Check for terminfo
output, err := server.Exec("ls /usr/share/terminfo/x/ | head -5")
if err != nil {
t.Logf("terminfo check: %v", err)
} else {
t.Logf("Available terminfo entries: %s", strings.TrimSpace(output))
}
// Check tput works
output, err = server.Exec("tput colors 2>/dev/null || echo 'tput not available'")
t.Logf("Color support: %s", strings.TrimSpace(output))
}