You've already forked agentic-coding-workflow
179 lines
4.4 KiB
Go
179 lines
4.4 KiB
Go
package terminal
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// testShell 返回各平台一个可交互读 stdin 的 shell。
|
|
func testShell() string {
|
|
if runtime.GOOS == "windows" {
|
|
return "cmd"
|
|
}
|
|
return "/bin/sh"
|
|
}
|
|
|
|
func newTestSup(t *testing.T, max int) *Supervisor {
|
|
t.Helper()
|
|
sup := NewSupervisor(SupervisorConfig{
|
|
Shell: testShell(),
|
|
MaxSessions: max,
|
|
KillGrace: 2 * time.Second,
|
|
EnvWhitelist: []string{
|
|
"PATH", "HOME", "TERM",
|
|
"SystemRoot", "TEMP", "TMP", "USERPROFILE", "ComSpec", "PATHEXT",
|
|
},
|
|
}, nil)
|
|
t.Cleanup(func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
sup.CloseAll(ctx)
|
|
})
|
|
return sup
|
|
}
|
|
|
|
// readUntil 在独立 goroutine 中持续读会话输出,直到累计内容包含 marker 或超时。
|
|
// 阻塞的 Read goroutine 会在会话被关闭(defer/Cleanup)后随 Read 报错而退出。
|
|
func readUntil(sess *Session, marker string, d time.Duration) string {
|
|
out := make(chan []byte, 16)
|
|
go func() {
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
n, err := sess.Read(buf)
|
|
if n > 0 {
|
|
b := make([]byte, n)
|
|
copy(b, buf[:n])
|
|
out <- b
|
|
}
|
|
if err != nil {
|
|
close(out)
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
deadline := time.After(d)
|
|
var acc strings.Builder
|
|
for {
|
|
select {
|
|
case b, ok := <-out:
|
|
if !ok {
|
|
return acc.String()
|
|
}
|
|
acc.Write(b)
|
|
if strings.Contains(acc.String(), marker) {
|
|
return acc.String()
|
|
}
|
|
case <-deadline:
|
|
return acc.String()
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestOpen_ReadsShellOutput 验证开会话后向 shell 写命令能读回其输出。
|
|
// Unix 走真 PTY;Windows 走降级管道实现。
|
|
func TestOpen_ReadsShellOutput(t *testing.T) {
|
|
sup := newTestSup(t, 5)
|
|
sess, err := sup.Open(OpenParams{})
|
|
if err != nil {
|
|
t.Fatalf("open: %v", err)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
defer sup.Close(ctx, sess.ID)
|
|
|
|
const marker = "ACWMARK42"
|
|
cmd := "echo " + marker + "\n"
|
|
if runtime.GOOS == "windows" {
|
|
cmd = "echo " + marker + "\r\n"
|
|
}
|
|
if _, err := sess.Write([]byte(cmd)); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
|
|
got := readUntil(sess, marker, 5*time.Second)
|
|
if !strings.Contains(got, marker) {
|
|
t.Fatalf("marker %q not found in shell output; got %q", marker, got)
|
|
}
|
|
}
|
|
|
|
// TestOpen_MaxSessions 验证达到上限后再开会话返回 ErrTooManySessions。
|
|
func TestOpen_MaxSessions(t *testing.T) {
|
|
sup := newTestSup(t, 1)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
s1, err := sup.Open(OpenParams{})
|
|
if err != nil {
|
|
t.Fatalf("first open: %v", err)
|
|
}
|
|
defer sup.Close(ctx, s1.ID)
|
|
|
|
if _, err := sup.Open(OpenParams{}); !errors.Is(err, ErrTooManySessions) {
|
|
t.Fatalf("expected ErrTooManySessions, got %v", err)
|
|
}
|
|
if got := sup.Count(); got != 1 {
|
|
t.Fatalf("expected 1 active session, got %d", got)
|
|
}
|
|
}
|
|
|
|
// TestClose_TerminatesSession 验证 Close 终止 shell(done 关闭)并从映射移除。
|
|
func TestClose_TerminatesSession(t *testing.T) {
|
|
sup := newTestSup(t, 5)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
sess, err := sup.Open(OpenParams{})
|
|
if err != nil {
|
|
t.Fatalf("open: %v", err)
|
|
}
|
|
if got := sup.Count(); got != 1 {
|
|
t.Fatalf("expected 1 active session, got %d", got)
|
|
}
|
|
|
|
sup.Close(ctx, sess.ID)
|
|
|
|
select {
|
|
case <-sess.Done():
|
|
case <-time.After(8 * time.Second):
|
|
t.Fatal("session not terminated within 8s after Close")
|
|
}
|
|
|
|
// monitor 在 done 关闭后异步移除映射,轮询直至归零。
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for sup.Count() != 0 {
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("session not removed from supervisor; count=%d", sup.Count())
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
// TestCloseAll_ClosesEverySession 验证 CloseAll 终止全部会话。
|
|
func TestCloseAll_ClosesEverySession(t *testing.T) {
|
|
sup := newTestSup(t, 5)
|
|
for i := 0; i < 3; i++ {
|
|
if _, err := sup.Open(OpenParams{}); err != nil {
|
|
t.Fatalf("open %d: %v", i, err)
|
|
}
|
|
}
|
|
if got := sup.Count(); got != 3 {
|
|
t.Fatalf("expected 3 active sessions, got %d", got)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
|
defer cancel()
|
|
sup.CloseAll(ctx)
|
|
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for sup.Count() != 0 {
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("sessions not all closed; count=%d", sup.Count())
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
}
|