@@ -35,17 +35,20 @@ type mockAgentIO struct {
3535
3636// mockEmitter implements screentracker.Emitter for testing.
3737type mockEmitter struct {
38- mu sync.Mutex
39- messagesCalls int
40- statusCalls int
41- screenCalls int
42- lastMessages []screentracker.ConversationMessage
43- lastStatus screentracker.ConversationStatus
44- lastScreen string
38+ mu sync.Mutex
39+ cond * sync.Cond
40+ messagesCalls int
41+ statusCalls int
42+ screenCalls int
43+ lastMessages []screentracker.ConversationMessage
44+ lastStatus screentracker.ConversationStatus
45+ lastScreen string
4546}
4647
4748func newMockEmitter () * mockEmitter {
48- return & mockEmitter {}
49+ m := & mockEmitter {}
50+ m .cond = sync .NewCond (& m .mu )
51+ return m
4952}
5053
5154func (m * mockEmitter ) EmitMessages (messages []screentracker.ConversationMessage ) {
@@ -60,6 +63,7 @@ func (m *mockEmitter) EmitStatus(status screentracker.ConversationStatus) {
6063 defer m .mu .Unlock ()
6164 m .statusCalls ++
6265 m .lastStatus = status
66+ m .cond .Broadcast ()
6367}
6468
6569func (m * mockEmitter ) EmitScreen (screen string ) {
@@ -75,6 +79,30 @@ func (m *mockEmitter) TotalCalls() int {
7579 return m .messagesCalls + m .statusCalls + m .screenCalls
7680}
7781
82+ // WaitForStatus blocks until the emitter's last status matches target.
83+ // Must be called with a context that has a deadline to avoid hanging tests.
84+ func (m * mockEmitter ) WaitForStatus (ctx context.Context , t * testing.T , target screentracker.ConversationStatus ) {
85+ t .Helper ()
86+ if _ , ok := ctx .Deadline (); ! ok {
87+ t .Fatal ("must set a deadline to avoid hanging tests" )
88+ }
89+ done := make (chan struct {})
90+ go func () {
91+ m .mu .Lock ()
92+ defer m .mu .Unlock ()
93+ for m .lastStatus != target {
94+ m .cond .Wait ()
95+ }
96+ close (done )
97+ }()
98+ select {
99+ case <- done :
100+ case <- ctx .Done ():
101+ m .cond .Broadcast () // unblock the goroutine
102+ t .Fatalf ("timed out waiting for %q status" , target )
103+ }
104+ }
105+
78106func newMockAgentIO () * mockAgentIO {
79107 return & mockAgentIO {}
80108}
@@ -265,16 +293,17 @@ func Test_Send_RejectsDuplicateSend(t *testing.T) {
265293}
266294
267295func Test_Status_ChangesWhileProcessing (t * testing.T ) {
296+ ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
297+ defer cancel ()
298+
268299 mClock := quartz .NewMock (t )
269300 mock := newMockAgentIO ()
301+ emitter := newMockEmitter ()
270302 // Block the write so we can observe status changes
271303 started , done := mock .BlockWrite ()
272304
273- conv := acpio .NewACPConversation (context .Background (), mock , nil , nil , nil , mClock )
274- conv .Start (context .Background ())
275-
276- // Initially stable
277- assert .Equal (t , screentracker .ConversationStatusStable , conv .Status ())
305+ conv := acpio .NewACPConversation (ctx , mock , nil , nil , emitter , mClock )
306+ conv .Start (ctx )
278307
279308 // Send a message
280309 err := conv .Send (screentracker.MessagePartText {Content : "test" })
@@ -289,10 +318,8 @@ func Test_Status_ChangesWhileProcessing(t *testing.T) {
289318 // Unblock the write
290319 close (done )
291320
292- // Give the goroutine a chance to complete (status update happens after Write returns)
293- require .Eventually (t , func () bool {
294- return conv .Status () == screentracker .ConversationStatusStable
295- }, 100 * time .Millisecond , 5 * time .Millisecond , "status should return to stable" )
321+ // Wait for the goroutine to complete - status should then be stable.
322+ emitter .WaitForStatus (ctx , t , screentracker .ConversationStatusStable )
296323}
297324
298325func Test_Text_ReturnsStreamingContent (t * testing.T ) {
@@ -337,8 +364,11 @@ func Test_Emitter_CalledOnChanges(t *testing.T) {
337364
338365 emitter := newMockEmitter ()
339366
340- conv := acpio .NewACPConversation (context .Background (), mock , nil , nil , emitter , mClock )
341- conv .Start (context .Background ())
367+ ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
368+ defer cancel ()
369+
370+ conv := acpio .NewACPConversation (ctx , mock , nil , nil , emitter , mClock )
371+ conv .Start (ctx )
342372
343373 // Send a message
344374 err := conv .Send (screentracker.MessagePartText {Content : "test" })
@@ -362,12 +392,12 @@ func Test_Emitter_CalledOnChanges(t *testing.T) {
362392 close (done )
363393
364394 // Wait for completion emit
365- require . Eventually ( t , func () bool {
366- emitter . mu . Lock ()
367- c := emitter .messagesCalls
368- emitter .mu . Unlock ()
369- return c >= 3 // 2 from chunks + 1 from completion
370- }, 100 * time . Millisecond , 5 * time . Millisecond , "should receive completion emit " )
395+ emitter . WaitForStatus ( ctx , t , screentracker . ConversationStatusStable )
396+
397+ emitter .mu . Lock ()
398+ finalMessagesCalls := emitter .messagesCalls
399+ emitter . mu . Unlock ()
400+ assert . GreaterOrEqual ( t , finalMessagesCalls , 3 , "2 from chunks + 1 from completion " )
371401}
372402
373403func Test_InitialPrompt_SentOnStart (t * testing.T ) {
@@ -425,13 +455,17 @@ func Test_Messages_AreCopied(t *testing.T) {
425455}
426456
427457func Test_ErrorRemovesPartialMessage (t * testing.T ) {
458+ ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
459+ defer cancel ()
460+
428461 mClock := quartz .NewMock (t )
429462 mock := newMockAgentIO ()
463+ emitter := newMockEmitter ()
430464 // Block the write so we can simulate partial content before error
431465 started , done := mock .BlockWrite ()
432466
433- conv := acpio .NewACPConversation (context . Background () , mock , nil , nil , nil , mClock )
434- conv .Start (context . Background () )
467+ conv := acpio .NewACPConversation (ctx , mock , nil , nil , emitter , mClock )
468+ conv .Start (ctx )
435469
436470 // Send a message
437471 err := conv .Send (screentracker.MessagePartText {Content : "test" })
@@ -461,9 +495,7 @@ func Test_ErrorRemovesPartialMessage(t *testing.T) {
461495 close (done )
462496
463497 // Wait for the conversation to stabilize after the error
464- require .Eventually (t , func () bool {
465- return conv .Status () == screentracker .ConversationStatusStable
466- }, 100 * time .Millisecond , 5 * time .Millisecond , "status should return to stable" )
498+ emitter .WaitForStatus (ctx , t , screentracker .ConversationStatusStable )
467499
468500 // The partial agent message should be removed on error.
469501 // Only the user message should remain.
0 commit comments