})
telegramWaitForSent(t, telegram, "Error: interrupted", "No turn runs.")
+
+ telegram.Add(TelegramUpdate{
+ UpdateID: 4,
+ Message: telegramOwnerMessage(stopCommand + " BlueFox"),
+ })
+
+ telegramWaitForSent(t, telegram, "No turn runs.", "No subagent BlueFox runs.")
+}
+
+func TestBotShowsOnlyTheStartAndTheEndOfASubagent(t *testing.T) {
+ telegram := newTelegramFake(t)
+ server := newModelFake(t)
+ server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
+ last := subagentTestLastItem(call)
+ switch {
+ case subagentTestCall(call):
+ // The report comes after the reply of the turn of the spawn.
+ require.Eventually(t, func() bool {
+ return slices.Contains(telegram.Sent(), "Started.")
+ }, 10*time.Second, time.Millisecond)
+ modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("There are 3 files.")}, 100, 0, 5))
+ case last["type"] == "function_call_output":
+ modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Started.")}, 100, 0, 5))
+ case strings.HasPrefix(subagentTestText(last), "["):
+ modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("It counted 3 files.")}, 100, 0, 5))
+ default:
+ modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "spawn", `{"tasks":["Count the files."],"effort":null}`)}, 100, 0, 5))
+ }
+ }
+
+ bot, _ := telegramStartTestBot(t, telegram)
+ daemonStartTestWithBot(t, server, bot)
+ telegram.Add(TelegramUpdate{
+ UpdateID: 1,
+ Message: telegramOwnerMessage("Count the files in a subagent."),
+ })
+
+ require.Eventually(t, func() bool {
+ return slices.Contains(telegram.Sent(), "It counted 3 files.")
+ }, 10*time.Second, time.Millisecond)
+
+ sent := telegram.Sent()
+ name := strings.TrimSuffix(strings.TrimPrefix(sent[0], "["), "] started")
+ require.True(t, subagentNameValid(name), sent[0])
+ require.Equal(t, []string{
+ "[" + name + "] started",
+ "Started.",
+ "[" + name + "] finished",
+ "It counted 3 files.",
+ }, sent)
}
func TestBotStartsAtTheStoredOffset(t *testing.T) {
}{
{
name: "rich message escapes the tag outside code",
- sent: []string{"a \\<details> tag and `<b>`"},
+ sent: []string{"a <details> & tag and `<b>`"},
},
{
name: "entity fallback shows the tag as written",
status: http.StatusBadRequest,
- sent: []string{"a <details> tag and <b>"},
+ sent: []string{"a <details> & tag and <b>"},
},
}
bot, _ := telegramStartTestBot(t, telegram)
bot.post(t.Context(), Event{
Kind: "reply",
- Text: "a <details> tag and `<b>`",
+ Text: "a <details> & tag and `<b>`",
})
require.Equal(t, tc.sent, telegram.Sent())
}
}
+func TestRefusedRichPartGoesWithTheTextAsWritten(t *testing.T) {
+ paragraph := "A <b> & c.\n\n"
+ reply := strings.Repeat(paragraph, telegramRichBytes/len(paragraph))
+
+ telegram := newTelegramFake(t)
+ refused := false
+ telegram.fail = func(call TelegramFakeCall) (int, string) {
+ if call.method != "sendRichMessage" || refused {
+ return 0, ""
+ }
+
+ refused = true
+
+ return http.StatusBadRequest, "Bad Request"
+ }
+
+ bot, _ := telegramStartTestBot(t, telegram)
+ bot.post(t.Context(), Event{
+ Kind: "reply",
+ Text: reply,
+ })
+
+ var fallback, rich []string
+ for _, call := range telegram.Calls() {
+ switch {
+ case !call.ok:
+ case call.method == "sendMessage":
+ fallback = append(fallback, call.params["text"].(string))
+ case call.method == "sendRichMessage":
+ rich = append(rich, call.params["rich_message"].(map[string]any)["markdown"].(string))
+ }
+ }
+
+ require.Greater(t, len(fallback), 1)
+ require.Len(t, rich, 1)
+ require.True(t, strings.HasPrefix(reply, strings.Join(fallback, "")), "fallback text is not the reply as written")
+
+ for _, text := range fallback {
+ require.LessOrEqual(t, telegramUTF16Units(text), telegramMessageUnits)
+ }
+
+ require.True(t, reply == strings.Join(fallback, "")+telegramUnescapeTags(rich[0]), "reply is not sent once and whole")
+}
+
func TestRefusedEntitiesGoAgainAsPlainText(t *testing.T) {
telegram := newTelegramFake(t)
telegram.fail = func(call TelegramFakeCall) (int, string) {
limit: 3,
parts: []string{"ab", "\u00e9c", "d"},
},
+ {
+ name: "long line is not cut inside an entity",
+ markdown: "abc&def",
+ limit: 6,
+ parts: []string{"abc", "&d", "ef"},
+ },
}
for _, tc := range testCases {