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.
186 lines
7.1 KiB
Go
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")
|
|
}
|
|
}
|