asks: a replayed tap is said as the answer already recorded, on its acknowledgement (issue 393)
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript), replays/merge-check.sh (go), asks/merge-check.sh …
mesh/delivery delivered

Since issue 372 the router answers the same answer again from the same
account with 'You already chose Approve at HH:MM: recorded.', on the
tap's acknowledgement rather than in the chat. The proof from #74 still
waited for a chat refusal, so it failed on every mesh-lab pull request.
The lab's phone now keeps a tap's acknowledgement words, and the replay
subtest waits for them; its checks that nothing more is done stay.
This commit is contained in:
jochen
2026-10-10 18:22:22 +02:00
parent 037bc32a6f
commit af0a32a780
2 changed files with 51 additions and 3 deletions
+45 -1
View File
@@ -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()
}
+6 -2
View File
@@ -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)