blob: 820d9282e722c7c389d39bfb1ce1f8ff87698052 [file] [log] [blame]
Philip Zeyliger9373c072025-05-01 10:27:01 -07001package loop
2
3import (
4 "context"
5 "testing"
6 "time"
7)
8
9// TestIteratorBasic tests basic iterator functionality
10func TestIteratorBasic(t *testing.T) {
11 // Create an agent with some predefined messages
12 agent := &Agent{
13 subscribers: []chan *AgentMessage{},
14 }
15
16 // Add some test messages to the history
17 agent.mu.Lock()
18 agent.history = []AgentMessage{
19 {Type: AgentMessageType, Content: "Message 1", Idx: 0},
20 {Type: AgentMessageType, Content: "Message 2", Idx: 1},
21 {Type: AgentMessageType, Content: "Message 3", Idx: 2},
22 }
23 agent.mu.Unlock()
24
25 // Create an iterator starting from the beginning
26 ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
27 defer cancel()
28 it := agent.NewIterator(ctx, 0)
29 defer it.Close()
30
31 // Read all messages and verify
32 for i := range 3 {
33 msg := it.Next()
34 if msg == nil {
35 t.Fatalf("Expected message %d but got nil", i)
36 }
37 expectedNum := i + 1
38 expectedContent := "Message " + string(rune('0')+rune(expectedNum))
39 if msg.Content != expectedContent {
40 t.Errorf("Expected message %d to be 'Message %d', got '%s'", i+1, i+1, msg.Content)
41 }
42 }
43}
44
45// TestIteratorStartFromMiddle tests starting an iterator from a specific index
46func TestIteratorStartFromMiddle(t *testing.T) {
47 // Create an agent with some predefined messages
48 agent := &Agent{
49 subscribers: []chan *AgentMessage{},
50 }
51
52 // Add some test messages to the history
53 agent.mu.Lock()
54 agent.history = []AgentMessage{
55 {Type: AgentMessageType, Content: "Message 1", Idx: 0},
56 {Type: AgentMessageType, Content: "Message 2", Idx: 1},
57 {Type: AgentMessageType, Content: "Message 3", Idx: 2},
58 }
59 agent.mu.Unlock()
60
61 // Create an iterator starting from index 1
62 ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
63 defer cancel()
64 it := agent.NewIterator(ctx, 1)
65 defer it.Close()
66
67 // We should get Message 2 and 3
68 msg := it.Next()
69 if msg == nil || msg.Content != "Message 2" {
70 t.Errorf("Expected 'Message 2', got %v", msg)
71 }
72
73 msg = it.Next()
74 if msg == nil || msg.Content != "Message 3" {
75 t.Errorf("Expected 'Message 3', got %v", msg)
76 }
77}
78
79// TestIteratorWithNewMessages tests that the iterator properly waits for and receives new messages
80func TestIteratorWithNewMessages(t *testing.T) {
81 // Create an agent
82 agent := &Agent{
83 subscribers: []chan *AgentMessage{},
84 }
85
86 // Create an iterator
87 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
88 defer cancel()
89 it := agent.NewIterator(ctx, 0)
90 defer it.Close()
91
92 // Use channels to synchronize instead of sleeps
93 msg1Added := make(chan struct{})
94 msg2Ready := make(chan struct{})
95
96 // Add messages in another goroutine
97 go func() {
98 // Add first message immediately
99 agent.pushToOutbox(context.Background(), AgentMessage{Type: AgentMessageType, Content: "New message 1"})
100
101 // Signal that message 1 is added
102 close(msg1Added)
103
104 // Wait for signal that we're ready for message 2
105 <-msg2Ready
106
107 // Add second message
108 agent.pushToOutbox(context.Background(), AgentMessage{Type: AgentMessageType, Content: "New message 2"})
109 }()
110
111 // Read first message
112 msg := it.Next()
113 if msg == nil || msg.Content != "New message 1" {
114 t.Errorf("Expected 'New message 1', got %v", msg)
115 }
116
117 // Signal that we're ready for message 2
118 close(msg2Ready)
119
120 // Read second message
121 msg = it.Next()
122 if msg == nil || msg.Content != "New message 2" {
123 t.Errorf("Expected 'New message 2', got %v", msg)
124 }
125}
126
127// TestIteratorClose tests that closing an iterator removes it from the subscribers list
128func TestIteratorClose(t *testing.T) {
129 // Create an agent
130 agent := &Agent{
131 subscribers: []chan *AgentMessage{},
132 }
133
134 // Create an iterator
135 ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
136 defer cancel()
137 it := agent.NewIterator(ctx, 0)
138
139 // Verify iterator was added to subscribers after it tries to get a message
140 // that doesn't exist yet
141 agent.mu.Lock()
142 agent.history = []AgentMessage{} // ensure history is empty
143 agent.mu.Unlock()
144
145 // Start a goroutine to call Next() which should subscribe
146 done := make(chan struct{})
147 go func() {
148 // This will block after subscribing
149 it.Next()
150 close(done)
151 }()
152
153 // Give a short time for the goroutine to run and subscribe
154 time.Sleep(10 * time.Millisecond)
155
156 // Check that we have a subscriber
157 agent.mu.Lock()
158 subscriberCount := len(agent.subscribers)
159 agent.mu.Unlock()
160
161 if subscriberCount != 1 {
162 t.Errorf("Expected 1 subscriber, got %d", subscriberCount)
163 }
164
165 // Close the iterator
166 it.Close()
167
168 // Add a message to trigger the goroutine to exit (in case it's still waiting)
169 agent.pushToOutbox(context.Background(), AgentMessage{Type: AgentMessageType, Content: "Test message"})
170
171 // Wait for the goroutine to finish
172 select {
173 case <-done:
174 // Good, it finished
175 case <-time.After(100 * time.Millisecond):
176 t.Error("Timed out waiting for iterator goroutine to finish")
177 }
178
179 // Verify the subscriber was removed
180 agent.mu.Lock()
181 subscriberCount = len(agent.subscribers)
182 agent.mu.Unlock()
183
184 if subscriberCount != 0 {
185 t.Errorf("Expected 0 subscribers after Close(), got %d", subscriberCount)
186 }
187}
188
189// TestIteratorContextCancel tests that an iterator stops properly when its context is cancelled
190func TestIteratorContextCancel(t *testing.T) {
191 // Create an agent
192 agent := &Agent{
193 subscribers: []chan *AgentMessage{},
194 }
195
196 // Create a context that we can cancel
197 ctx, cancel := context.WithCancel(context.Background())
198 it := agent.NewIterator(ctx, 0)
199 defer it.Close()
200
201 // Start a goroutine to call Next() which will block
202 resultChan := make(chan *AgentMessage)
203 go func() {
204 resultChan <- it.Next()
205 }()
206
207 // Wait a minimal time, then cancel the context
208 time.Sleep(10 * time.Millisecond)
209 cancel()
210
211 // Verify we get nil from the iterator
212 select {
213 case result := <-resultChan:
214 if result != nil {
215 t.Errorf("Expected nil result after context cancel, got %v", result)
216 }
217 case <-time.After(100 * time.Millisecond):
218 t.Error("Timed out waiting for iterator to return after context cancel")
219 }
220
221 // Verify the subscriber was removed due to context cancellation
222 agent.mu.Lock()
223 subscriberCount := len(agent.subscribers)
224 agent.mu.Unlock()
225
226 if subscriberCount != 0 {
227 t.Errorf("Expected 0 subscribers after context cancel, got %d", subscriberCount)
228 }
229}