X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/a5c83e92d7feea2e104b5515043a76c8c4c56c60..e86df5fad9aa2f280aad845adeefd239b0e944ae:/daemon_message_test.go?ds=inline diff --git a/daemon_message_test.go b/daemon_message_test.go index 432a640..a7792e8 100644 --- a/daemon_message_test.go +++ b/daemon_message_test.go @@ -24,6 +24,7 @@ func TestParentMessageWakesChildAndFinalReplyWakesParent(t *testing.T) { modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem(text)}, 100, 0, 5)) } + test := newDaemonMessageTest(t, t.TempDir(), server) test.Start(t, 0, 1, 2) require.NoError(t, test.messages.Tell("OneChat", "Garden", "Check the gate size.")) @@ -68,6 +69,7 @@ func TestChildReportSendsATelegramCompletionBeforeTheParentReply(t *testing.T) { modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem(text)}, 100, 0, 5)) } + test := newDaemonMessageTest(t, t.TempDir(), server) telegram := newTelegramFake(t) bot := newTelegramBot(newTelegramAPI(telegram.URL, telegramTestToken), telegramTestOwner, filepath.Join(t.TempDir(), telegramOffsetFileName), discardLog()) @@ -97,6 +99,7 @@ func TestChildReportSendsATelegramCompletionBeforeTheParentReply(t *testing.T) { require.Eventually(t, func() bool { return len(telegram.Sent()) >= 2 }, 10*time.Second, time.Millisecond) + require.Equal(t, []string{"[Garden] finished", "I will ask the user."}, telegram.Sent()) require.Equal(t, subagentFinished, test.events[0].LastEvent("work").Status) require.Equal(t, "[Garden] Approve the gate size.", test.events[0].Last("work")) @@ -122,20 +125,22 @@ func TestEmptyChildReplySendsNothingAndWakesNobody(t *testing.T) { text string }{ { - name: "empty final reply", + name: "sends nothing for an empty final reply", text: "", }, { - name: "whitespace final reply", + name: "sends nothing for a whitespace final reply", text: " \n\t ", }, } + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { server := newModelFake(t) server.reply = func(w http.ResponseWriter, _ ModelFakeCall) { modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem(tc.text)}, 100, 0, 5)) } + test := newDaemonMessageTest(t, t.TempDir(), server) test.Start(t, 0, 1, 2) require.NoError(t, test.messages.Tell("OneChat", "Garden", "Keep the plan.")) @@ -162,6 +167,7 @@ func TestBusyChildQueuesTheNextParentMessage(t *testing.T) { modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("")}, 100, 0, 5)) } + test := newDaemonMessageTest(t, t.TempDir(), server) test.Start(t, 0, 1) require.NoError(t, test.messages.Tell("OneChat", "Garden", "Check the first plan.")) @@ -194,6 +200,7 @@ func TestBusyParentQueuesItsChildReport(t *testing.T) { modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem(text)}, 100, 0, 5)) } + test := newDaemonMessageTest(t, t.TempDir(), server) test.Start(t, 0, 1) @@ -232,6 +239,7 @@ func TestChildReportsOnlyTheFinalReplyAndTakesQueuedToolInput(t *testing.T) { modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("The plan needs approval.")}, 100, 0, 5)) } + test := newDaemonMessageTest(t, t.TempDir(), server) test.Start(t, 1) require.NoError(t, test.messages.Tell("OneChat", "Garden", "Check the plan.")) @@ -259,6 +267,7 @@ func TestQueuedParentMessageSurvivesDaemonRestart(t *testing.T) { server.reply = func(w http.ResponseWriter, _ ModelFakeCall) { modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("")}, 100, 0, 5)) } + first := newDaemonMessageTest(t, dir, server) require.NoError(t, first.messages.Tell("OneChat", "Garden", "Remember the garden plan.")) require.Equal(t, 1, first.agents[1].memory.Len()) @@ -319,6 +328,7 @@ func TestInterruptedChildDeliveryResumesAfterDaemonRestart(t *testing.T) { modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("")}, 100, 0, 5)) } + first := newDaemonMessageTest(t, dir, server) first.Start(t, 1) require.NoError(t, first.messages.Tell("OneChat", "Garden", "Finish the garden plan.")) @@ -348,6 +358,7 @@ func TestInterruptedParentReportResumesAfterDaemonRestart(t *testing.T) { modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("The plan is ready.")}, 100, 0, 5)) } + first := newDaemonMessageTest(t, dir, server) first.Start(t, 0, 1) require.NoError(t, first.messages.Tell("OneChat", "Garden", "Review the plan.")) @@ -419,6 +430,7 @@ func TestAgentMessageSnapshotBlocksDelivery(t *testing.T) { require.Never(t, func() bool { return len(sent) > 0 }, 50*time.Millisecond, time.Millisecond) + once.Do(func() { close(release) }) @@ -453,16 +465,17 @@ func TestParentCanMessageOnlyItsOwnChildren(t *testing.T) { target: "OneChat", }, { - name: "unknown sender", + name: "refuses an unknown sender", source: "Other", target: "Garden", }, { - name: "unknown recipient", + name: "refuses an unknown recipient", source: "OneChat", target: "Other", }, } + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { require.ErrorContains(t, test.messages.Tell(tc.source, tc.target, "Do not deliver."), "not a child") @@ -481,22 +494,23 @@ func TestInvalidAgentMessageQueueStopsStartup(t *testing.T) { file string }{ { - name: "broken JSON", + name: "stops startup for broken JSON", file: "{", }, { - name: "unknown sender", + name: "stops startup for unknown sender", file: `{"next":1,"pending":[{"id":1,"source":"Other","target":"Garden","kind":"user"}]}`, }, { - name: "sibling delivery", + name: "stops startup for sibling delivery", file: `{"next":1,"pending":[{"id":1,"source":"Library","target":"Garden","kind":"user"}]}`, }, { - name: "invalid sequence", + name: "stops startup for invalid sequence", file: `{"next":0,"pending":[{"id":1,"source":"OneChat","target":"Garden","kind":"user"}]}`, }, } + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { dir := t.TempDir() @@ -537,6 +551,7 @@ func TestChildScheduleWakesOnlyItsOwner(t *testing.T) { server.reply = func(w http.ResponseWriter, _ ModelFakeCall) { modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("")}, 100, 0, 5)) } + test := newDaemonMessageTest(t, t.TempDir(), server) child := test.agents[1] at := child.clock().Add(time.Minute).Format(scheduleAtLayout) @@ -565,14 +580,14 @@ func TestRoutedWhatsAppBatchesWakeOnlyTheirChild(t *testing.T) { chat WhatsAppChat }{ { - name: "private chat", + name: "routes a private chat only to its child", chat: WhatsAppChat{ ID: whatsappTestAna.String(), Name: "Ana", }, }, { - name: "group without a mention", + name: "routes an unmentioned group only to its child", chat: WhatsAppChat{ ID: whatsappTestFamily.String(), Name: "Garden plans", @@ -580,12 +595,14 @@ func TestRoutedWhatsAppBatchesWakeOnlyTheirChild(t *testing.T) { }, }, } + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { server := newModelFake(t) server.reply = func(w http.ResponseWriter, _ ModelFakeCall) { modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("")}, 100, 0, 5)) } + dir := t.TempDir() test := newDaemonMessageTest(t, dir, server) inbox := whatsappRouteTestInbox(t, dir) @@ -621,6 +638,7 @@ func TestWhatsAppUnrouteRestoresRootDelivery(t *testing.T) { ID: whatsappTestAna.String(), Name: "Ana", } + source := "whatsapp:" + chat.ID require.NoError(t, test.messages.routes.Set(source, "Garden")) whatsappRouteTestAdd(t, inbox, chat, "Keep the gate plan.") @@ -646,6 +664,7 @@ func TestMissingWhatsAppRouteChildFallsBackToRoot(t *testing.T) { ID: whatsappTestAna.String(), Name: "Ana", } + require.NoError(t, test.messages.routes.Set("whatsapp:"+chat.ID, "Missing")) whatsappRouteTestAdd(t, inbox, chat, "Please check the plan.") require.NoError(t, inbox.deliver(t.Context(), test.loops[0], WhatsAppLink{}, true)) @@ -672,6 +691,7 @@ func TestRoutedWhatsAppRestartDoesNotDeliverTheBatchTwice(t *testing.T) { finish: true, }, } + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { dir := t.TempDir() @@ -679,12 +699,14 @@ func TestRoutedWhatsAppRestartDoesNotDeliverTheBatchTwice(t *testing.T) { server.reply = func(w http.ResponseWriter, _ ModelFakeCall) { modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("")}, 100, 0, 5)) } + first := newDaemonMessageTest(t, dir, server) inbox := whatsappRouteTestInbox(t, dir) chat := WhatsAppChat{ ID: whatsappTestAna.String(), Name: "Ana", } + source := "whatsapp:" + chat.ID require.NoError(t, first.messages.routes.Set(source, "Garden")) whatsappRouteTestAdd(t, inbox, chat, "Remember the gate plan.") @@ -739,14 +761,15 @@ func TestQueuedWhatsAppBatchRecoversBeforeItsLogAppend(t *testing.T) { id *int }{ { - name: "before log ID is stored", + name: "recovers one message before its log ID is stored", id: nil, }, { - name: "after log ID is stored", + name: "recovers one message after its log ID is stored", id: new(0), }, } + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { dir := t.TempDir() @@ -768,6 +791,7 @@ func TestQueuedWhatsAppBatchRecoversBeforeItsLogAppend(t *testing.T) { WhatsAppBatch: 1, }}, } + data, err := json.Marshal(file) require.NoError(t, err) require.NoError(t, os.WriteFile(path, data, 0o600)) @@ -796,6 +820,7 @@ func TestQueuedWhatsAppBatchUsesRootIfItsChildIsRemoved(t *testing.T) { ID: whatsappTestAna.String(), Name: "Ana", } + require.NoError(t, first.messages.routes.Set("whatsapp:"+chat.ID, "Garden")) whatsappRouteTestAdd(t, inbox, chat, "Keep the gate plan.") require.NoError(t, inbox.deliver(t.Context(), first.loops[0], WhatsAppLink{}, true)) @@ -824,11 +849,13 @@ func TestRoutedWhatsAppBatchesCanCloseOutOfNumberOrder(t *testing.T) { ID: whatsappTestAna.String(), Name: "Ana", } + second := WhatsAppChat{ ID: whatsappTestFamily.String(), Name: "Garden plans", Group: true, } + require.NoError(t, test.messages.routes.Set("whatsapp:"+first.ID, "Garden")) require.NoError(t, test.messages.routes.Set("whatsapp:"+second.ID, "Garden")) @@ -867,14 +894,17 @@ func TestWhatsAppCatchupSeparatesChildAndRootBatches(t *testing.T) { Name: "Garden plans", Group: true, } + library := WhatsAppChat{ ID: whatsappTestBruno.String(), Name: "Bruno", } + root := WhatsAppChat{ ID: whatsappTestAna.String(), Name: "Ana", } + require.NoError(t, test.messages.routes.Set("whatsapp:"+garden.ID, "Garden")) require.NoError(t, test.messages.routes.Set("whatsapp:"+library.ID, "Library")) inbox.waitForCatchup() @@ -911,6 +941,7 @@ func TestRoutedWhatsAppQueueFailureKeepsTheInboxBatch(t *testing.T) { ID: whatsappTestAna.String(), Name: "Ana", } + require.NoError(t, first.messages.routes.Set("whatsapp:"+chat.ID, "Garden")) require.NoError(t, os.Mkdir(first.messages.path, 0o700)) whatsappRouteTestAdd(t, inbox, chat, "Keep the gate plan.")