multi_handler_test.go
1 package daemon 2 3 import ( 4 "testing" 5 "time" 6 7 "github.com/Kocoro-lab/ShanClaw/internal/agent" 8 ) 9 10 // spyHandler counts each callback. Used by all multiHandler tests. 11 type spyHandler struct { 12 toolCalls int 13 toolResults int 14 text int 15 streamDelta int 16 approvalCalls int 17 approvalReturn bool // what OnApprovalNeeded returns 18 usage int 19 cloudAgent int 20 cloudProgress int 21 cloudPlan int 22 } 23 24 func (s *spyHandler) OnToolCall(name, args string) { s.toolCalls++ } 25 func (s *spyHandler) OnToolResult(name, args string, r agent.ToolResult, e time.Duration) { 26 s.toolResults++ 27 } 28 func (s *spyHandler) OnText(t string) { s.text++ } 29 func (s *spyHandler) OnStreamDelta(d string) { s.streamDelta++ } 30 func (s *spyHandler) OnApprovalNeeded(t, a string) bool { 31 s.approvalCalls++ 32 return s.approvalReturn 33 } 34 func (s *spyHandler) OnUsage(u agent.TurnUsage) {} 35 func (s *spyHandler) OnCloudAgent(id, status, msg string) { s.cloudAgent++ } 36 func (s *spyHandler) OnCloudProgress(done, total int) { s.cloudProgress++ } 37 func (s *spyHandler) OnCloudPlan(pt, content string, needsReview bool) { s.cloudPlan++ } 38 39 // usageSpy wraps spyHandler with a real OnUsage that increments the counter. 40 type usageSpy struct { 41 spyHandler 42 } 43 44 func (u *usageSpy) OnUsage(turn agent.TurnUsage) { u.usage++ } 45 46 func TestMultiHandlerFansOutBaseMethods(t *testing.T) { 47 a, b := &usageSpy{}, &usageSpy{} 48 m := &multiHandler{handlers: []agent.EventHandler{a, b}} 49 50 m.OnToolCall("bash", "ls") 51 m.OnToolResult("bash", "ls", agent.ToolResult{}, 0) 52 m.OnText("hi") 53 m.OnStreamDelta("d") 54 m.OnUsage(agent.TurnUsage{}) 55 m.OnCloudAgent("id", "running", "msg") 56 m.OnCloudProgress(1, 2) 57 m.OnCloudPlan("research", "content", false) 58 59 for _, s := range []*usageSpy{a, b} { 60 if s.toolCalls != 1 || s.toolResults != 1 || s.text != 1 || s.streamDelta != 1 || 61 s.usage != 1 || s.cloudAgent != 1 || s.cloudProgress != 1 || s.cloudPlan != 1 { 62 t.Fatalf("spy counts off: %+v", s.spyHandler) 63 } 64 } 65 } 66 67 func TestMultiHandlerApprovalORsResults(t *testing.T) { 68 a := &usageSpy{} 69 a.approvalReturn = false 70 b := &usageSpy{} 71 b.approvalReturn = true 72 m := &multiHandler{handlers: []agent.EventHandler{a, b}} 73 74 if !m.OnApprovalNeeded("bash", "rm -rf /") { 75 t.Fatal("want OR=true when any handler returns true") 76 } 77 if a.approvalCalls != 1 || b.approvalCalls != 1 { 78 t.Fatalf("approval calls: a=%d b=%d, want 1 each", a.approvalCalls, b.approvalCalls) 79 } 80 81 // When all return false, result is false. 82 c := &usageSpy{} 83 c.approvalReturn = false 84 d := &usageSpy{} 85 d.approvalReturn = false 86 m2 := &multiHandler{handlers: []agent.EventHandler{c, d}} 87 if m2.OnApprovalNeeded("bash", "ls") { 88 t.Fatal("want false when all handlers return false") 89 } 90 } 91 92 // Confirms multiHandler can be assigned to agent.EventHandler — both the loop 93 // and tests pass it via that interface. A compile-time check keeps signature 94 // drift loud. 95 func TestMultiHandlerSatisfiesEventHandlerInterface(t *testing.T) { 96 var _ agent.EventHandler = (*multiHandler)(nil) 97 } 98 99 // sessIDSpy implements agent.EventHandler (via embedded spyHandler) AND SetSessionID. 100 // Used to verify multiHandler.SetSessionID propagates via type assertion. 101 type sessIDSpy struct { 102 usageSpy 103 receivedID string 104 } 105 106 func (s *sessIDSpy) SetSessionID(id string) { s.receivedID = id } 107 108 // plainSpy does NOT implement SetSessionID — used to verify the type assertion 109 // skips handlers cleanly rather than panicking. 110 type plainSpy struct { 111 usageSpy 112 } 113 114 func TestMultiHandlerSetSessionIDPropagatesToImplementers(t *testing.T) { 115 setter := &sessIDSpy{} 116 plain := &plainSpy{} 117 m := &multiHandler{handlers: []agent.EventHandler{setter, plain}} 118 119 m.SetSessionID("sess_abc") 120 121 if setter.receivedID != "sess_abc" { 122 t.Fatalf("setter.receivedID = %q, want sess_abc", setter.receivedID) 123 } 124 // plain has no SetSessionID; surviving the call without panic is the assertion. 125 // Also verify the plain spy still receives base callbacks — the SetSessionID 126 // bypass must not break the normal fan-out. 127 m.OnText("x") 128 if plain.text != 1 { 129 t.Fatalf("plain.text = %d, want 1 — SetSessionID bypass must not break fan-out", plain.text) 130 } 131 } 132 133 // RunAgent type-asserts the top-level handler against the optional SetSessionID 134 // interface. multiHandler must itself satisfy that optional interface so the 135 // RunAgent assertion succeeds when the injected handler is a *multiHandler. 136 func TestMultiHandlerItselfImplementsSetSessionID(t *testing.T) { 137 m := &multiHandler{} 138 _, ok := interface{}(m).(interface{ SetSessionID(string) }) 139 if !ok { 140 t.Fatal("multiHandler does not satisfy SetSessionID(string) interface") 141 } 142 } 143 144 // runStatusSpy implements agent.RunStatusHandler (and agent.EventHandler via the 145 // embedded usageSpy). Used to verify multiHandler.OnRunStatus propagates via 146 // type assertion. 147 type runStatusSpy struct { 148 usageSpy 149 calls []string // "code:detail" 150 } 151 152 func (s *runStatusSpy) OnRunStatus(code, detail string) { 153 s.calls = append(s.calls, code+":"+detail) 154 } 155 156 func TestMultiHandlerOnRunStatusPropagatesToImplementers(t *testing.T) { 157 rsh := &runStatusSpy{} 158 plain := &plainSpy{} 159 m := &multiHandler{handlers: []agent.EventHandler{rsh, plain}} 160 161 m.OnRunStatus("idle_soft", "15s idle") 162 163 if len(rsh.calls) != 1 || rsh.calls[0] != "idle_soft:15s idle" { 164 t.Fatalf("rsh.calls = %+v", rsh.calls) 165 } 166 // plain has no OnRunStatus — call must not panic and not affect rsh. 167 } 168 169 // Verify multiHandler satisfies agent.RunStatusHandler. The watchdog in the 170 // agent loop does `a.handler.(RunStatusHandler)` type assertion at 171 // runner.go:917-935; with multiHandler in the chain, the assertion must 172 // succeed so the watchdog can emit soft/hard idle events. 173 func TestMultiHandlerSatisfiesRunStatusHandlerInterface(t *testing.T) { 174 var _ agent.RunStatusHandler = (*multiHandler)(nil) // compile-time check 175 }