Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
98 lines
2.6 KiB
Go
98 lines
2.6 KiB
Go
//go:build !windows
|
|
|
|
package agent
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const codexAppServerProcessTreeTestTimeout = 3 * time.Second
|
|
|
|
func TestCodexCLIModelListerStopsProcessTreeOnTimeout(t *testing.T) {
|
|
childPIDPath := filepath.Join(t.TempDir(), "model-child.pid")
|
|
scriptPath := writeHangingCodexAppServer(t, childPIDPath)
|
|
|
|
startedAt := time.Now()
|
|
_, err := (CodexCLIModelLister{
|
|
Command: scriptPath,
|
|
Timeout: codexAppServerProcessTreeTestTimeout,
|
|
}).ListModels(context.Background())
|
|
if err == nil || !strings.Contains(err.Error(), "model/list timed out") {
|
|
t.Fatalf("ListModels error = %v, want model/list timeout", err)
|
|
}
|
|
if elapsed := time.Since(startedAt); elapsed > 5*time.Second {
|
|
t.Fatalf("ListModels returned after %s, want bounded process cleanup", elapsed)
|
|
}
|
|
assertProcessExited(t, readChildPID(t, childPIDPath))
|
|
}
|
|
|
|
func TestCodexCLICapabilityListerStopsProcessTreeOnTimeout(t *testing.T) {
|
|
childPIDPath := filepath.Join(t.TempDir(), "capability-child.pid")
|
|
scriptPath := writeHangingCodexAppServer(t, childPIDPath)
|
|
|
|
startedAt := time.Now()
|
|
_, err := (CodexCLICapabilityLister{
|
|
Command: scriptPath,
|
|
Timeout: codexAppServerProcessTreeTestTimeout,
|
|
}).List(context.Background(), "")
|
|
if err == nil || !strings.Contains(err.Error(), "capability discovery timed out") {
|
|
t.Fatalf("List error = %v, want capability discovery timeout", err)
|
|
}
|
|
if elapsed := time.Since(startedAt); elapsed > 5*time.Second {
|
|
t.Fatalf("List returned after %s, want bounded process cleanup", elapsed)
|
|
}
|
|
assertProcessExited(t, readChildPID(t, childPIDPath))
|
|
}
|
|
|
|
func writeHangingCodexAppServer(t *testing.T, childPIDPath string) string {
|
|
t.Helper()
|
|
scriptPath := filepath.Join(t.TempDir(), "codex")
|
|
script := fmt.Sprintf(`#!/bin/sh
|
|
sleep 5 &
|
|
child_pid=$!
|
|
printf '%%s\n' "$child_pid" > %q
|
|
wait "$child_pid"
|
|
`, childPIDPath)
|
|
if err := os.WriteFile(scriptPath, []byte(script), 0o755); err != nil {
|
|
t.Fatalf("write fake codex app-server: %v", err)
|
|
}
|
|
return scriptPath
|
|
}
|
|
|
|
func readChildPID(t *testing.T, path string) int {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read child pid: %v", err)
|
|
}
|
|
pid, err := strconv.Atoi(strings.TrimSpace(string(raw)))
|
|
if err != nil {
|
|
t.Fatalf("parse child pid %q: %v", raw, err)
|
|
}
|
|
return pid
|
|
}
|
|
|
|
func assertProcessExited(t *testing.T, pid int) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for {
|
|
err := syscall.Kill(pid, 0)
|
|
if errors.Is(err, syscall.ESRCH) {
|
|
return
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("child process %d is still alive after app-server timeout", pid)
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
}
|