mirror of
https://github.com/cexll/myclaude.git
synced 2026-02-05 02:30:26 +08:00
修复 PR #53 中发现的问题,实现完整的多后端功能: **多后端功能完整性** - Claude/Gemini 后端支持 workdir (-C) 和 resume (--session-id) 参数 - 并行模式支持全局 --backend 参数和任务级 backend 配置 - 后端参数映射统一,支持 new/resume 两种模式 **安全控制** - Claude 后端默认启用 --dangerously-skip-permissions 以支持自动化 - 通过 CODEAGENT_SKIP_PERMISSIONS 环境变量控制权限检查 - 不同后端行为区分:Claude 默认跳过,Codex/Gemini 默认启用 **并发控制** - 新增 CODEAGENT_MAX_PARALLEL_WORKERS 环境变量限制并发数 - 实现 fail-fast context 取消机制 - Worker pool 防止资源耗尽,支持并发监控日志 **向后兼容** - 版本号统一管理,提供 codex-wrapper 兼容脚本 - 所有默认行为保持不变 - 支持渐进式迁移 **测试覆盖** - 总体覆盖率 93.4%(超过 90% 要求) - 新增后端参数、并行模式、并发控制测试用例 - 核心模块覆盖率:backend.go 100%, config.go 97.8%, executor.go 96.4% **文档更新** - 更新 skills/codeagent/SKILL.md 反映多后端和安全控制 - 添加 CHANGELOG.md 记录重要变更 - 更新 README 版本说明和安装脚本 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
577 lines
18 KiB
Go
577 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type execFakeProcess struct {
|
|
pid int
|
|
signals []os.Signal
|
|
killed atomic.Int32
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func (p *execFakeProcess) Pid() int { return p.pid }
|
|
func (p *execFakeProcess) Kill() error {
|
|
p.killed.Add(1)
|
|
return nil
|
|
}
|
|
func (p *execFakeProcess) Signal(sig os.Signal) error {
|
|
p.mu.Lock()
|
|
p.signals = append(p.signals, sig)
|
|
p.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
type writeCloserStub struct {
|
|
bytes.Buffer
|
|
closed atomic.Bool
|
|
}
|
|
|
|
func (w *writeCloserStub) Close() error {
|
|
w.closed.Store(true)
|
|
return nil
|
|
}
|
|
|
|
type reasonReadCloser struct {
|
|
r io.Reader
|
|
closed []string
|
|
mu sync.Mutex
|
|
closedC chan struct{}
|
|
}
|
|
|
|
func newReasonReadCloser(data string) *reasonReadCloser {
|
|
return &reasonReadCloser{r: strings.NewReader(data), closedC: make(chan struct{}, 1)}
|
|
}
|
|
|
|
func (rc *reasonReadCloser) Read(p []byte) (int, error) { return rc.r.Read(p) }
|
|
func (rc *reasonReadCloser) Close() error { rc.record("close"); return nil }
|
|
func (rc *reasonReadCloser) CloseWithReason(reason string) error {
|
|
rc.record(reason)
|
|
return nil
|
|
}
|
|
|
|
func (rc *reasonReadCloser) record(reason string) {
|
|
rc.mu.Lock()
|
|
rc.closed = append(rc.closed, reason)
|
|
rc.mu.Unlock()
|
|
select {
|
|
case rc.closedC <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
type execFakeRunner struct {
|
|
stdout io.ReadCloser
|
|
process processHandle
|
|
stdin io.WriteCloser
|
|
waitErr error
|
|
waitDelay time.Duration
|
|
startErr error
|
|
stdoutErr error
|
|
stdinErr error
|
|
allowNilProcess bool
|
|
started atomic.Bool
|
|
}
|
|
|
|
func (f *execFakeRunner) Start() error {
|
|
if f.startErr != nil {
|
|
return f.startErr
|
|
}
|
|
f.started.Store(true)
|
|
return nil
|
|
}
|
|
func (f *execFakeRunner) Wait() error {
|
|
if f.waitDelay > 0 {
|
|
time.Sleep(f.waitDelay)
|
|
}
|
|
return f.waitErr
|
|
}
|
|
func (f *execFakeRunner) StdoutPipe() (io.ReadCloser, error) {
|
|
if f.stdoutErr != nil {
|
|
return nil, f.stdoutErr
|
|
}
|
|
if f.stdout == nil {
|
|
f.stdout = io.NopCloser(strings.NewReader(""))
|
|
}
|
|
return f.stdout, nil
|
|
}
|
|
func (f *execFakeRunner) StdinPipe() (io.WriteCloser, error) {
|
|
if f.stdinErr != nil {
|
|
return nil, f.stdinErr
|
|
}
|
|
if f.stdin != nil {
|
|
return f.stdin, nil
|
|
}
|
|
return &writeCloserStub{}, nil
|
|
}
|
|
func (f *execFakeRunner) SetStderr(io.Writer) {}
|
|
func (f *execFakeRunner) Process() processHandle {
|
|
if f.process != nil {
|
|
return f.process
|
|
}
|
|
if f.allowNilProcess {
|
|
return nil
|
|
}
|
|
return &execFakeProcess{pid: 1}
|
|
}
|
|
|
|
func TestExecutorHelperCoverage(t *testing.T) {
|
|
t.Run("realCmdAndProcess", func(t *testing.T) {
|
|
rc := &realCmd{}
|
|
if err := rc.Start(); err == nil {
|
|
t.Fatalf("expected error for nil command")
|
|
}
|
|
if err := rc.Wait(); err == nil {
|
|
t.Fatalf("expected error for nil command")
|
|
}
|
|
if _, err := rc.StdoutPipe(); err == nil {
|
|
t.Fatalf("expected error for nil command")
|
|
}
|
|
if _, err := rc.StdinPipe(); err == nil {
|
|
t.Fatalf("expected error for nil command")
|
|
}
|
|
rc.SetStderr(io.Discard)
|
|
if rc.Process() != nil {
|
|
t.Fatalf("expected nil process")
|
|
}
|
|
rcWithCmd := &realCmd{cmd: &exec.Cmd{}}
|
|
rcWithCmd.SetStderr(io.Discard)
|
|
echoCmd := exec.Command("echo", "ok")
|
|
rcProc := &realCmd{cmd: echoCmd}
|
|
stdoutPipe, err := rcProc.StdoutPipe()
|
|
if err != nil {
|
|
t.Fatalf("StdoutPipe error: %v", err)
|
|
}
|
|
stdinPipe, err := rcProc.StdinPipe()
|
|
if err != nil {
|
|
t.Fatalf("StdinPipe error: %v", err)
|
|
}
|
|
rcProc.SetStderr(io.Discard)
|
|
if err := rcProc.Start(); err != nil {
|
|
t.Fatalf("Start failed: %v", err)
|
|
}
|
|
_, _ = stdinPipe.Write([]byte{})
|
|
_ = stdinPipe.Close()
|
|
procHandle := rcProc.Process()
|
|
if procHandle == nil {
|
|
t.Fatalf("expected process handle")
|
|
}
|
|
_ = procHandle.Signal(syscall.SIGTERM)
|
|
_ = procHandle.Kill()
|
|
_ = rcProc.Wait()
|
|
_, _ = io.ReadAll(stdoutPipe)
|
|
|
|
rp := &realProcess{}
|
|
if rp.Pid() != 0 {
|
|
t.Fatalf("nil process should have pid 0")
|
|
}
|
|
if rp.Kill() != nil {
|
|
t.Fatalf("nil process Kill should be nil")
|
|
}
|
|
if rp.Signal(syscall.SIGTERM) != nil {
|
|
t.Fatalf("nil process Signal should be nil")
|
|
}
|
|
rpLive := &realProcess{proc: &os.Process{Pid: 99}}
|
|
if rpLive.Pid() != 99 {
|
|
t.Fatalf("expected pid 99, got %d", rpLive.Pid())
|
|
}
|
|
_ = rpLive.Kill()
|
|
_ = rpLive.Signal(syscall.SIGTERM)
|
|
})
|
|
|
|
t.Run("topologicalSortAndSkip", func(t *testing.T) {
|
|
layers, err := topologicalSort([]TaskSpec{{ID: "root"}, {ID: "child", Dependencies: []string{"root"}}})
|
|
if err != nil || len(layers) != 2 {
|
|
t.Fatalf("unexpected topological sort result: layers=%d err=%v", len(layers), err)
|
|
}
|
|
if _, err := topologicalSort([]TaskSpec{{ID: "cycle", Dependencies: []string{"cycle"}}}); err == nil {
|
|
t.Fatalf("expected cycle detection error")
|
|
}
|
|
|
|
failed := map[string]TaskResult{"root": {ExitCode: 1}}
|
|
if skip, _ := shouldSkipTask(TaskSpec{ID: "child", Dependencies: []string{"root"}}, failed); !skip {
|
|
t.Fatalf("should skip when dependency failed")
|
|
}
|
|
if skip, _ := shouldSkipTask(TaskSpec{ID: "leaf"}, failed); skip {
|
|
t.Fatalf("should not skip task without dependencies")
|
|
}
|
|
if skip, _ := shouldSkipTask(TaskSpec{ID: "child-ok", Dependencies: []string{"root"}}, map[string]TaskResult{}); skip {
|
|
t.Fatalf("should not skip when dependencies succeeded")
|
|
}
|
|
})
|
|
|
|
t.Run("cancelledTaskResult", func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
res := cancelledTaskResult("t1", ctx)
|
|
if res.ExitCode != 130 {
|
|
t.Fatalf("expected cancel exit code, got %d", res.ExitCode)
|
|
}
|
|
|
|
timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 0)
|
|
defer timeoutCancel()
|
|
res = cancelledTaskResult("t2", timeoutCtx)
|
|
if res.ExitCode != 124 {
|
|
t.Fatalf("expected timeout exit code, got %d", res.ExitCode)
|
|
}
|
|
})
|
|
|
|
t.Run("generateFinalOutputAndArgs", func(t *testing.T) {
|
|
out := generateFinalOutput([]TaskResult{
|
|
{TaskID: "ok", ExitCode: 0},
|
|
{TaskID: "fail", ExitCode: 1, Error: "boom"},
|
|
})
|
|
if !strings.Contains(out, "ok") || !strings.Contains(out, "fail") {
|
|
t.Fatalf("unexpected summary output: %s", out)
|
|
}
|
|
out = generateFinalOutput([]TaskResult{{TaskID: "rich", ExitCode: 0, SessionID: "sess", LogPath: "/tmp/log", Message: "hello"}})
|
|
if !strings.Contains(out, "Session: sess") || !strings.Contains(out, "Log: /tmp/log") || !strings.Contains(out, "hello") {
|
|
t.Fatalf("rich output missing fields: %s", out)
|
|
}
|
|
|
|
args := buildCodexArgs(&Config{Mode: "new", WorkDir: "/tmp"}, "task")
|
|
if len(args) == 0 || args[3] != "/tmp" {
|
|
t.Fatalf("unexpected codex args: %+v", args)
|
|
}
|
|
args = buildCodexArgs(&Config{Mode: "resume", SessionID: "sess"}, "target")
|
|
if args[3] != "resume" || args[4] != "sess" {
|
|
t.Fatalf("unexpected resume args: %+v", args)
|
|
}
|
|
})
|
|
|
|
t.Run("executeConcurrentWrapper", func(t *testing.T) {
|
|
orig := runCodexTaskFn
|
|
defer func() { runCodexTaskFn = orig }()
|
|
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
|
return TaskResult{TaskID: task.ID, ExitCode: 0, Message: "done"}
|
|
}
|
|
os.Setenv("CODEAGENT_MAX_PARALLEL_WORKERS", "1")
|
|
defer os.Unsetenv("CODEAGENT_MAX_PARALLEL_WORKERS")
|
|
|
|
results := executeConcurrent([][]TaskSpec{{{ID: "wrap"}}}, 1)
|
|
if len(results) != 1 || results[0].TaskID != "wrap" {
|
|
t.Fatalf("unexpected wrapper results: %+v", results)
|
|
}
|
|
|
|
unbounded := executeConcurrentWithContext(context.Background(), [][]TaskSpec{{{ID: "unbounded"}}}, 1, 0)
|
|
if len(unbounded) != 1 || unbounded[0].ExitCode != 0 {
|
|
t.Fatalf("unexpected unbounded result: %+v", unbounded)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
cancelled := executeConcurrentWithContext(ctx, [][]TaskSpec{{{ID: "cancel"}}}, 1, 1)
|
|
if cancelled[0].ExitCode == 0 {
|
|
t.Fatalf("expected cancelled result, got %+v", cancelled[0])
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestExecutorRunCodexTaskWithContext(t *testing.T) {
|
|
origRunner := newCommandRunner
|
|
defer func() { newCommandRunner = origRunner }()
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
var firstStdout *reasonReadCloser
|
|
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
|
rc := newReasonReadCloser(`{"type":"item.completed","item":{"type":"agent_message","text":"hello"}}`)
|
|
if firstStdout == nil {
|
|
firstStdout = rc
|
|
}
|
|
return &execFakeRunner{stdout: rc, process: &execFakeProcess{pid: 1234}}
|
|
}
|
|
|
|
res := runCodexTaskWithContext(context.Background(), TaskSpec{ID: "task-1", Task: "payload", WorkDir: "."}, nil, nil, false, false, 1)
|
|
if res.Error != "" || res.Message != "hello" || res.ExitCode != 0 {
|
|
t.Fatalf("unexpected result: %+v", res)
|
|
}
|
|
|
|
select {
|
|
case <-firstStdout.closedC:
|
|
case <-time.After(1 * time.Second):
|
|
t.Fatalf("stdout not closed with reason")
|
|
}
|
|
|
|
orig := runCodexTaskFn
|
|
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
|
return TaskResult{TaskID: task.ID, ExitCode: 0, Message: "ok"}
|
|
}
|
|
t.Cleanup(func() { runCodexTaskFn = orig })
|
|
|
|
if res := runCodexTask(TaskSpec{Task: "task-text", WorkDir: "."}, true, 1); res.ExitCode != 0 {
|
|
t.Fatalf("runCodexTask failed: %+v", res)
|
|
}
|
|
|
|
msg, threadID, code := runCodexProcess(context.Background(), []string{"arg"}, "content", false, 1)
|
|
if code != 0 || msg == "" {
|
|
t.Fatalf("runCodexProcess unexpected result: msg=%q code=%d threadID=%s", msg, code, threadID)
|
|
}
|
|
})
|
|
|
|
t.Run("startErrors", func(t *testing.T) {
|
|
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
|
return &execFakeRunner{startErr: errors.New("executable file not found"), process: &execFakeProcess{pid: 1}}
|
|
}
|
|
res := runCodexTaskWithContext(context.Background(), TaskSpec{Task: "payload", WorkDir: "."}, nil, nil, false, false, 1)
|
|
if res.ExitCode != 127 {
|
|
t.Fatalf("expected missing executable exit code, got %d", res.ExitCode)
|
|
}
|
|
|
|
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
|
return &execFakeRunner{startErr: errors.New("start failed"), process: &execFakeProcess{pid: 2}}
|
|
}
|
|
res = runCodexTaskWithContext(context.Background(), TaskSpec{Task: "payload", WorkDir: "."}, nil, nil, false, false, 1)
|
|
if res.ExitCode == 0 {
|
|
t.Fatalf("expected non-zero exit on start failure")
|
|
}
|
|
})
|
|
|
|
t.Run("timeoutAndPipes", func(t *testing.T) {
|
|
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
|
return &execFakeRunner{
|
|
stdout: newReasonReadCloser(`{"type":"item.completed","item":{"type":"agent_message","text":"slow"}}`),
|
|
process: &execFakeProcess{pid: 5},
|
|
waitDelay: 20 * time.Millisecond,
|
|
}
|
|
}
|
|
res := runCodexTaskWithContext(context.Background(), TaskSpec{Task: "payload", WorkDir: ".", UseStdin: true}, nil, nil, false, false, 0)
|
|
if res.ExitCode == 0 {
|
|
t.Fatalf("expected timeout result, got %+v", res)
|
|
}
|
|
})
|
|
|
|
t.Run("pipeErrors", func(t *testing.T) {
|
|
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
|
return &execFakeRunner{stdoutErr: errors.New("stdout fail"), process: &execFakeProcess{pid: 6}}
|
|
}
|
|
res := runCodexTaskWithContext(context.Background(), TaskSpec{Task: "payload", WorkDir: "."}, nil, nil, false, false, 1)
|
|
if res.ExitCode == 0 {
|
|
t.Fatalf("expected failure on stdout pipe error")
|
|
}
|
|
|
|
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
|
return &execFakeRunner{stdinErr: errors.New("stdin fail"), process: &execFakeProcess{pid: 7}}
|
|
}
|
|
res = runCodexTaskWithContext(context.Background(), TaskSpec{Task: "payload", WorkDir: ".", UseStdin: true}, nil, nil, false, false, 1)
|
|
if res.ExitCode == 0 {
|
|
t.Fatalf("expected failure on stdin pipe error")
|
|
}
|
|
})
|
|
|
|
t.Run("waitExitError", func(t *testing.T) {
|
|
err := exec.Command("false").Run()
|
|
exitErr, _ := err.(*exec.ExitError)
|
|
if exitErr == nil {
|
|
t.Fatalf("expected exec.ExitError")
|
|
}
|
|
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
|
return &execFakeRunner{
|
|
stdout: newReasonReadCloser(`{"type":"item.completed","item":{"type":"agent_message","text":"ignored"}}`),
|
|
process: &execFakeProcess{pid: 8},
|
|
waitErr: exitErr,
|
|
}
|
|
}
|
|
res := runCodexTaskWithContext(context.Background(), TaskSpec{Task: "payload", WorkDir: "."}, nil, nil, false, false, 1)
|
|
if res.ExitCode == 0 {
|
|
t.Fatalf("expected non-zero exit on wait error")
|
|
}
|
|
})
|
|
|
|
t.Run("contextCancelled", func(t *testing.T) {
|
|
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
|
return &execFakeRunner{
|
|
stdout: newReasonReadCloser(`{"type":"item.completed","item":{"type":"agent_message","text":"cancel"}}`),
|
|
process: &execFakeProcess{pid: 9},
|
|
waitDelay: 10 * time.Millisecond,
|
|
}
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
res := runCodexTaskWithContext(ctx, TaskSpec{Task: "payload", WorkDir: "."}, nil, nil, false, false, 1)
|
|
if res.ExitCode == 0 {
|
|
t.Fatalf("expected cancellation result")
|
|
}
|
|
})
|
|
|
|
t.Run("silentLogger", func(t *testing.T) {
|
|
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
|
return &execFakeRunner{
|
|
stdout: newReasonReadCloser(`{"type":"item.completed","item":{"type":"agent_message","text":"quiet"}}`),
|
|
process: &execFakeProcess{pid: 10},
|
|
}
|
|
}
|
|
_ = closeLogger()
|
|
res := runCodexTaskWithContext(context.Background(), TaskSpec{Task: "payload", WorkDir: "."}, nil, nil, false, true, 1)
|
|
if res.ExitCode != 0 || res.LogPath == "" {
|
|
t.Fatalf("expected success with temp logger, got %+v", res)
|
|
}
|
|
_ = closeLogger()
|
|
})
|
|
|
|
t.Run("missingMessage", func(t *testing.T) {
|
|
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
|
return &execFakeRunner{
|
|
stdout: newReasonReadCloser(`{"type":"item.completed","item":{"type":"task","text":"noop"}}`),
|
|
process: &execFakeProcess{pid: 11},
|
|
}
|
|
}
|
|
res := runCodexTaskWithContext(context.Background(), TaskSpec{Task: "payload", WorkDir: "."}, nil, nil, false, false, 1)
|
|
if res.ExitCode == 0 {
|
|
t.Fatalf("expected failure when no agent_message returned")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestExecutorSignalAndTermination(t *testing.T) {
|
|
forceKillDelay.Store(0)
|
|
defer forceKillDelay.Store(5)
|
|
|
|
proc := &execFakeProcess{pid: 42}
|
|
cmd := &execFakeRunner{process: proc}
|
|
|
|
origNotify := signalNotifyFn
|
|
origStop := signalStopFn
|
|
defer func() {
|
|
signalNotifyFn = origNotify
|
|
signalStopFn = origStop
|
|
}()
|
|
|
|
signalNotifyFn = func(c chan<- os.Signal, sigs ...os.Signal) {
|
|
go func() { c <- syscall.SIGINT }()
|
|
}
|
|
signalStopFn = func(c chan<- os.Signal) {}
|
|
|
|
forwardSignals(context.Background(), cmd, func(string) {})
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
proc.mu.Lock()
|
|
signalled := len(proc.signals)
|
|
proc.mu.Unlock()
|
|
if signalled == 0 {
|
|
t.Fatalf("process did not receive signal")
|
|
}
|
|
if proc.killed.Load() == 0 {
|
|
t.Fatalf("process was not killed after signal")
|
|
}
|
|
|
|
timer := terminateProcess(cmd)
|
|
if timer == nil {
|
|
t.Fatalf("terminateProcess returned nil timer")
|
|
}
|
|
timer.Stop()
|
|
|
|
ft := terminateCommand(cmd)
|
|
if ft == nil {
|
|
t.Fatalf("terminateCommand returned nil")
|
|
}
|
|
ft.Stop()
|
|
|
|
cmdKill := &execFakeRunner{process: &execFakeProcess{pid: 50}}
|
|
ftKill := terminateCommand(cmdKill)
|
|
time.Sleep(10 * time.Millisecond)
|
|
if p, ok := cmdKill.process.(*execFakeProcess); ok && p.killed.Load() == 0 {
|
|
t.Fatalf("terminateCommand did not kill process")
|
|
}
|
|
ftKill.Stop()
|
|
|
|
cmdKill2 := &execFakeRunner{process: &execFakeProcess{pid: 51}}
|
|
timer2 := terminateProcess(cmdKill2)
|
|
time.Sleep(10 * time.Millisecond)
|
|
if p, ok := cmdKill2.process.(*execFakeProcess); ok && p.killed.Load() == 0 {
|
|
t.Fatalf("terminateProcess did not kill process")
|
|
}
|
|
timer2.Stop()
|
|
|
|
if terminateCommand(nil) != nil {
|
|
t.Fatalf("terminateCommand should return nil for nil cmd")
|
|
}
|
|
if terminateCommand(&execFakeRunner{allowNilProcess: true}) != nil {
|
|
t.Fatalf("terminateCommand should return nil when process is nil")
|
|
}
|
|
if terminateProcess(nil) != nil {
|
|
t.Fatalf("terminateProcess should return nil for nil cmd")
|
|
}
|
|
if terminateProcess(&execFakeRunner{allowNilProcess: true}) != nil {
|
|
t.Fatalf("terminateProcess should return nil when process is nil")
|
|
}
|
|
|
|
signalNotifyFn = func(c chan<- os.Signal, sigs ...os.Signal) {}
|
|
ctxDone, cancelDone := context.WithCancel(context.Background())
|
|
cancelDone()
|
|
forwardSignals(ctxDone, &execFakeRunner{process: &execFakeProcess{pid: 70}}, func(string) {})
|
|
}
|
|
|
|
func TestExecutorCancelReasonAndCloseWithReason(t *testing.T) {
|
|
if reason := cancelReason("", nil); !strings.Contains(reason, "Context") {
|
|
t.Fatalf("unexpected cancelReason for nil ctx: %s", reason)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 0)
|
|
defer cancel()
|
|
if !strings.Contains(cancelReason("cmd", ctx), "timeout") {
|
|
t.Fatalf("expected timeout reason")
|
|
}
|
|
cancelCtx, cancelFn := context.WithCancel(context.Background())
|
|
cancelFn()
|
|
if !strings.Contains(cancelReason("cmd", cancelCtx), "Execution cancelled") {
|
|
t.Fatalf("expected cancellation reason")
|
|
}
|
|
if !strings.Contains(cancelReason("", cancelCtx), "codex") {
|
|
t.Fatalf("expected default command name in cancel reason")
|
|
}
|
|
|
|
rc := &reasonReadCloser{r: strings.NewReader("data"), closedC: make(chan struct{}, 1)}
|
|
closeWithReason(rc, "why")
|
|
select {
|
|
case <-rc.closedC:
|
|
default:
|
|
t.Fatalf("CloseWithReason was not called")
|
|
}
|
|
|
|
plain := io.NopCloser(strings.NewReader("x"))
|
|
closeWithReason(plain, "noop")
|
|
closeWithReason(nil, "noop")
|
|
}
|
|
|
|
func TestExecutorForceKillTimerStop(t *testing.T) {
|
|
done := make(chan struct{}, 1)
|
|
ft := &forceKillTimer{timer: time.AfterFunc(50*time.Millisecond, func() { done <- struct{}{} }), done: done}
|
|
ft.Stop()
|
|
|
|
done2 := make(chan struct{}, 1)
|
|
ft2 := &forceKillTimer{timer: time.AfterFunc(0, func() { done2 <- struct{}{} }), done: done2}
|
|
time.Sleep(10 * time.Millisecond)
|
|
ft2.Stop()
|
|
|
|
var nilTimer *forceKillTimer
|
|
nilTimer.Stop()
|
|
(&forceKillTimer{}).Stop()
|
|
}
|
|
|
|
func TestExecutorForwardSignalsDefaults(t *testing.T) {
|
|
origNotify := signalNotifyFn
|
|
origStop := signalStopFn
|
|
signalNotifyFn = nil
|
|
signalStopFn = nil
|
|
defer func() {
|
|
signalNotifyFn = origNotify
|
|
signalStopFn = origStop
|
|
}()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
forwardSignals(ctx, &execFakeRunner{process: &execFakeProcess{pid: 80}}, func(string) {})
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|