internal/opencode: runner (idle/hard timeout, process-group kill, resume-fallback) + verdict/json parsing
All checks were successful
build-test / build (push) Successful in 1m21s
All checks were successful
build-test / build (push) Successful in 1m21s
This commit is contained in:
131
internal/opencode/runner_test.go
Normal file
131
internal/opencode/runner_test.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package opencode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// fakeOpenCode создаёт shell-скрипт, имитирующий opencode run:
|
||||
// $FAKE_MODE=ok -> мгновенный успех, печатает NDJSON c session_id
|
||||
// $FAKE_MODE=slow-> спит долго (для idle/hard timeout)
|
||||
// $FAKE_MODE=fail-> exit 7 (resume-fallback)
|
||||
func fakeOpenCode(t *testing.T, workdir string) string {
|
||||
t.Helper()
|
||||
bin := filepath.Join(workdir, "opencode")
|
||||
script := `#!/bin/sh
|
||||
mode="${FAKE_MODE:-ok}"
|
||||
case "$mode" in
|
||||
ok)
|
||||
echo '{"type":"text","part":{"text":"done"}}'
|
||||
echo '{"session_id":"sess-123"}'
|
||||
exit 0
|
||||
;;
|
||||
slow)
|
||||
sleep 30
|
||||
;;
|
||||
fail)
|
||||
echo '{"type":"text","part":{"text":"boom"}}'
|
||||
exit 7
|
||||
;;
|
||||
esac
|
||||
`
|
||||
if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
|
||||
t.Fatalf("write fake opencode: %v", err)
|
||||
}
|
||||
return bin
|
||||
}
|
||||
|
||||
func TestRun_Success(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
bin := fakeOpenCode(t, dir)
|
||||
t.Setenv("FAKE_MODE", "ok")
|
||||
|
||||
r := &Runner{Bin: bin, PollInterval: 20 * time.Millisecond}
|
||||
res, err := r.Run(context.Background(), "task", dir, "dev", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Run err: %v", err)
|
||||
}
|
||||
if res.RC != 0 {
|
||||
t.Errorf("RC = %d, want 0", res.RC)
|
||||
}
|
||||
if res.SessionID != "sess-123" {
|
||||
t.Errorf("SessionID = %q, want sess-123", res.SessionID)
|
||||
}
|
||||
if !contains(res.Stdout, "done") {
|
||||
t.Errorf("Stdout = %q, want to contain done", res.Stdout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_IdleTimeout(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
bin := fakeOpenCode(t, dir)
|
||||
t.Setenv("FAKE_MODE", "slow")
|
||||
|
||||
r := &Runner{Bin: bin, IdleTimeout: 50 * time.Millisecond,
|
||||
PollInterval: 10 * time.Millisecond}
|
||||
res, err := r.Run(context.Background(), "task", dir, "dev", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Run err: %v", err)
|
||||
}
|
||||
if res.RC != -1 {
|
||||
t.Errorf("RC = %d, want -1 (timeout kill)", res.RC)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_ContextCancel(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
bin := fakeOpenCode(t, dir)
|
||||
t.Setenv("FAKE_MODE", "slow")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
r := &Runner{Bin: bin, HardTimeout: time.Minute,
|
||||
PollInterval: 10 * time.Millisecond}
|
||||
done := make(chan *Result, 1)
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
res, err := r.Run(ctx, "task", dir, "dev", "")
|
||||
done <- res
|
||||
errCh <- err
|
||||
}()
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
cancel()
|
||||
res := <-done
|
||||
if err := <-errCh; err != nil {
|
||||
t.Fatalf("Run err: %v", err)
|
||||
}
|
||||
if res.RC != -1 {
|
||||
t.Errorf("RC = %d, want -1", res.RC)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResumeDev_Fallback(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
bin := fakeOpenCode(t, dir)
|
||||
t.Setenv("FAKE_MODE", "fail")
|
||||
|
||||
r := &Runner{Bin: bin, PollInterval: 20 * time.Millisecond}
|
||||
res, timedOut := r.ResumeDev(context.Background(), "task", dir, "lost-session")
|
||||
if timedOut {
|
||||
t.Error("timedOut = true, want false")
|
||||
}
|
||||
// fake fail всегда exit 7, fallback тоже 7 — проверяем что RC от fallback-вызова
|
||||
if res.RC != 7 {
|
||||
t.Errorf("RC = %d, want 7 (fallback повтор с тем же кодом)", res.RC)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, sub string) bool {
|
||||
return len(s) >= len(sub) && (s == sub || len(s) > 0 && indexOf(s, sub) >= 0)
|
||||
}
|
||||
|
||||
func indexOf(s, sub string) int {
|
||||
for i := 0; i+len(sub) <= len(s); i++ {
|
||||
if s[i:i+len(sub)] == sub {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
Reference in New Issue
Block a user