Files
mesh-catalog/modules/messenger/cmd/messenger/feedback_test.go
T
jochen 35c4de7d08
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
messenger, telegram: a second tap on the recorded answer is said as recorded, and the fallback claims nothing (issue 372 review)
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.
2026-10-10 14:05:41 +02:00

186 lines
7.1 KiB
Go

package main
// What the operator sees when they answer a question (issue 372): at once, where they answered, that it was
// recorded — never before it was — or why it was not; then the question itself says how it ended, with no
// answers left on it.
import (
"errors"
"strings"
"testing"
"time"
)
// inOrder is a channel that keeps what it was given in one line, sends and edits alike.
type inOrder struct {
*fakeChannel
log *[]string
}
func (o inOrder) Send(m Message) (string, error) {
*o.log = append(*o.log, "send "+m.Title)
return o.fakeChannel.Send(m)
}
func (o inOrder) Edit(id string, m Message) error {
*o.log = append(*o.log, "edit "+m.Title)
return o.fakeChannel.Edit(id, m)
}
// failingAsks is the router's state refusing to record an ask's end.
type failingAsks struct {
*memAsks
err error
}
func (f failingAsks) UpdateAsk(k string, r AskRecord, rev uint64) (uint64, error) {
return 0, f.err
}
func TestAnApprovalIsConfirmedWhereItWasGivenAndThenTheQuestionSaysHowItEnded(t *testing.T) {
h, tg, _, c, store, _ := newRouter(t)
var log []string
h.Away = inOrder{tg, &log}
link(h, store, c, "42", c.now().Add(-2*time.Hour))
submit(t, h, "mesh-delivery", heldAsk(c, "d-1"))
settle(h, c)
log = nil
ch := choiceOf(ticketFor(tg.sends[0], "Release"), "42", c.now())
if out, err := h.Chosen("telegram", ch); err != nil || out != "chosen" {
t.Fatalf("%s %v", out, err)
}
want := []string{
"send You chose Release: recorded, and mesh-delivery was told.",
"edit You chose Release at 12:00 on Telegram · Delivery of hq held for 36 hours",
}
if strings.Join(log, "\n") != strings.Join(want, "\n") {
t.Fatalf("the operator saw, in order:\n%s\nwant:\n%s", strings.Join(log, "\n"), strings.Join(want, "\n"))
}
if confirmed := tg.sends[len(tg.sends)-1]; confirmed.Reply != ch.ID || confirmed.Body != "" {
t.Errorf("the confirmation is not a reply of one line to the tap: %+v", confirmed)
}
// The same tap redelivered is acted on once: nothing more is said, and the question is edited once.
if out, _ := h.Chosen("telegram", ch); out != "chosen" || len(log) != 2 {
t.Errorf("a redelivered tap was said again: %s %q", out, log)
}
}
// A Decline is confirmed as what it is, in the option's own word.
func TestADeclineIsConfirmedInItsOwnWord(t *testing.T) {
h, tg, _, c, store, _ := 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)
if out, err := h.Chosen("telegram", choiceOf(ticketFor(tg.sends[0], "Stop"), "42", c.now())); err != nil || out != "chosen" {
t.Fatalf("%s %v", out, err)
}
if got := tg.sends[len(tg.sends)-1].Title; got != "You chose Stop: recorded, and mesh-delivery was told." {
t.Errorf("confirmed as %q", got)
}
}
// The confirmation never claims what did not happen: an end that could not be recorded is said as refused,
// with why, and nothing says "recorded"; a warrant the bus did not take is said as not yet told.
func TestTheConfirmationNeverClaimsWhatDidNotHappen(t *testing.T) {
t.Run("not recorded", func(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)
h.Asks = failingAsks{store, errors.New("the bucket is away")}
before := len(tg.sends)
out, err := h.Chosen("telegram", choiceOf(ticketFor(tg.sends[0], "Release"), "42", c.now()))
if err == nil || out != "refused" {
t.Fatalf("%s %v", out, err)
}
for _, m := range tg.sends[before:] {
if strings.Contains(m.Title+m.Body, "recorded,") || strings.Contains(m.Title, "You chose") {
t.Errorf("an answer not recorded was confirmed: %+v", m)
}
}
last := tg.sends[len(tg.sends)-1]
if last.Title != "That answer was not taken" || !strings.Contains(last.Body, "could not be recorded") || last.Reply == "" {
t.Errorf("the refusal does not say why, in reply to the tap: %+v", last)
}
if len(w.of("mesh-delivery")) != 0 || len(tg.edits) != 0 {
t.Error("an answer not recorded was told or shown as the question's end")
}
})
t.Run("not told", func(t *testing.T) {
h, tg, _, c, store, _ := 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)
h.Publish = func(subject string, body any, id string) error { return errors.New("the bus is away") }
if out, err := h.Chosen("telegram", choiceOf(ticketFor(tg.sends[0], "Release"), "42", c.now())); err != nil || out != "chosen" {
t.Fatalf("%s %v", out, err)
}
if got := tg.sends[len(tg.sends)-1].Title; got != "You chose Release: recorded; mesh-delivery is told as soon as the bus takes it." {
t.Errorf("confirmed as %q", got)
}
var edited Message
for _, m := range tg.edits {
edited = m
}
if !strings.HasPrefix(edited.Body, "Told to mesh-delivery as soon as the bus takes it") {
t.Errorf("the question claims its asker was told: %q", edited.Body)
}
})
}
// A refused answer says why, in reply to the tap, with nothing claimed done.
func TestARefusedAnswerSaysWhyInReplyToTheTap(t *testing.T) {
h, tg, _, c, store, _ := 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")
_, _ = h.Chosen("telegram", 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 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" ||
!strings.HasSuffix(last.Body, "Nothing was done.") || len(strings.TrimSuffix(last.Body, "Nothing was done.")) < 10 {
t.Errorf("the refusal: %+v", last)
}
if _, ok := Check(last.Text()); !ok {
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")
}
}