opencode-cursor-agent/internal/process/process_test.go

284 lines
7.0 KiB
Go
Raw Normal View History

2026-03-30 14:09:15 +00:00
package process_test
import (
"context"
"cursor-api-proxy/internal/process"
"os"
"testing"
"time"
)
// sh 是跨平台 shell 執行小 script 的輔助函式
func sh(t *testing.T, script string, opts process.RunOptions) (process.RunResult, error) {
t.Helper()
return process.Run("sh", []string{"-c", script}, opts)
}
func TestRun_StdoutAndStderr(t *testing.T) {
result, err := sh(t, "echo hello; echo world >&2", process.RunOptions{})
if err != nil {
t.Fatal(err)
}
if result.Code != 0 {
t.Errorf("Code = %d, want 0", result.Code)
}
if result.Stdout != "hello\n" {
t.Errorf("Stdout = %q, want %q", result.Stdout, "hello\n")
}
if result.Stderr != "world\n" {
t.Errorf("Stderr = %q, want %q", result.Stderr, "world\n")
}
}
func TestRun_BasicSpawn(t *testing.T) {
result, err := sh(t, "printf ok", process.RunOptions{})
if err != nil {
t.Fatal(err)
}
if result.Code != 0 {
t.Errorf("Code = %d, want 0", result.Code)
}
if result.Stdout != "ok" {
t.Errorf("Stdout = %q, want ok", result.Stdout)
}
}
func TestRun_ConfigDir_Propagated(t *testing.T) {
result, err := process.Run("sh", []string{"-c", `printf "$CURSOR_CONFIG_DIR"`},
process.RunOptions{ConfigDir: "/test/account/dir"})
if err != nil {
t.Fatal(err)
}
if result.Stdout != "/test/account/dir" {
t.Errorf("Stdout = %q, want /test/account/dir", result.Stdout)
}
}
func TestRun_ConfigDir_Absent(t *testing.T) {
// 確保沒有殘留的環境變數
_ = os.Unsetenv("CURSOR_CONFIG_DIR")
result, err := process.Run("sh", []string{"-c", `printf "${CURSOR_CONFIG_DIR:-unset}"`},
process.RunOptions{})
if err != nil {
t.Fatal(err)
}
if result.Stdout != "unset" {
t.Errorf("Stdout = %q, want unset", result.Stdout)
}
}
func TestRun_NonZeroExit(t *testing.T) {
result, err := sh(t, "exit 42", process.RunOptions{})
if err != nil {
t.Fatal(err)
}
if result.Code != 42 {
t.Errorf("Code = %d, want 42", result.Code)
}
}
func TestRun_Timeout(t *testing.T) {
start := time.Now()
result, err := sh(t, "sleep 30", process.RunOptions{TimeoutMs: 300})
elapsed := time.Since(start)
if err != nil {
t.Fatal(err)
}
if result.Code == 0 {
t.Error("expected non-zero exit code after timeout")
}
if elapsed > 2*time.Second {
t.Errorf("elapsed %v, want < 2s", elapsed)
}
}
func TestRunStreaming_OnLine(t *testing.T) {
var lines []string
result, err := process.RunStreaming("sh", []string{"-c", "printf 'a\nb\nc\n'"},
process.RunStreamingOptions{
OnLine: func(line string) { lines = append(lines, line) },
})
if err != nil {
t.Fatal(err)
}
if result.Code != 0 {
t.Errorf("Code = %d, want 0", result.Code)
}
if len(lines) != 3 {
t.Errorf("got %d lines, want 3: %v", len(lines), lines)
}
if lines[0] != "a" || lines[1] != "b" || lines[2] != "c" {
t.Errorf("lines = %v, want [a b c]", lines)
}
}
func TestRunStreaming_FlushFinalLine(t *testing.T) {
var lines []string
result, err := process.RunStreaming("sh", []string{"-c", "printf tail"},
process.RunStreamingOptions{
OnLine: func(line string) { lines = append(lines, line) },
})
if err != nil {
t.Fatal(err)
}
if result.Code != 0 {
t.Errorf("Code = %d, want 0", result.Code)
}
if len(lines) != 1 {
t.Errorf("got %d lines, want 1: %v", len(lines), lines)
}
if lines[0] != "tail" {
t.Errorf("lines[0] = %q, want tail", lines[0])
}
}
func TestRunStreaming_ConfigDir(t *testing.T) {
var lines []string
_, err := process.RunStreaming("sh", []string{"-c", `printf "$CURSOR_CONFIG_DIR"`},
process.RunStreamingOptions{
RunOptions: process.RunOptions{ConfigDir: "/my/config/dir"},
OnLine: func(line string) { lines = append(lines, line) },
})
if err != nil {
t.Fatal(err)
}
if len(lines) != 1 || lines[0] != "/my/config/dir" {
t.Errorf("lines = %v, want [/my/config/dir]", lines)
}
}
func TestRunStreaming_Stderr(t *testing.T) {
result, err := process.RunStreaming("sh", []string{"-c", "echo err-output >&2"},
process.RunStreamingOptions{OnLine: func(string) {}})
if err != nil {
t.Fatal(err)
}
if result.Stderr == "" {
t.Error("expected stderr to contain output")
}
}
func TestRunStreaming_Timeout(t *testing.T) {
start := time.Now()
result, err := process.RunStreaming("sh", []string{"-c", "sleep 30"},
process.RunStreamingOptions{
RunOptions: process.RunOptions{TimeoutMs: 300},
OnLine: func(string) {},
})
elapsed := time.Since(start)
if err != nil {
t.Fatal(err)
}
if result.Code == 0 {
t.Error("expected non-zero exit code after timeout")
}
if elapsed > 2*time.Second {
t.Errorf("elapsed %v, want < 2s", elapsed)
}
}
func TestRunStreaming_Concurrent(t *testing.T) {
var lines1, lines2 []string
done := make(chan struct{}, 2)
run := func(label string, target *[]string) {
process.RunStreaming("sh", []string{"-c", "printf '" + label + "'"},
process.RunStreamingOptions{
OnLine: func(line string) { *target = append(*target, line) },
})
done <- struct{}{}
}
go run("stream1", &lines1)
go run("stream2", &lines2)
<-done
<-done
if len(lines1) != 1 || lines1[0] != "stream1" {
t.Errorf("lines1 = %v, want [stream1]", lines1)
}
if len(lines2) != 1 || lines2[0] != "stream2" {
t.Errorf("lines2 = %v, want [stream2]", lines2)
}
}
func TestRunStreaming_ContextCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
start := time.Now()
done := make(chan struct{})
go func() {
process.RunStreaming("sh", []string{"-c", "sleep 30"},
process.RunStreamingOptions{
RunOptions: process.RunOptions{Ctx: ctx},
OnLine: func(string) {},
})
close(done)
}()
time.AfterFunc(100*time.Millisecond, cancel)
<-done
elapsed := time.Since(start)
if elapsed > 2*time.Second {
t.Errorf("elapsed %v, want < 2s", elapsed)
}
}
func TestRun_ContextCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
start := time.Now()
done := make(chan process.RunResult, 1)
go func() {
r, _ := process.Run("sh", []string{"-c", "sleep 30"}, process.RunOptions{Ctx: ctx})
done <- r
}()
time.AfterFunc(100*time.Millisecond, cancel)
result := <-done
elapsed := time.Since(start)
if result.Code == 0 {
t.Error("expected non-zero exit code after cancel")
}
if elapsed > 2*time.Second {
t.Errorf("elapsed %v, want < 2s", elapsed)
}
}
func TestRun_AlreadyCancelledContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // 已取消
start := time.Now()
result, _ := process.Run("sh", []string{"-c", "sleep 30"}, process.RunOptions{Ctx: ctx})
elapsed := time.Since(start)
if result.Code == 0 {
t.Error("expected non-zero exit code")
}
if elapsed > 2*time.Second {
t.Errorf("elapsed %v, want < 2s", elapsed)
}
}
func TestKillAllChildProcesses(t *testing.T) {
done := make(chan process.RunResult, 1)
go func() {
r, _ := process.Run("sh", []string{"-c", "sleep 30"}, process.RunOptions{})
done <- r
}()
time.Sleep(80 * time.Millisecond)
process.KillAllChildProcesses()
result := <-done
if result.Code == 0 {
t.Error("expected non-zero exit code after kill")
}
// 再次呼叫不應 panic
process.KillAllChildProcesses()
}