Files
kandev/apps/backend/internal/agent/loginpty/manager_test.go
T

160 lines
3.9 KiB
Go

package loginpty
import (
"bytes"
"errors"
"sync"
"testing"
"time"
"github.com/kandev/kandev/internal/common/logger"
)
func newTestManager(t *testing.T, onExit func(string, int, error)) *Manager {
t.Helper()
log, err := logger.NewLogger(logger.LoggingConfig{Level: "error", Format: "json"})
if err != nil {
t.Fatalf("logger: %v", err)
}
return NewManager(log, onExit)
}
// drainOutput consumes the subscriber channel until it closes; returns all bytes received.
func drainOutput(sess *Session) []byte {
ch := make(chan []byte, 32)
sess.Subscribe(ch)
defer sess.Unsubscribe(ch)
var buf bytes.Buffer
deadline := time.After(2 * time.Second)
for {
select {
case data, ok := <-ch:
if !ok {
return buf.Bytes()
}
buf.Write(data)
case <-deadline:
return buf.Bytes()
}
}
}
func TestStart_RunsCommandAndExits(t *testing.T) {
exited := make(chan struct {
agent string
code int
}, 1)
mgr := newTestManager(t, func(agent string, code int, _ error) {
exited <- struct {
agent string
code int
}{agent, code}
})
sess, err := mgr.Start("test-agent", []string{"sh", "-c", "echo hello && exit 0"}, 80, 24)
if err != nil {
t.Fatalf("Start: %v", err)
}
if sess.ID == "" {
t.Fatal("expected session id")
}
// Subscribe right away to catch the output (some may be missed if we race
// the readLoop; BufferedOutput() compensates after exit).
got := drainOutput(sess)
select {
case info := <-exited:
if info.agent != "test-agent" {
t.Errorf("agent = %s", info.agent)
}
if info.code != 0 {
t.Errorf("code = %d, want 0", info.code)
}
case <-time.After(3 * time.Second):
t.Fatal("supervise loop did not fire onExit")
}
// Output may have been received via the subscriber or buffered; check both.
combined := append([]byte(nil), got...)
combined = append(combined, sess.BufferedOutput()...)
if !bytes.Contains(combined, []byte("hello")) {
t.Errorf("expected 'hello' in output, got %q", combined)
}
}
func TestStart_IdempotentPerAgent(t *testing.T) {
mgr := newTestManager(t, nil)
first, err := mgr.Start("test-agent", []string{"sh", "-c", "sleep 5"}, 80, 24)
if err != nil {
t.Fatalf("first start: %v", err)
}
t.Cleanup(func() { _ = mgr.Stop("test-agent") })
second, err := mgr.Start("test-agent", []string{"sh", "-c", "sleep 5"}, 80, 24)
if !errors.Is(err, ErrSessionAlreadyRunning) {
t.Fatalf("err = %v, want ErrSessionAlreadyRunning", err)
}
if first.ID != second.ID {
t.Errorf("expected same session id, got %s and %s", first.ID, second.ID)
}
}
func TestStart_EmptyCommand(t *testing.T) {
mgr := newTestManager(t, nil)
_, err := mgr.Start("test-agent", nil, 80, 24)
if err == nil {
t.Fatal("expected error for empty command")
}
}
func TestStop_TerminatesRunningSession(t *testing.T) {
var wg sync.WaitGroup
wg.Add(1)
mgr := newTestManager(t, func(_ string, _ int, _ error) {
wg.Done()
})
_, err := mgr.Start("test-agent", []string{"sh", "-c", "sleep 30"}, 80, 24)
if err != nil {
t.Fatalf("Start: %v", err)
}
if err := mgr.Stop("test-agent"); err != nil {
t.Fatalf("Stop: %v", err)
}
done := make(chan struct{})
go func() { wg.Wait(); close(done) }()
select {
case <-done:
case <-time.After(3 * time.Second):
t.Fatal("session did not exit after Stop")
}
}
func TestWriteAndResize_WhenNotRunning(t *testing.T) {
mgr := newTestManager(t, nil)
sess, err := mgr.Start("test-agent", []string{"true"}, 80, 24)
if err != nil {
t.Fatalf("Start: %v", err)
}
// Wait for exit.
deadline := time.After(2 * time.Second)
for sess.Status().Running {
select {
case <-deadline:
t.Fatal("session never exited")
case <-time.After(20 * time.Millisecond):
}
}
if _, err := sess.Write([]byte("x")); !errors.Is(err, ErrSessionNotRunning) {
t.Errorf("Write err = %v, want ErrSessionNotRunning", err)
}
if err := sess.Resize(120, 40); !errors.Is(err, ErrSessionNotRunning) {
t.Errorf("Resize err = %v, want ErrSessionNotRunning", err)
}
}