160 lines
3.9 KiB
Go
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)
|
|
}
|
|
}
|