You've already forked agentic-coding-workflow
144 lines
4.5 KiB
Go
144 lines
4.5 KiB
Go
package proc
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// helperShell 返回执行一段 shell 脚本的跨平台命令前缀(command, args...)。
|
|
func helperShell(script string) (string, []string) {
|
|
if runtime.GOOS == "windows" {
|
|
return "cmd", []string{"/c", script}
|
|
}
|
|
return "sh", []string{"-c", script}
|
|
}
|
|
|
|
// TestRunOnce_ExitCodeAndCapture 验证捕获 stdout / stderr 与非零退出码。
|
|
func TestRunOnce_ExitCodeAndCapture(t *testing.T) {
|
|
var script string
|
|
if runtime.GOOS == "windows" {
|
|
script = "echo out& echo err 1>&2& exit 3"
|
|
} else {
|
|
script = "echo out; echo err 1>&2; exit 3"
|
|
}
|
|
cmd, args := helperShell(script)
|
|
res, err := RunOnce(context.Background(), ExecSpec{Command: cmd, Args: args, Timeout: 10 * time.Second})
|
|
if err != nil {
|
|
t.Fatalf("RunOnce: %v", err)
|
|
}
|
|
if res.ExitCode != 3 {
|
|
t.Fatalf("exit code = %d, want 3", res.ExitCode)
|
|
}
|
|
if !strings.Contains(res.Stdout, "out") {
|
|
t.Fatalf("stdout = %q, want contains 'out'", res.Stdout)
|
|
}
|
|
if !strings.Contains(res.Stderr, "err") {
|
|
t.Fatalf("stderr = %q, want contains 'err'", res.Stderr)
|
|
}
|
|
if res.TimedOut {
|
|
t.Fatal("TimedOut should be false")
|
|
}
|
|
}
|
|
|
|
// TestRunOnce_Success 验证零退出码路径。
|
|
func TestRunOnce_Success(t *testing.T) {
|
|
cmd, args := helperShell("echo hello")
|
|
res, err := RunOnce(context.Background(), ExecSpec{Command: cmd, Args: args, Timeout: 10 * time.Second})
|
|
if err != nil {
|
|
t.Fatalf("RunOnce: %v", err)
|
|
}
|
|
if res.ExitCode != 0 {
|
|
t.Fatalf("exit code = %d, want 0", res.ExitCode)
|
|
}
|
|
if !strings.Contains(res.Stdout, "hello") {
|
|
t.Fatalf("stdout = %q", res.Stdout)
|
|
}
|
|
}
|
|
|
|
// TestRunOnce_Truncation 验证 stdout 超过 MaxOutputBytes 时截断并置标志。
|
|
func TestRunOnce_Truncation(t *testing.T) {
|
|
// 打印约 1000 字节,但上限设为 100。
|
|
var cmd string
|
|
var args []string
|
|
if runtime.GOOS == "windows" {
|
|
// PowerShell 直接调用避免 cmd 转义;-NoProfile 加快启动。
|
|
cmd, args = "powershell", []string{"-NoProfile", "-Command", "Write-Output ('a' * 1000)"}
|
|
} else {
|
|
cmd, args = "sh", []string{"-c", "for i in $(seq 1 1000); do printf a; done"}
|
|
}
|
|
res, err := RunOnce(context.Background(), ExecSpec{Command: cmd, Args: args, MaxOutputBytes: 100, Timeout: 15 * time.Second})
|
|
if err != nil {
|
|
t.Fatalf("RunOnce: %v", err)
|
|
}
|
|
if !res.Truncated {
|
|
t.Fatalf("Truncated should be true; stdout len=%d size=%d", len(res.Stdout), res.StdoutSize)
|
|
}
|
|
if len(res.Stdout) > 100 {
|
|
t.Fatalf("captured stdout len=%d exceeds limit 100", len(res.Stdout))
|
|
}
|
|
}
|
|
|
|
// TestRunOnce_Timeout 验证超时触发整树终止并置 TimedOut。
|
|
//
|
|
// Unix:父 shell trap 掉 TERM 并 sleep,再后台派生一个 grandchild sleep;超时后
|
|
// TerminateGroup 必须 SIGKILL 整组,确保父退出(done 被关闭)。
|
|
// Windows:直接 timeout 长睡,Job-close 终止。
|
|
func TestRunOnce_Timeout(t *testing.T) {
|
|
var cmd string
|
|
var args []string
|
|
if runtime.GOOS == "windows" {
|
|
// ping -n N localhost 是无需控制台的可靠延时(timeout /t 在无 stdin 下立即返回)。
|
|
cmd, args = "cmd", []string{"/c", "ping -n 30 127.0.0.1 >nul"}
|
|
} else {
|
|
// trap '' TERM 忽略 SIGTERM;后台派生 grandchild;wait 阻塞存活。
|
|
cmd, args = "sh", []string{"-c", "trap '' TERM; sleep 30 & sleep 30; wait"}
|
|
}
|
|
start := time.Now()
|
|
res, err := RunOnce(context.Background(), ExecSpec{
|
|
Command: cmd,
|
|
Args: args,
|
|
Timeout: 1 * time.Second,
|
|
KillGrace: 500 * time.Millisecond,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("RunOnce: %v", err)
|
|
}
|
|
if !res.TimedOut {
|
|
t.Fatal("TimedOut should be true")
|
|
}
|
|
// 必须在远小于 30s 内返回(超时 + grace + kill),证明整树被回收而非等自然退出。
|
|
if elapsed := time.Since(start); elapsed > 10*time.Second {
|
|
t.Fatalf("RunOnce blocked %v, expected prompt tree-kill", elapsed)
|
|
}
|
|
}
|
|
|
|
// TestRunOnce_CtxCancel 验证 ctx 取消亦触发树终止并尽快返回。
|
|
func TestRunOnce_CtxCancel(t *testing.T) {
|
|
cmd, args := helperShell(fmt.Sprintf("%s", sleepScript(30)))
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
go func() {
|
|
time.Sleep(500 * time.Millisecond)
|
|
cancel()
|
|
}()
|
|
start := time.Now()
|
|
res, err := RunOnce(ctx, ExecSpec{Command: cmd, Args: args, KillGrace: 500 * time.Millisecond})
|
|
if err != nil {
|
|
t.Fatalf("RunOnce: %v", err)
|
|
}
|
|
if elapsed := time.Since(start); elapsed > 10*time.Second {
|
|
t.Fatalf("RunOnce blocked %v after ctx cancel", elapsed)
|
|
}
|
|
_ = res
|
|
}
|
|
|
|
func sleepScript(sec int) string {
|
|
if runtime.GOOS == "windows" {
|
|
return fmt.Sprintf("ping -n %d 127.0.0.1 >nul", sec)
|
|
}
|
|
return fmt.Sprintf("sleep %d", sec)
|
|
}
|