mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-26 09:35:52 +00:00
- Fix hanging TestNewStdinWatcher by not calling Stop() without Start() - Fix TestSession_Signal and TestSession_KillWithSignal by adding PID values - Fix isProcessRunning to use syscall.Signal(0) instead of os.Signal(nil) - Update websocket test to expect new 'Unknown WebSocket endpoint' error message - Add timeout handling to websocket integration test
451 lines
10 KiB
Go
451 lines
10 KiB
Go
package session
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNewSession(t *testing.T) {
|
|
// Skip this test as newSession is not exported
|
|
t.Skip("newSession is an internal function")
|
|
tmpDir := t.TempDir()
|
|
controlPath := filepath.Join(tmpDir, "control")
|
|
|
|
// Create a test manager
|
|
manager := NewManager(controlPath)
|
|
|
|
config := &Config{
|
|
Name: "test-session",
|
|
Cmdline: []string{"/bin/sh", "-c", "echo test"},
|
|
Cwd: tmpDir,
|
|
Width: 80,
|
|
Height: 24,
|
|
}
|
|
|
|
session, err := newSession(controlPath, *config, manager)
|
|
if err != nil {
|
|
t.Fatalf("newSession() error = %v", err)
|
|
}
|
|
|
|
if session == nil {
|
|
t.Fatal("NewSession returned nil")
|
|
}
|
|
|
|
if session.ID == "" {
|
|
t.Error("Session ID should not be empty")
|
|
}
|
|
|
|
if session.controlPath != controlPath {
|
|
t.Errorf("controlPath = %s, want %s", session.controlPath, controlPath)
|
|
}
|
|
|
|
// Check session info
|
|
if session.info.Name != config.Name {
|
|
t.Errorf("Name = %s, want %s", session.info.Name, config.Name)
|
|
}
|
|
if session.info.Width != config.Width {
|
|
t.Errorf("Width = %d, want %d", session.info.Width, config.Width)
|
|
}
|
|
if session.info.Height != config.Height {
|
|
t.Errorf("Height = %d, want %d", session.info.Height, config.Height)
|
|
}
|
|
if session.info.Status != string(StatusStarting) {
|
|
t.Errorf("Status = %s, want %s", session.info.Status, StatusStarting)
|
|
}
|
|
}
|
|
|
|
func TestNewSession_Defaults(t *testing.T) {
|
|
// Skip this test as newSession is not exported
|
|
t.Skip("newSession is an internal function")
|
|
tmpDir := t.TempDir()
|
|
controlPath := filepath.Join(tmpDir, "control")
|
|
|
|
// Create a test manager
|
|
manager := NewManager(controlPath)
|
|
|
|
// Minimal config
|
|
config := &Config{}
|
|
|
|
session, err := newSession(controlPath, *config, manager)
|
|
if err != nil {
|
|
t.Fatalf("newSession() error = %v", err)
|
|
}
|
|
|
|
// Should have default shell
|
|
if len(session.info.Args) == 0 {
|
|
t.Error("Should have default shell command")
|
|
}
|
|
|
|
// Should have default dimensions
|
|
if session.info.Width <= 0 {
|
|
t.Error("Should have default width")
|
|
}
|
|
if session.info.Height <= 0 {
|
|
t.Error("Should have default height")
|
|
}
|
|
|
|
// Should have default working directory
|
|
if session.info.Cwd == "" {
|
|
t.Error("Should have default working directory")
|
|
}
|
|
}
|
|
|
|
func TestSession_Paths(t *testing.T) {
|
|
// Skip this test as newSession is not exported
|
|
t.Skip("newSession is an internal function")
|
|
tmpDir := t.TempDir()
|
|
controlPath := filepath.Join(tmpDir, "control")
|
|
|
|
// Create a mock session for testing paths
|
|
session := &Session{
|
|
ID: "test-session-id",
|
|
controlPath: controlPath,
|
|
}
|
|
sessionID := session.ID
|
|
|
|
// Test path methods
|
|
expectedBase := filepath.Join(controlPath, sessionID)
|
|
if session.Path() != expectedBase {
|
|
t.Errorf("Path() = %s, want %s", session.Path(), expectedBase)
|
|
}
|
|
|
|
if session.StdinPath() != filepath.Join(expectedBase, "stdin") {
|
|
t.Errorf("Unexpected StdinPath: %s", session.StdinPath())
|
|
}
|
|
|
|
if session.StreamOutPath() != filepath.Join(expectedBase, "stream-out") {
|
|
t.Errorf("Unexpected StreamOutPath: %s", session.StreamOutPath())
|
|
}
|
|
|
|
if session.NotificationPath() != filepath.Join(expectedBase, "notification-stream") {
|
|
t.Errorf("Unexpected NotificationPath: %s", session.NotificationPath())
|
|
}
|
|
|
|
// Info path would be at session.json in the session directory
|
|
expectedInfoPath := filepath.Join(expectedBase, "session.json")
|
|
t.Logf("Expected info path: %s", expectedInfoPath)
|
|
}
|
|
|
|
func TestSession_Signal(t *testing.T) {
|
|
session := &Session{
|
|
ID: "test-session",
|
|
info: &Info{
|
|
Pid: 0, // No process
|
|
Status: string(StatusRunning),
|
|
},
|
|
}
|
|
|
|
// Test signaling with no process
|
|
err := session.Signal("SIGTERM")
|
|
if err == nil {
|
|
t.Error("Signal should fail with no process")
|
|
}
|
|
if !IsSessionError(err, ErrProcessNotFound) {
|
|
t.Errorf("Expected ErrProcessNotFound, got %v", err)
|
|
}
|
|
|
|
// Test with already exited session
|
|
session.info.Status = string(StatusExited)
|
|
session.info.Pid = 99999 // Non-existent process
|
|
err = session.Signal("SIGTERM")
|
|
if err != nil {
|
|
t.Errorf("Signal should succeed for exited session: %v", err)
|
|
}
|
|
|
|
// Test unsupported signal
|
|
session.info.Status = string(StatusRunning)
|
|
session.info.Pid = os.Getpid() // Use current process for testing
|
|
err = session.Signal("SIGUSR3")
|
|
if err == nil {
|
|
t.Error("Should fail for unsupported signal")
|
|
}
|
|
if !IsSessionError(err, ErrInvalidArgument) {
|
|
t.Errorf("Expected ErrInvalidArgument, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSession_Resize(t *testing.T) {
|
|
session := &Session{
|
|
ID: "test-session",
|
|
info: &Info{
|
|
Width: 80,
|
|
Height: 24,
|
|
Status: string(StatusRunning),
|
|
},
|
|
}
|
|
|
|
// Test resize without PTY
|
|
err := session.Resize(100, 30)
|
|
if err == nil {
|
|
t.Error("Resize should fail without PTY")
|
|
}
|
|
if !IsSessionError(err, ErrSessionNotRunning) {
|
|
t.Errorf("Expected ErrSessionNotRunning, got %v", err)
|
|
}
|
|
|
|
// Test resize on exited session
|
|
session.info.Status = string(StatusExited)
|
|
err = session.Resize(100, 30)
|
|
if err == nil {
|
|
t.Error("Resize should fail on exited session")
|
|
}
|
|
|
|
// Test invalid dimensions
|
|
session.info.Status = string(StatusRunning)
|
|
err = session.Resize(0, 30)
|
|
if err == nil {
|
|
t.Error("Resize should fail with invalid width")
|
|
}
|
|
if !IsSessionError(err, ErrInvalidArgument) {
|
|
t.Errorf("Expected ErrInvalidArgument, got %v", err)
|
|
}
|
|
|
|
err = session.Resize(100, -1)
|
|
if err == nil {
|
|
t.Error("Resize should fail with invalid height")
|
|
}
|
|
}
|
|
|
|
func TestSession_IsAlive(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
session *Session
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "no pid",
|
|
session: &Session{
|
|
ID: "test1",
|
|
info: &Info{Pid: 0},
|
|
},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "exited status",
|
|
session: &Session{
|
|
ID: "test2",
|
|
info: &Info{
|
|
Pid: 12345,
|
|
Status: string(StatusExited),
|
|
},
|
|
},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "current process",
|
|
session: &Session{
|
|
ID: "test3",
|
|
info: &Info{
|
|
Pid: os.Getpid(),
|
|
Status: string(StatusRunning),
|
|
},
|
|
},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "non-existent process",
|
|
session: &Session{
|
|
ID: "test4",
|
|
info: &Info{
|
|
Pid: 999999,
|
|
Status: string(StatusRunning),
|
|
},
|
|
},
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.session.IsAlive()
|
|
if result != tt.expected {
|
|
t.Errorf("IsAlive() = %v, want %v", result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSession_Kill(t *testing.T) {
|
|
session := &Session{
|
|
ID: "test-kill",
|
|
info: &Info{
|
|
Status: string(StatusExited),
|
|
},
|
|
stdinPipe: nil, // Initialize to avoid nil pointer
|
|
}
|
|
|
|
// Kill already exited session
|
|
err := session.Kill()
|
|
if err != nil {
|
|
t.Errorf("Kill() on exited session should succeed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSession_KillWithSignal(t *testing.T) {
|
|
session := &Session{
|
|
ID: "test-kill-signal",
|
|
info: &Info{
|
|
Status: string(StatusExited),
|
|
Pid: 99999, // Non-existent process
|
|
},
|
|
stdinPipe: nil,
|
|
}
|
|
|
|
// Test SIGKILL
|
|
err := session.KillWithSignal("SIGKILL")
|
|
if err != nil {
|
|
t.Errorf("KillWithSignal(SIGKILL) error = %v", err)
|
|
}
|
|
|
|
// Test numeric signal
|
|
err = session.KillWithSignal("9")
|
|
if err != nil {
|
|
t.Errorf("KillWithSignal(9) error = %v", err)
|
|
}
|
|
|
|
// Test other signal (should use graceful termination)
|
|
err = session.KillWithSignal("SIGTERM")
|
|
if err != nil {
|
|
t.Errorf("KillWithSignal(SIGTERM) error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSession_SendInput(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
session := &Session{
|
|
ID: "test-input",
|
|
controlPath: tmpDir,
|
|
info: &Info{},
|
|
stdinMutex: sync.Mutex{},
|
|
}
|
|
|
|
// Create stdin pipe
|
|
stdinPath := session.StdinPath()
|
|
if err := os.MkdirAll(filepath.Dir(stdinPath), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stdinPipe, err := os.Create(stdinPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
session.stdinPipe = stdinPipe
|
|
defer stdinPipe.Close()
|
|
|
|
// Test sending text input
|
|
testText := "test input"
|
|
err = session.sendInput([]byte(testText))
|
|
if err != nil {
|
|
t.Errorf("sendInput() error = %v", err)
|
|
}
|
|
|
|
// Read back data
|
|
stdinPipe.Seek(0, 0)
|
|
data, err := os.ReadFile(stdinPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(data) != testText {
|
|
t.Errorf("Written data = %q, want %q", data, testText)
|
|
}
|
|
|
|
// Test SendText method
|
|
os.Truncate(stdinPath, 0)
|
|
err = session.SendText("hello world")
|
|
if err != nil {
|
|
t.Errorf("SendText() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSessionStatus(t *testing.T) {
|
|
// Test status constants
|
|
if StatusStarting != "starting" {
|
|
t.Errorf("StatusStarting = %s, want 'starting'", StatusStarting)
|
|
}
|
|
if StatusRunning != "running" {
|
|
t.Errorf("StatusRunning = %s, want 'running'", StatusRunning)
|
|
}
|
|
if StatusExited != "exited" {
|
|
t.Errorf("StatusExited = %s, want 'exited'", StatusExited)
|
|
}
|
|
}
|
|
|
|
func TestSession_SpecialKeys(t *testing.T) {
|
|
// Test that SendKey method accepts various keys
|
|
tmpDir := t.TempDir()
|
|
session := &Session{
|
|
ID: "test-keys",
|
|
controlPath: tmpDir,
|
|
info: &Info{},
|
|
stdinMutex: sync.Mutex{},
|
|
}
|
|
|
|
// Create stdin pipe
|
|
stdinPath := session.StdinPath()
|
|
os.MkdirAll(filepath.Dir(stdinPath), 0755)
|
|
stdinPipe, _ := os.Create(stdinPath)
|
|
session.stdinPipe = stdinPipe
|
|
defer stdinPipe.Close()
|
|
|
|
// Test various keys
|
|
keys := []string{"arrow_up", "arrow_down", "escape", "enter"}
|
|
for _, key := range keys {
|
|
err := session.SendKey(key)
|
|
if err == nil {
|
|
t.Logf("SendKey(%s) succeeded", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInfo_SaveLoad(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
infoPath := filepath.Join(tmpDir, "session.json")
|
|
|
|
// Create test info
|
|
info := &Info{
|
|
ID: "test-id",
|
|
Name: "test-session",
|
|
Cmdline: "bash",
|
|
Cwd: "/tmp",
|
|
Pid: 12345,
|
|
Status: "running",
|
|
StartedAt: time.Now(),
|
|
Term: "xterm",
|
|
Width: 80,
|
|
Height: 24,
|
|
Args: []string{"bash"},
|
|
IsSpawned: true,
|
|
}
|
|
|
|
// Save
|
|
if err := info.Save(tmpDir); err != nil {
|
|
t.Fatalf("Save() error = %v", err)
|
|
}
|
|
|
|
// Verify file exists
|
|
if _, err := os.Stat(infoPath); err != nil {
|
|
t.Fatalf("Info file not created: %v", err)
|
|
}
|
|
|
|
// Load
|
|
loaded, err := LoadInfo(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("LoadInfo() error = %v", err)
|
|
}
|
|
|
|
// Compare
|
|
if loaded.ID != info.ID {
|
|
t.Errorf("ID = %s, want %s", loaded.ID, info.ID)
|
|
}
|
|
if loaded.Name != info.Name {
|
|
t.Errorf("Name = %s, want %s", loaded.Name, info.Name)
|
|
}
|
|
if loaded.Pid != info.Pid {
|
|
t.Errorf("Pid = %d, want %d", loaded.Pid, info.Pid)
|
|
}
|
|
if loaded.Width != info.Width {
|
|
t.Errorf("Width = %d, want %d", loaded.Width, info.Width)
|
|
}
|
|
}
|