diff --git a/asks/faketelegram_test.go b/asks/faketelegram_test.go index 1ad1a1d..15fadd9 100644 --- a/asks/faketelegram_test.go +++ b/asks/faketelegram_test.go @@ -27,6 +27,11 @@ type fakeTelegram struct { msgID int64 taps int // answerCallbackQuery calls deleted int + // toasts are the words a tap's acknowledgement showed over the chat (answerCallbackQuery), by the chat the + // tap came from: what the phone shows the tapper and never puts in the chat. Kept apart from sent, so a + // chat's messages stay what the chat holds. + toasts []tgMessage + tappedIn map[string]int64 // a tap's callback id to the chat it came from } // tgMessage is one message the bot sent or edited: its chat, its id, its words and its buttons (label → @@ -108,9 +113,18 @@ func (f *fakeTelegram) serve(w http.ResponseWriter, r *http.Request) { f.t.Logf("phone: %s to %d (#%d): %q %v", method, chat, m.ID, firstLine(text), m.Buttons) ok(map[string]any{"message_id": m.ID, "chat": map[string]any{"id": chat, "type": "private"}, "text": text}) case "answerCallbackQuery": + id, _ := body["callback_query_id"].(string) + text, _ := body["text"].(string) f.mu.Lock() f.taps++ + chat, known := f.tappedIn[id] + if known && text != "" { + f.toasts = append(f.toasts, tgMessage{Chat: chat, Text: text, At: time.Now()}) + } f.mu.Unlock() + if text != "" { + f.t.Logf("phone: answerCallbackQuery %s in %d: %q", id, chat, firstLine(text)) + } ok(true) case "deleteMessage": f.mu.Lock() @@ -152,7 +166,14 @@ func (f *fakeTelegram) tapped(from int64, chat int64, chatType string, onMessage // tappedOn is a tap on a message showing the words given: what a message changed under the operator's thumb, // or forwarded and edited elsewhere, carries. func (f *fakeTelegram) tappedOn(from int64, chat int64, chatType string, onMessage int64, data, text string) { - f.push(map[string]any{"callback_query": map[string]any{"id": fmt.Sprintf("cb-%d", f.nextID()), "data": data, + id := fmt.Sprintf("cb-%d", f.nextID()) + f.mu.Lock() + if f.tappedIn == nil { + f.tappedIn = map[string]int64{} + } + f.tappedIn[id] = chat + f.mu.Unlock() + f.push(map[string]any{"callback_query": map[string]any{"id": id, "data": data, "from": map[string]any{"id": from, "is_bot": false, "first_name": fmt.Sprintf("Account %d", from)}, "message": map[string]any{"message_id": onMessage, "text": text, "chat": map[string]any{"id": chat, "type": chatType}}}}) @@ -197,6 +218,26 @@ func (f *fakeTelegram) waitFor(what string, chat int64, since time.Time, within return tgMessage{} } +// toldTapper waits for words the phone showed in a chat at or after a time, as a message in it or as a tap's +// acknowledgement over it: where the channel says the router's word on a tap (issue 372). +func (f *fakeTelegram) toldTapper(what string, chat int64, since time.Time, within time.Duration, match func(string) bool) string { + f.t.Helper() + deadline := time.Now().Add(within) + for time.Now().Before(deadline) { + f.mu.Lock() + for _, m := range append(append([]tgMessage{}, f.sent...), f.toasts...) { + if m.Chat == chat && !m.At.Before(since) && match(m.Text) { + f.mu.Unlock() + return m.Text + } + } + f.mu.Unlock() + time.Sleep(100 * time.Millisecond) + } + f.t.Fatalf("the phone never showed %s (chat %d) within %s; it showed:\n%s", what, chat, within, f.transcript()) + return "" +} + // since is every message to a chat sent at or after a time. func (f *fakeTelegram) since(chat int64, at time.Time) []tgMessage { f.mu.Lock() @@ -218,6 +259,9 @@ func (f *fakeTelegram) transcript() string { fmt.Fprintf(&b, " %s to %d #%d edited=%v %q %v\n", m.At.Format("15:04:05.000"), m.Chat, m.ID, m.Edited, firstLine(m.Text), m.Buttons) } + for _, m := range f.toasts { + fmt.Fprintf(&b, " %s to %d tap acknowledged %q\n", m.At.Format("15:04:05.000"), m.Chat, firstLine(m.Text)) + } return b.String() } diff --git a/asks/proof_test.go b/asks/proof_test.go index 19feac0..1343f10 100644 --- a/asks/proof_test.go +++ b/asks/proof_test.go @@ -178,11 +178,15 @@ func TestTheOperatorsAnswerEndToEnd(t *testing.T) { }) // --- 4. A replayed tap, and a redelivered warrant, do nothing more. + // The replay is the operator's own answer again: the router says it is the one already recorded (on the tap's + // acknowledgement, issue 372), not refused as if the approval failed, and it is no second warrant or act. t.Run("a replayed tap and a redelivered warrant do nothing more", func(t *testing.T) { tapped := time.Now() l.tg.tapped(operatorAccount, operatorAccount, "private", firstMessage.ID, button(firstMessage, "Approve")) - l.tg.waitFor("the refusal of a replayed tap", operatorAccount, tapped, 90*time.Second, - func(m tgMessage) bool { return strings.Contains(m.Text, "That answer was not taken") }) + l.tg.toldTapper("that a replayed tap is the answer already recorded", operatorAccount, tapped, 90*time.Second, + func(text string) bool { + return strings.Contains(text, "You already chose Approve") && strings.Contains(text, "recorded") + }) time.Sleep(2 * time.Second) if n := a.wordsOn("a1"); n != 1 { t.Errorf("the router said %d words on one ask", n)