first commit

This commit is contained in:
2026-08-14 23:41:57 +08:00
commit 086803a8dd
471 changed files with 91938 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
package run
import (
"context"
"strings"
"testing"
"github.com/smallnest/pigo/internal/hooks"
)
func stopDispatcher(t *testing.T, event, cmd string) *hooks.Dispatcher {
t.Helper()
set := hooks.HookSet{
event: {{Matcher: "*", Hooks: []hooks.HookConfig{{Command: cmd}}}},
}
d := hooks.NewDispatcher(set, t.TempDir(), nil)
if d == nil {
t.Fatal("expected non-nil dispatcher")
}
return d
}
// TestStopHookBlocksThenForceStops: a Stop hook that always blocks (exit 2) is
// honored up to the limit, then the decorator force-stops (returns nil) so the
// run cannot loop forever (FR-12).
func TestStopHookBlocksThenForceStops(t *testing.T) {
d := stopDispatcher(t, "Stop", `echo "not done yet" 1>&2; exit 2`)
var warn strings.Builder
seam := stopHook(d, HookDeps{SessionID: "s1", WarnLog: &warn}, "Stop", 3)
if seam == nil {
t.Fatal("expected non-nil seam")
}
// First 3 consultations block with the reason as guidance.
for i := 0; i < 3; i++ {
dec := seam(context.Background(), nil)
if dec == nil || !dec.Block {
t.Fatalf("consult %d: expected a blocking decision", i+1)
}
if !strings.Contains(dec.Guidance, "not done yet") {
t.Fatalf("consult %d: block reason not surfaced as guidance, got %q", i+1, dec.Guidance)
}
}
// 4th consultation exceeds the limit: force stop (nil) + a warning.
if dec := seam(context.Background(), nil); dec != nil {
t.Fatalf("expected force-stop (nil) past the limit, got %+v", dec)
}
if !strings.Contains(warn.String(), "forcing stop") {
t.Fatalf("force-stop should warn, got %q", warn.String())
}
}
// TestStopHookResetsCounterOnAllow: a non-blocking hook always returns nil, so
// the run is free to end and the consecutive-block counter never accrues.
func TestStopHookResetsCounterOnAllow(t *testing.T) {
d := stopDispatcher(t, "Stop", `exit 0`)
seam := stopHook(d, HookDeps{SessionID: "s1"}, "Stop", 2)
for i := 0; i < 5; i++ {
if dec := seam(context.Background(), nil); dec != nil {
t.Fatalf("consult %d: a non-blocking hook must let the run end (nil), got %+v", i+1, dec)
}
}
}
// TestStopHookNilDispatcher: no hooks configured yields a nil seam (no wrapping).
func TestStopHookNilDispatcher(t *testing.T) {
if seam := stopHook(nil, HookDeps{}, "Stop", 0); seam != nil {
t.Fatal("nil dispatcher must yield a nil seam")
}
}
// TestSubagentStopEvent: InstallSubagentStop wires a SubagentStop decorator whose
// block keeps a sub-agent running with the hook's reason.
func TestSubagentStopEvent(t *testing.T) {
d := stopDispatcher(t, "SubagentStop", `echo "sub not done" 1>&2; exit 2`)
seam := stopHook(d, HookDeps{SessionID: "child"}, "SubagentStop", 5)
if seam == nil {
t.Fatal("expected non-nil SubagentStop seam")
}
dec := seam(context.Background(), nil)
if dec == nil || !dec.Block || !strings.Contains(dec.Guidance, "sub not done") {
t.Fatalf("SubagentStop block not honored, got %+v", dec)
}
}