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) } }