13 "github.com/stretchr/testify/require"
16 func TestParentMessageWakesChildAndFinalReplyWakesParent(t *testing.T) {
17 server := newModelFake(t)
18 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
19 text := "I will ask the user."
20 if call.body["model"] == "gpt-test-garden" {
21 text = "Approve the gate size."
24 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem(text)}, 100, 0, 5))
26 test := newDaemonMessageTest(t, t.TempDir(), server)
27 test.Start(t, 0, 1, 2)
28 require.NoError(t, test.messages.Tell("OneChat", "Garden", "Check the gate size."))
29 test.events[0].WaitFor(t, "idle", 1)
30 test.events[1].WaitFor(t, "idle", 1)
32 require.Equal(t, "OneChat", test.events[1].LastEvent("user").Source)
33 require.Equal(t, "Check the gate size.", test.events[1].Last("user"))
34 require.Equal(t, "Garden", test.events[0].LastEvent("work").Source)
35 require.Equal(t, "[Garden] Approve the gate size.", test.events[0].Last("work"))
36 require.Equal(t, []string{"user", "busy", "reply", "idle"}, test.events[1].Kinds())
37 require.Equal(t, []string{"work", "busy", "reply", "idle"}, test.events[0].Kinds())
38 require.Equal(t, 2, server.Calls())
39 require.Zero(t, test.agents[2].memory.Len())
40 require.Empty(t, test.events[2].Kinds())
42 first, err := test.agents[1].memory.Message(0)
43 require.NoError(t, err)
44 require.Equal(t, "user", first.Kind)
46 report, err := test.agents[0].memory.Message(0)
47 require.NoError(t, err)
48 require.Equal(t, "work", report.Kind)
49 require.Equal(t, "[Garden] Approve the gate size.", report.Text)
50 require.Contains(t, test.logs.String(), "turn started name=Garden")
51 require.Contains(t, test.logs.String(), "turn done name=Garden")
52 require.NotContains(t, test.logs.String(), "gate size")
53 require.Empty(t, test.Queue(t).Pending)
55 info, err := os.Stat(test.messages.path)
56 require.NoError(t, err)
57 require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
60 func TestChildReportSendsATelegramCompletionBeforeTheParentReply(t *testing.T) {
61 server := newModelFake(t)
62 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
63 text := "I will ask the user."
64 if call.body["model"] == "gpt-test-garden" {
65 text = "Approve the gate size."
68 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem(text)}, 100, 0, 5))
70 test := newDaemonMessageTest(t, t.TempDir(), server)
71 telegram := newTelegramFake(t)
72 bot := newTelegramBot(newTelegramAPI(telegram.URL, telegramTestToken), telegramTestOwner, filepath.Join(t.TempDir(), telegramOffsetFileName), discardLog())
73 test.loops[0].notify = func(event Event) {
74 test.events[0].Add(event)
78 ctx, cancel := context.WithCancel(t.Context())
81 done := make(chan struct{})
94 require.NoError(t, test.messages.Tell("OneChat", "Garden", "Check the gate size."))
95 test.events[0].WaitFor(t, "idle", 1)
96 require.Eventually(t, func() bool {
97 return len(telegram.Sent()) >= 2
98 }, 10*time.Second, time.Millisecond)
99 require.Equal(t, []string{"[Garden] finished", "I will ask the user."}, telegram.Sent())
100 require.Equal(t, subagentFinished, test.events[0].LastEvent("work").Status)
101 require.Equal(t, "[Garden] Approve the gate size.", test.events[0].Last("work"))
104 func TestQueuedAgentMessageFailureKeepsItsOperationAndCause(t *testing.T) {
105 server := newModelFake(t)
106 test := newDaemonMessageTest(t, t.TempDir(), server)
107 require.NoError(t, test.messages.Tell("OneChat", "Garden", "Keep the plan."))
109 failure := errors.New("fake queued message failure")
110 test.loops[1].fail(failure)
112 err := test.loops[1].Run(t.Context())
113 require.ErrorIs(t, err, failure)
114 require.ErrorContains(t, err, "check turn loop failure")
115 require.Zero(t, server.Calls())
118 func TestEmptyChildReplySendsNothingAndWakesNobody(t *testing.T) {
119 testCases := []struct {
124 name: "empty final reply",
128 name: "whitespace final reply",
132 for _, tc := range testCases {
133 t.Run(tc.name, func(t *testing.T) {
134 server := newModelFake(t)
135 server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
136 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem(tc.text)}, 100, 0, 5))
138 test := newDaemonMessageTest(t, t.TempDir(), server)
139 test.Start(t, 0, 1, 2)
140 require.NoError(t, test.messages.Tell("OneChat", "Garden", "Keep the plan."))
141 test.events[1].WaitFor(t, "idle", 1)
142 require.Equal(t, 1, server.Calls())
143 require.Zero(t, test.agents[0].memory.Len())
144 require.Empty(t, test.events[0].Kinds())
145 require.False(t, test.loops[0].waiting())
146 require.Empty(t, test.events[2].Kinds())
147 require.Empty(t, test.Queue(t).Pending)
152 func TestBusyChildQueuesTheNextParentMessage(t *testing.T) {
153 server := newModelFake(t)
154 arrived := make(chan struct{})
155 release := make(chan struct{})
156 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
157 if call.number == 0 {
162 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("")}, 100, 0, 5))
164 test := newDaemonMessageTest(t, t.TempDir(), server)
166 require.NoError(t, test.messages.Tell("OneChat", "Garden", "Check the first plan."))
168 require.True(t, test.loops[1].Busy())
169 require.NoError(t, test.messages.Tell("OneChat", "Garden", "Check the second plan too."))
170 require.Equal(t, 1, server.Calls())
171 require.Len(t, test.Queue(t).Pending, 2)
173 test.events[1].WaitFor(t, "idle", 2)
174 require.Equal(t, 2, server.Calls())
176 texts := turnInputTexts(t, server.Call(1))
177 require.Equal(t, "Check the second plan too.", texts[len(texts)-1])
178 require.Empty(t, test.Queue(t).Pending)
181 func TestBusyParentQueuesItsChildReport(t *testing.T) {
182 server := newModelFake(t)
183 arrived := make(chan struct{})
184 release := make(chan struct{})
185 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
186 text := "The plan is ready."
187 if call.number == 0 {
191 text = "I will continue."
194 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem(text)}, 100, 0, 5))
196 test := newDaemonMessageTest(t, t.TempDir(), server)
199 _, err := test.loops[0].Submit("terminal", "Start the work.")
200 require.NoError(t, err)
202 require.NoError(t, test.messages.Tell("OneChat", "Garden", "Prepare the plan."))
203 test.events[1].WaitFor(t, "idle", 1)
204 require.Equal(t, "[Garden] The plan is ready.", test.events[0].Last("work"))
205 require.True(t, test.loops[0].Busy())
206 require.Equal(t, 2, server.Calls())
208 test.events[0].WaitFor(t, "idle", 2)
209 require.Equal(t, 3, server.Calls())
211 texts := turnInputTexts(t, server.Call(2))
212 require.Equal(t, "[Garden] The plan is ready.", texts[len(texts)-1])
213 require.Empty(t, test.Queue(t).Pending)
216 func TestChildReportsOnlyTheFinalReplyAndTakesQueuedToolInput(t *testing.T) {
217 server := newModelFake(t)
218 arrived := make(chan struct{})
219 release := make(chan struct{})
220 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
221 if call.number == 0 {
224 modelWriteEvents(w, modelCompletedEvent([]any{
225 modelMessageItem("I will check the plan."),
226 modelFunctionCallItem("fc_1", "call_1", "date", `{"id":0}`),
232 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("The plan needs approval.")}, 100, 0, 5))
234 test := newDaemonMessageTest(t, t.TempDir(), server)
236 require.NoError(t, test.messages.Tell("OneChat", "Garden", "Check the plan."))
238 require.NoError(t, test.messages.Tell("OneChat", "Garden", "Also check the date."))
240 test.events[1].WaitFor(t, "idle", 1)
241 require.Equal(t, 2, server.Calls())
242 require.Equal(t, 1, test.events[1].Count("busy"))
243 require.Equal(t, 1, test.events[0].Count("work"))
244 require.Equal(t, "[Garden] The plan needs approval.", test.events[0].Last("work"))
245 require.NotContains(t, test.events[0].Last("work"), "I will check")
247 texts := turnInputTexts(t, server.Call(1))
248 require.Equal(t, "Also check the date.", texts[len(texts)-1])
250 queue := test.Queue(t)
251 require.Len(t, queue.Pending, 1)
252 require.Equal(t, "work", queue.Pending[0].Kind)
255 func TestQueuedParentMessageSurvivesDaemonRestart(t *testing.T) {
257 server := newModelFake(t)
258 server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
259 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("")}, 100, 0, 5))
261 first := newDaemonMessageTest(t, dir, server)
262 require.NoError(t, first.messages.Tell("OneChat", "Garden", "Remember the garden plan."))
263 require.Equal(t, 1, first.agents[1].memory.Len())
266 second := newDaemonMessageTest(t, dir, server)
267 require.Equal(t, 1, second.agents[1].memory.Len())
268 require.Equal(t, "OneChat", second.events[1].LastEvent("user").Source)
269 second.Start(t, 0, 1)
270 second.events[1].WaitFor(t, "idle", 1)
271 require.Equal(t, 1, server.Calls())
272 require.Empty(t, second.Queue(t).Pending)
275 third := newDaemonMessageTest(t, dir, server)
276 require.Equal(t, 1, third.agents[1].memory.Len())
277 require.False(t, third.loops[1].waiting())
278 require.Empty(t, third.events[1].Kinds())
281 func TestQueuedChildReportSurvivesDaemonRestart(t *testing.T) {
283 server := newModelFake(t)
284 first := newDaemonMessageTest(t, dir, server)
286 require.NoError(t, first.messages.Tell("OneChat", "Garden", "Review the plan."))
287 first.events[1].WaitFor(t, "idle", 1)
288 require.Equal(t, "[Garden] a line", first.events[0].Last("work"))
289 require.Len(t, first.Queue(t).Pending, 1)
292 second := newDaemonMessageTest(t, dir, server)
293 require.Equal(t, 1, second.agents[0].memory.Len())
294 require.False(t, second.loops[1].waiting())
295 second.Start(t, 0, 1)
296 second.events[0].WaitFor(t, "idle", 1)
297 require.Equal(t, 2, server.Calls())
298 require.Equal(t, 2, second.agents[0].memory.Len())
299 require.Empty(t, second.Queue(t).Pending)
302 third := newDaemonMessageTest(t, dir, server)
303 require.Equal(t, 2, third.agents[0].memory.Len())
304 require.False(t, third.loops[0].waiting())
305 require.Empty(t, third.events[0].Kinds())
308 func TestInterruptedChildDeliveryResumesAfterDaemonRestart(t *testing.T) {
310 server := newModelFake(t)
311 arrived := make(chan struct{})
312 release := make(chan struct{})
313 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
314 if call.number == 0 {
319 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("")}, 100, 0, 5))
321 first := newDaemonMessageTest(t, dir, server)
323 require.NoError(t, first.messages.Tell("OneChat", "Garden", "Finish the garden plan."))
328 second := newDaemonMessageTest(t, dir, server)
329 require.Equal(t, 1, second.agents[1].memory.Len())
331 second.events[1].WaitFor(t, "idle", 1)
332 require.Equal(t, 2, server.Calls())
333 require.Equal(t, 1, second.agents[1].memory.Len())
334 require.Empty(t, second.Queue(t).Pending)
337 func TestInterruptedParentReportResumesAfterDaemonRestart(t *testing.T) {
339 server := newModelFake(t)
340 arrived := make(chan struct{})
341 release := make(chan struct{})
342 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
343 if call.number == 1 {
348 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("The plan is ready.")}, 100, 0, 5))
350 first := newDaemonMessageTest(t, dir, server)
352 require.NoError(t, first.messages.Tell("OneChat", "Garden", "Review the plan."))
357 second := newDaemonMessageTest(t, dir, server)
358 require.Equal(t, 1, second.agents[0].memory.Len())
359 require.False(t, second.loops[1].waiting())
360 second.Start(t, 0, 1)
361 second.events[0].WaitFor(t, "idle", 1)
362 require.Equal(t, 3, server.Calls())
363 require.Equal(t, 2, second.agents[0].memory.Len())
364 require.Empty(t, second.Queue(t).Pending)
366 texts := turnInputTexts(t, server.Call(2))
367 require.Equal(t, "[Garden] The plan is ready.", texts[len(texts)-1])
370 func TestAgentMessageWriteFailureStopsDelivery(t *testing.T) {
371 server := newModelFake(t)
372 test := newDaemonMessageTest(t, t.TempDir(), server)
373 require.NoError(t, os.Mkdir(test.messages.path, 0o700))
374 require.ErrorContains(t, test.messages.Tell("OneChat", "Garden", "Keep the plan."), "save agent messages")
375 require.ErrorContains(t, test.messages.Tell("OneChat", "Garden", "Do not send again."), "save agent messages")
377 for _, loop := range test.loops {
378 require.ErrorContains(t, loop.failed(), "save agent messages")
379 require.Zero(t, loop.memory.Len())
383 func TestAgentMessageSnapshotBlocksDelivery(t *testing.T) {
384 server := newModelFake(t)
385 test := newDaemonMessageTest(t, t.TempDir(), server)
386 entered := make(chan struct{})
387 release := make(chan struct{})
391 defer once.Do(func() {
395 snapshot := make(chan error, 1)
397 snapshot <- test.messages.Snapshot(func() error {
398 return daemonSnapshot(t.Context(), test.loops, func() error {
409 sent := make(chan error, 1)
410 started := make(chan struct{})
414 sent <- test.messages.Tell("OneChat", "Garden", "Record the plan after the copy.")
418 require.Never(t, func() bool {
420 }, 50*time.Millisecond, time.Millisecond)
424 require.NoError(t, <-snapshot)
425 require.NoError(t, <-sent)
426 require.Equal(t, 1, test.agents[1].memory.Len())
427 require.Len(t, test.Queue(t).Pending, 1)
430 func TestParentCanMessageOnlyItsOwnChildren(t *testing.T) {
431 server := newModelFake(t)
433 test := newDaemonMessageTest(t, t.TempDir(), server)
434 testCases := []struct {
440 name: "no sibling messages",
445 name: "no arbitrary child reports",
450 name: "no self messages",
455 name: "unknown sender",
460 name: "unknown recipient",
465 for _, tc := range testCases {
466 t.Run(tc.name, func(t *testing.T) {
467 require.ErrorContains(t, test.messages.Tell(tc.source, tc.target, "Do not deliver."), "not a child")
471 for _, agent := range test.agents {
472 require.Zero(t, agent.memory.Len())
476 func TestInvalidAgentMessageQueueStopsStartup(t *testing.T) {
477 server := newModelFake(t)
478 testCases := []struct {
487 name: "unknown sender",
488 file: `{"next":1,"pending":[{"id":1,"source":"Other","target":"Garden","kind":"user"}]}`,
491 name: "sibling delivery",
492 file: `{"next":1,"pending":[{"id":1,"source":"Library","target":"Garden","kind":"user"}]}`,
495 name: "invalid sequence",
496 file: `{"next":0,"pending":[{"id":1,"source":"OneChat","target":"Garden","kind":"user"}]}`,
499 for _, tc := range testCases {
500 t.Run(tc.name, func(t *testing.T) {
502 test := newDaemonMessageTest(t, dir, server)
503 path := test.messages.path
505 require.NoError(t, os.WriteFile(path, []byte(tc.file), 0o600))
507 agents, err := openDaemonAgents(t.Context(), dir, server.URL, ModelAPIKey(modelTestKey), defaultConfig().Compactor, newTestClock().Now, discardLog())
508 require.NoError(t, err)
511 require.NoError(t, agents.Close())
514 var loops []*TurnLoop
516 for _, agent := range agents {
517 loop, err := agent.Loop(nil, func(Event) {
519 require.NoError(t, err)
521 loops = append(loops, loop)
524 _, err = openDaemonMessages(loops)
525 require.Error(t, err)
527 for _, agent := range agents {
528 require.Zero(t, agent.memory.Len())
534 func TestChildScheduleWakesOnlyItsOwner(t *testing.T) {
535 server := newModelFake(t)
536 server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
537 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("")}, 100, 0, 5))
539 test := newDaemonMessageTest(t, t.TempDir(), server)
540 child := test.agents[1]
541 at := child.clock().Add(time.Minute).Format(scheduleAtLayout)
542 output := test.loops[1].tools.run(t.Context(), "schedule_set", `{"text":"Check the plants.","at":"`+at+`","cron":null}`)
543 require.Contains(t, output, "Schedule 1 set.")
544 require.Equal(t, "Garden", child.schedules.sorted()[0].Owner)
546 child.schedules.clock = func() time.Time {
547 return child.clock().Add(time.Minute)
549 require.ErrorContains(t, child.schedules.deliver(t.Context(), test.loops[0]), "does not match")
550 require.NoError(t, child.schedules.deliver(t.Context(), test.loops[1]))
551 test.Start(t, 0, 1, 2)
552 test.events[1].WaitFor(t, "idle", 1)
553 require.Equal(t, 1, server.Calls())
554 require.Equal(t, child.agent.CacheKey(), server.Call(0).body["prompt_cache_key"])
555 require.Zero(t, test.agents[0].memory.Len())
556 require.Zero(t, test.agents[2].memory.Len())
557 require.Contains(t, test.events[1].LastEvent("work").Text, "Check the plants.")
558 require.Equal(t, scheduleSource, test.events[1].LastEvent("work").Source)