first commit
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user