mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-11 23:40:28 -10:00
* [LOCAL PR-Y] CLI: graceful shutdown of headless exec runs (Ctrl+C) zero exec wrapped agent.Run in context.Background(), so Ctrl+C/SIGTERM killed the process abruptly — leaking the in-flight provider request and any spawned children. runExec now wraps the run in a signal-aware context (signalContext): a signal cancels the run, the loop returns cleanly, and exec reports 'Interrupted.' with exit 130 (stream-json: an 'interrupted' run-end). Scoped to runExec — no dispatch-wide signature churn. NOTE: SIGINT runtime delivery needs a manual Ctrl+C check before PR; unit tests cover the helper + the no-regression of the exec suite. * [LOCAL PR-Y] Observability: crash capture (panic -> saved report) New internal/observability package: a recovered top-level panic is written to a local crash report (timestamp, label, full stack) under ~/.zero/crashes and surfaced as a brief notice instead of a raw stack trace. Wired into runWithDeps via a named-return defer. Fail-open and dependency-free; this is the hook a Sentry/OTEL adapter attaches to for remote reporting (deferred, needs a real endpoint to verify). Tested: report formatting/writing, panic capture sets the crash exit code + writes one report, and the no-panic path is a no-op. --------- Co-authored-by: KRATOS <[email protected]>
66 lines
1.5 KiB
Go
66 lines
1.5 KiB
Go
package observability
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestWriteAndFormatCrashReport(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ts := time.Date(2026, 6, 8, 10, 30, 0, 0, time.UTC)
|
|
path, err := WriteCrashReport(dir, "cli", "boom", []byte("goroutine 1 [running]:\nmain.x()"), ts)
|
|
if err != nil {
|
|
t.Fatalf("WriteCrashReport: %v", err)
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read report: %v", err)
|
|
}
|
|
report := string(data)
|
|
for _, want := range []string{"boom", "cli", "2026-06-08T10:30:00Z", "goroutine 1"} {
|
|
if !strings.Contains(report, want) {
|
|
t.Fatalf("report missing %q:\n%s", want, report)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRecoverCapturesPanic(t *testing.T) {
|
|
dir := t.TempDir()
|
|
var stderr bytes.Buffer
|
|
code := 0
|
|
|
|
func() {
|
|
defer Recover(dir, "test", &stderr, &code)
|
|
panic("kaboom")
|
|
}()
|
|
|
|
if code != crashExitCode {
|
|
t.Fatalf("exit code = %d, want %d", code, crashExitCode)
|
|
}
|
|
if !strings.Contains(stderr.String(), "zero crashed") || !strings.Contains(stderr.String(), "kaboom") {
|
|
t.Fatalf("missing crash notice: %q", stderr.String())
|
|
}
|
|
entries, _ := os.ReadDir(dir)
|
|
if len(entries) != 1 {
|
|
t.Fatalf("expected one crash report written, got %d", len(entries))
|
|
}
|
|
}
|
|
|
|
func TestRecoverNoPanicIsNoop(t *testing.T) {
|
|
dir := t.TempDir()
|
|
var stderr bytes.Buffer
|
|
code := 7
|
|
func() {
|
|
defer Recover(dir, "test", &stderr, &code)
|
|
}()
|
|
if code != 7 {
|
|
t.Fatalf("code changed without a panic: %d", code)
|
|
}
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("unexpected output without a panic: %q", stderr.String())
|
|
}
|
|
}
|