asks: the proof sees what the phone shows at a tap, and the question's outcome as its title (hq issue 372)
mesh/merge-gate error: the check could not run: the base branch proofs/asks-answered-on-the-phone cannot be read, and the check it holds judges the change:…
mesh/repo-check error: the check could not run: the base branch proofs/asks-answered-on-the-phone cannot be read, and the check it holds judges the change:…
mesh/delivery rejected: the gate failed or could not run, or the repository's own check failed

The operator approved on Telegram and saw nothing: the tap was acknowledged
with no words and the question kept its title. The proof now checks the toast
says the approval was recorded, no earlier than the warrant; that the edited
question opens with how it ended; and that the same answer tapped again is
said as already recorded, not refused.
This commit is contained in:
jochen
2026-10-10 14:12:45 +02:00
parent a10aeb9843
commit 7289edb276
2 changed files with 54 additions and 5 deletions
+37 -1
View File
@@ -25,10 +25,17 @@ type fakeTelegram struct {
arrived chan struct{}
sent []tgMessage
msgID int64
taps int // answerCallbackQuery calls
taps int // answerCallbackQuery calls
toasts []toast // what each acknowledgement showed on the phone (issue 372)
deleted int
}
// toast is a tap's acknowledgement: the short line the phone shows over the chat, and when.
type toast struct {
Text string
At time.Time
}
// tgMessage is one message the bot sent or edited: its chat, its id, its words and its buttons (label →
// callback data).
type tgMessage struct {
@@ -108,9 +115,12 @@ 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":
text, _ := body["text"].(string)
f.mu.Lock()
f.taps++
f.toasts = append(f.toasts, toast{Text: text, At: time.Now()})
f.mu.Unlock()
f.t.Logf("phone: a tap acknowledged with %q", text)
ok(true)
case "deleteMessage":
f.mu.Lock()
@@ -280,3 +290,29 @@ func firstLine(s string) string {
}
return s
}
// waitForToast waits for a tap's acknowledgement since a time that shows what match wants, and fails saying
// what the phone showed instead.
func (f *fakeTelegram) waitForToast(what string, since time.Time, within time.Duration, match func(string) bool) toast {
f.t.Helper()
deadline := time.Now().Add(within)
for {
f.mu.Lock()
var seen []string
for _, t := range f.toasts {
if t.At.Before(since) {
continue
}
if match(t.Text) {
f.mu.Unlock()
return t
}
seen = append(seen, fmt.Sprintf("%q", t.Text))
}
f.mu.Unlock()
if time.Now().After(deadline) {
f.t.Fatalf("the phone never showed %s; it showed: %s", what, strings.Join(seen, ", "))
}
time.Sleep(100 * time.Millisecond)
}
}
+17 -4
View File
@@ -161,9 +161,19 @@ func TestTheOperatorsAnswerEndToEnd(t *testing.T) {
if got := a.performed(); len(got) != 1 || got[0]["arg.switch"] != "approve" {
t.Fatalf("performed %v, want the one bound act once", got)
}
// Every copy says how it ended, its buttons gone.
l.tg.waitFor("the question edited to its outcome", operatorAccount, tapped, 90*time.Second,
// At the tap, the phone says it was recorded, and never before the warrant was (issue 372).
ack := l.tg.waitForToast("the approval confirmed", tapped, 30*time.Second, func(s string) bool {
return strings.HasPrefix(s, "You chose Approve: recorded")
})
if ack.At.Before(w.At) {
t.Errorf("the phone said recorded at %s, before the warrant at %s", ack.At, w.At)
}
// Every copy says how it ended — as its title, read at a glance — its buttons gone.
edited := l.tg.waitFor("the question edited to its outcome", operatorAccount, tapped, 90*time.Second,
func(m tgMessage) bool { return m.Edited && m.ID == firstMessage.ID && len(m.Buttons) == 0 })
if !strings.HasPrefix(edited.Text, "You chose Approve at ") || !strings.Contains(firstLine(edited.Text), " on Telegram · ") {
t.Errorf("the question does not open with how it ended: %q", firstLine(edited.Text))
}
// And the router's record holds who approved what.
e, _ := l.routerRecord("messenger_asks", "lab-asker.a1")
var rec struct {
@@ -181,8 +191,11 @@ func TestTheOperatorsAnswerEndToEnd(t *testing.T) {
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") })
// The same answer tapped again is what was recorded: the phone says so, and nothing more is done (issue 372:
// a refusal here read as the approval failing).
l.tg.waitForToast("the replayed tap said as already recorded", tapped, 30*time.Second, func(s string) bool {
return strings.HasPrefix(s, "You already chose Approve at ") && strings.HasSuffix(s, ": recorded.")
})
time.Sleep(2 * time.Second)
if n := a.wordsOn("a1"); n != 1 {
t.Errorf("the router said %d words on one ask", n)