Files
kandev/apps/backend/internal/agentctl/server/process/runner_lifecycle_test.go
T
2026-02-18 18:24:24 +00:00

212 lines
5.0 KiB
Go

package process
import (
"context"
"testing"
"time"
"github.com/kandev/kandev/internal/agentctl/types"
)
func TestInteractiveRunner_Start(t *testing.T) {
log := newTestLogger(t)
runner := NewInteractiveRunner(nil, log, 2*1024*1024)
req := InteractiveStartRequest{
SessionID: "test-session",
Command: []string{"echo", "hello"},
ImmediateStart: true,
DefaultCols: 80,
DefaultRows: 24,
}
info, err := runner.Start(context.Background(), req)
if err != nil {
t.Fatalf("Start() error = %v", err)
}
if info.ID == "" {
t.Error("Start() returned empty ID")
}
if info.SessionID != "test-session" {
t.Errorf("Start() SessionID = %q, want %q", info.SessionID, "test-session")
}
if info.Status != types.ProcessStatusRunning {
t.Errorf("Start() Status = %v, want %v", info.Status, types.ProcessStatusRunning)
}
// Wait for process to exit
time.Sleep(500 * time.Millisecond)
// Process should have completed
procInfo, ok := runner.Get(info.ID, false)
if !ok {
// Process may have been removed after exit, which is expected
return
}
if procInfo.Status == types.ProcessStatusRunning {
t.Error("Process should have exited")
}
}
func TestInteractiveRunner_Start_ValidationErrors(t *testing.T) {
log := newTestLogger(t)
runner := NewInteractiveRunner(nil, log, 2*1024*1024)
tests := []struct {
name string
req InteractiveStartRequest
wantErr bool
}{
{
name: "missing session_id",
req: InteractiveStartRequest{
Command: []string{"echo"},
},
wantErr: true,
},
{
name: "missing command",
req: InteractiveStartRequest{
SessionID: "test",
},
wantErr: true,
},
{
name: "valid request",
req: InteractiveStartRequest{
SessionID: "test",
Command: []string{"echo"},
ImmediateStart: true,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := runner.Start(context.Background(), tt.req)
if (err != nil) != tt.wantErr {
t.Errorf("Start() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestInteractiveRunner_DeferredStart(t *testing.T) {
log := newTestLogger(t)
runner := NewInteractiveRunner(nil, log, 2*1024*1024)
// Start without ImmediateStart - process should be deferred
// Use 'cat' which blocks waiting for input, giving us time to check status
req := InteractiveStartRequest{
SessionID: "deferred-session",
Command: []string{"cat"},
}
info, err := runner.Start(context.Background(), req)
if err != nil {
t.Fatalf("Start() error = %v", err)
}
// Try to write - should fail because process not started
err = runner.WriteStdin(info.ID, "test")
if err == nil {
t.Error("WriteStdin() should fail for deferred process")
}
// Trigger start via resize
err = runner.ResizeBySession("deferred-session", 80, 24)
if err != nil {
t.Fatalf("ResizeBySession() error = %v", err)
}
// Give process time to start
time.Sleep(100 * time.Millisecond)
// Now get process info - process should exist and be running
procInfo, ok := runner.GetBySession("deferred-session")
if !ok {
t.Fatal("GetBySession() should find process after resize")
}
if procInfo.Status != types.ProcessStatusRunning {
t.Errorf("Process status = %v, want running", procInfo.Status)
}
// Clean up - stop the process
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_ = runner.Stop(ctx, info.ID)
}
func TestInteractiveRunner_WriteStdin(t *testing.T) {
log := newTestLogger(t)
runner := NewInteractiveRunner(nil, log, 2*1024*1024)
// Start cat process that echoes input
req := InteractiveStartRequest{
SessionID: "stdin-test",
Command: []string{"cat"},
ImmediateStart: true,
DefaultCols: 80,
DefaultRows: 24,
}
info, err := runner.Start(context.Background(), req)
if err != nil {
t.Fatalf("Start() error = %v", err)
}
// Give process time to start
time.Sleep(100 * time.Millisecond)
// Write to stdin
err = runner.WriteStdin(info.ID, "hello\n")
if err != nil {
t.Errorf("WriteStdin() error = %v", err)
}
// Stop the process
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_ = runner.Stop(ctx, info.ID)
}
func TestInteractiveRunner_Stop(t *testing.T) {
log := newTestLogger(t)
runner := NewInteractiveRunner(nil, log, 2*1024*1024)
// Start a long-running process
req := InteractiveStartRequest{
SessionID: "stop-test",
Command: []string{"sleep", "60"},
ImmediateStart: true,
DefaultCols: 80,
DefaultRows: 24,
}
info, err := runner.Start(context.Background(), req)
if err != nil {
t.Fatalf("Start() error = %v", err)
}
// Give process time to start
time.Sleep(100 * time.Millisecond)
// Stop the process
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = runner.Stop(ctx, info.ID)
if err != nil {
t.Errorf("Stop() error = %v", err)
}
// Process should be removed after stop
time.Sleep(200 * time.Millisecond)
_, ok := runner.Get(info.ID, false)
if ok {
t.Error("Process should be removed after stop")
}
}