You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user