Skip to content

Commit 19b3a83

Browse files
committed
chore: add MockEmitter.WaitForStatus
1 parent 0ff03dc commit 19b3a83

1 file changed

Lines changed: 62 additions & 30 deletions

File tree

x/acpio/acp_conversation_test.go

Lines changed: 62 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,20 @@ type mockAgentIO struct {
3535

3636
// mockEmitter implements screentracker.Emitter for testing.
3737
type 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

4748
func newMockEmitter() *mockEmitter {
48-
return &mockEmitter{}
49+
m := &mockEmitter{}
50+
m.cond = sync.NewCond(&m.mu)
51+
return m
4952
}
5053

5154
func (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

6569
func (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+
78106
func newMockAgentIO() *mockAgentIO {
79107
return &mockAgentIO{}
80108
}
@@ -265,16 +293,17 @@ func Test_Send_RejectsDuplicateSend(t *testing.T) {
265293
}
266294

267295
func 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

298325
func 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

373403
func Test_InitialPrompt_SentOnStart(t *testing.T) {
@@ -425,13 +455,17 @@ func Test_Messages_AreCopied(t *testing.T) {
425455
}
426456

427457
func 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

Comments
 (0)