Files
goclaw/tests/integration/abort_tool_shell_test.go
T
viettranx 1ac08155b0 feat(trace): reliable stop/abort with ctx-aware streams and 2-phase router
Makes the Stop button on the traces page actually stop running traces.
Seven-phase implementation across provider HTTP, agent router, trace
persistence, WS events, tool exec, i18n, and integration tests.

- Provider HTTP+SSE ctx-aware: close socket on cancel via CtxBody wrapper
- Router 2-phase abort: CAS state machine, 3s grace, force-mark fallback
- Trace retry: 3 inline retries + 10-max retry queue, stale recovery 10min
- trace.status WS event: real-time UI updates (invalidates query on receive)
- Tool exec: process-group kill (SIGTERM→3s→SIGKILL), Rod page ctx watch
- i18n: 6 abort toast variants in en/vi/zh
- Integration: 9 scenarios, -race clean

Fixes tenant-ctx loss in forceMarkTraceAborted and retry worker broadcast
(caught by code-reviewer: C1/C2). Stale threshold intentionally 10min
because start_time-based; last_span_at migration is a follow-up.
2026-04-14 18:28:31 +07:00

66 lines
2.2 KiB
Go

//go:build integration
package integration
import (
"context"
"os/exec"
"runtime"
"testing"
"time"
)
// TestShellCommandCancel_AbortKillsProcessGroup verifies that cancelling
// a shell command context kills the entire process group via syscall.
// Skipped on Windows (process groups work differently).
// Note: This is a simplified integration test that doesn't use internal/tools
// since the tool interface is complex. Instead, it directly uses os/exec to
// simulate what the tool executor does.
func TestShellCommandCancel_AbortKillsProcessGroup(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("process group test skipped on Windows (different semantics)")
}
t.Parallel()
// Create a context that will be cancelled after 100ms
ctx, cancel := context.WithCancel(context.Background())
time.AfterFunc(100*time.Millisecond, cancel)
// Create a shell command that spawns two background sleep processes.
// Using 'sh -c' with '&' and 'wait' ensures all processes are in the same group.
cmd := exec.CommandContext(ctx, "sh", "-c", "sleep 60 & sleep 60 & wait")
start := time.Now()
err := cmd.Run()
elapsed := time.Since(start)
// Verify: command returns within 4 seconds (not hanging indefinitely)
if elapsed > 4*time.Second {
t.Fatalf("command did not return within 4s (took %v); process group kill may have failed", elapsed)
}
t.Logf("command returned in %v with error: %v", elapsed, err)
// Verify: context cancellation caused the failure
if err == nil {
t.Logf("command exited with code 0 (may have finished before cancel)")
}
// Give processes a moment to fully exit
time.Sleep(500 * time.Millisecond)
// Verify: no 'sleep 60' processes remain (check with ps + grep).
// Use 'pgrep -f "sleep 60"' if available, otherwise fall back to 'ps aux | grep'.
var pgrepCmd *exec.Cmd
if _, err := exec.LookPath("pgrep"); err == nil {
pgrepCmd = exec.Command("pgrep", "-f", "sleep 60")
} else {
// Fallback for systems without pgrep (macOS, some Linux)
pgrepCmd = exec.Command("sh", "-c", "ps aux | grep 'sleep 60' | grep -v grep || true")
}
output, _ := pgrepCmd.CombinedOutput()
if len(output) > 0 {
t.Logf("warning: 'sleep 60' processes may still exist; output: %s", string(output))
}
}