messenger, telegram: a second tap on the recorded answer is said as recorded, and the fallback claims nothing (issue 372 review)
mesh/merge-gate pass: builds messenger, telegram → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request

The review found that a double tap while the phone's spinner turned would be
answered 'That answer was not taken', reading as if the approval failed; it is
now 'You already chose Approve at 13:30: recorded.' and does nothing more. The
fallback toast no longer promises a change that may not come (a stranger's
tap), the wait outlasts the router's 10 s wait for the controller's root word,
the channel's own refusals stay in the chat too, and the store's error words
stay in the journal.
This commit is contained in:
jochen
2026-10-10 14:05:41 +02:00
parent 020c05fe65
commit 35c4de7d08
6 changed files with 75 additions and 12 deletions
@@ -387,10 +387,17 @@ func TestAnApproveFromTheLinkedAccountIsAWarrantForTheAskerAlone(t *testing.T) {
if out, _ := h.Chosen("telegram", choiceOf(release, "42", c.now())); out != "chosen" {
t.Errorf("a redelivery of the same answer was acted on again: %s", out)
}
// The same answer from the same account, as a new tap, is what was recorded: said so, nothing more done
// (issue 372). From another account it is refused.
again := choiceOf(release, "42", c.now())
again.ID = "another"
if out, err := h.Chosen("telegram", again); err == nil || out != "refused" {
t.Errorf("a used ticket was taken: %s", out)
if out, err := h.Chosen("telegram", again); err != nil || out != "chosen" ||
!strings.HasPrefix(tg.sends[len(tg.sends)-1].Title, "You already chose Release at ") {
t.Errorf("the same answer again was not said as recorded: %s %v %q", out, err, tg.sends[len(tg.sends)-1].Title)
}
stranger := choiceOf(release, "666", c.now())
if out, err := h.Chosen("telegram", stranger); err == nil || out != "refused" {
t.Errorf("a used ticket was taken from another account: %s", out)
}
stop := ticketFor(tg.sends[0], "Stop")
if out, err := h.Chosen("telegram", choiceOf(stop, "42", c.now())); err == nil || !strings.Contains(err.Error(), "already answered") {
+18 -1
View File
@@ -755,6 +755,13 @@ func (h *Holder) confirmChoice(kind string, c asks.Choice, label, asker string,
h.record(Sent{At: h.Now(), Channel: kind, Key: "(answer)", What: "confirmed " + label, Outcome: outcomeOf(err)})
}
// confirmAgain says, in reply to the same answer given again, that it was already recorded.
func (h *Holder) confirmAgain(kind string, c asks.Choice, label string, at time.Time) {
err := h.showOn(kind, Message{Title: "You already chose " + label + " at " + at.In(h.zone()).Format("15:04") + ": recorded.",
Quiet: true, Reply: c.ID})
h.record(Sent{At: h.Now(), Channel: kind, Key: "(answer)", What: "confirmed again " + label, Outcome: outcomeOf(err)})
}
// sweepAsks ends asks past their expiry, and tells again an asker not yet told. Called every minute.
func (h *Holder) sweepAsks(now time.Time) {
h.mu.Lock()
@@ -865,6 +872,14 @@ func (h *Holder) judge(kind string, c asks.Choice, key string, root func(KindInf
}
now := h.Now()
t, issued := rec.Tickets[c.Ticket]
if w := rec.Warrant; issued && rec.State == string(asks.OutcomeChosen) && w != nil && w.Option == t.Option &&
w.By != nil && w.By.Kind == kind && w.By.Identity == c.Sender.Identity && (c.Sender.Authenticated || !h.Bench.Promises(kind, "verified-sender")) {
// The same answer again from the same account — a second tap while the first was being confirmed, or one
// redelivered after a restart: it is what was recorded, so it is said so, not refused as if it failed
// (issue 372). Nothing more is done.
h.confirmAgain(kind, c, w.Label, rec.Closed)
return "chosen", nil
}
switch {
case !issued:
return h.refuseChoice(kind, c, "it belongs to no ask this router holds")
@@ -948,7 +963,9 @@ func (h *Holder) judge(kind string, c asks.Choice, key string, root func(KindInf
if errors.Is(err, ErrChanged) {
return h.refuseChoice(kind, c, "the question was already answered")
}
return h.refuseChoice(kind, c, "it could not be recorded: "+err.Error())
// The store's own words stay in the journal: they are not the operator's to read on the phone.
h.Logf("[messenger] the answer to %s's ask %s could not be recorded: %v", rec.Asker, rec.Ask.ID, err)
return h.refuseChoice(kind, c, "it could not be recorded")
}
h.record(Sent{At: now, Channel: kind, Key: rec.Record, What: "answer " + o.Label, Outcome: "warrant to " + rec.Asker})
h.Logf("[messenger] %s: a warrant for %s's ask %s", w.Says(), rec.Asker, rec.Ask.ID)
@@ -136,10 +136,10 @@ func TestARefusedAnswerSaysWhyInReplyToTheTap(t *testing.T) {
settle(h, c)
release := ticketFor(tg.sends[0], "Release")
_, _ = h.Chosen("telegram", choiceOf(release, "42", c.now()))
again := choiceOf(release, "42", c.now())
again := choiceOf(ticketFor(tg.sends[0], "Stop"), "42", c.now())
again.ID = "tg-tap-again"
if out, _ := h.Chosen("telegram", again); out != "refused" {
t.Fatalf("a used ticket was taken: %s", out)
t.Fatalf("a second answer to an answered question was taken: %s", out)
}
last := tg.sends[len(tg.sends)-1]
if last.Reply != "tg-tap-again" || last.Title != "That answer was not taken" ||
@@ -150,3 +150,36 @@ func TestARefusedAnswerSaysWhyInReplyToTheTap(t *testing.T) {
t.Error("the refusal carries what may not leave")
}
}
// A second tap on the answer just recorded — the phone's spinner still turning, or a redelivery after a restart
// — is said as recorded, not refused as if it failed, and nothing more is done (issue 372).
func TestTheSameAnswerTappedTwiceIsSaidAsRecorded(t *testing.T) {
h, tg, _, c, store, w := newRouter(t)
link(h, store, c, "42", c.now().Add(-2*time.Hour))
submit(t, h, "mesh-delivery", heldAsk(c, "d-1"))
settle(h, c)
release := ticketFor(tg.sends[0], "Release")
first := choiceOf(release, "42", c.now())
first.ID = "tg-tap-1"
if out, err := h.Chosen("telegram", first); err != nil || out != "chosen" {
t.Fatalf("%s %v", out, err)
}
edits := len(tg.edits)
second := choiceOf(release, "42", c.now())
second.ID = "tg-tap-2"
if out, err := h.Chosen("telegram", second); err != nil || out != "chosen" {
t.Fatalf("the second tap: %s %v", out, err)
}
last := tg.sends[len(tg.sends)-1]
if last.Reply != "tg-tap-2" || last.Title != "You already chose Release at 12:00: recorded." {
t.Errorf("the second tap was answered %+v", last)
}
for _, m := range tg.sends {
if m.Title == "That answer was not taken" {
t.Errorf("a tap on the answer recorded was refused: %+v", m)
}
}
if len(w.of("mesh-delivery")) != 1 || len(tg.edits) != edits {
t.Error("the second tap did something more")
}
}