The operator stopped believing the mesh's notifications: one problem stood twice on the desk (a reminder beside the message it followed, an escalation then a reminder 31 s later), a unit failing again at each apply was said again 22 times, and a question's buttons outlived it on a reminder the ask's end never reached. - messenger: a reminder or escalation is said in place of the problem's message (Replace under its handle), alone, never as a digest line; a problem first said in a digest gets its own message next time. A reminder counts from when it was last said. A clearing is said once it stays cleared 2 min (10 min once it came back); back before that, the message continues without a word. Cleared before ever said and back: said. An ask's expiry is said with its day. - desk-channel: a send or show under a handle already shown replaces its notification by the notifier's replace id. - telegram: the same deletes the earlier message and sends the new one, ringing as its kind rings; one it cannot delete loses its buttons.
213 lines
7.4 KiB
Go
213 lines
7.4 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Issue 371: tapping Details on any but the newest message about a problem was refused "it belongs to no
|
|
// open message", because each new message replaced the ticket of the one before it. Every message still
|
|
// shown keeps a Details of its own, used once, and Details never acts.
|
|
|
|
// shownOn is every message a channel was given: sent, or said again in place of an earlier one (issue 373).
|
|
func shownOn(ch *fakeChannel) []Message {
|
|
out := append([]Message(nil), ch.sends...)
|
|
for _, r := range ch.replaced {
|
|
out = append(out, r.m)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func detailsOf(t *testing.T, m Message) string {
|
|
t.Helper()
|
|
tok := ticketFor(m, DetailsLabel)
|
|
if tok == "" {
|
|
t.Fatalf("no Details on %q: %+v", m.Title, m.Actions)
|
|
}
|
|
return tok
|
|
}
|
|
|
|
// The acceptance of issue 371: three warnings in a row, Details works on each of them, once each.
|
|
func TestDetailsWorksOnEachOfThreeWarningsOnceEach(t *testing.T) {
|
|
for _, same := range []bool{true, false} {
|
|
t.Run(fmt.Sprintf("same problem %v", same), func(t *testing.T) {
|
|
h, tg, _, c, _ := newHolder(t)
|
|
h.Desk = nil
|
|
var toks []string
|
|
if same {
|
|
// One problem said three times: raised, escalated, and reminded.
|
|
k := cond("machine.novox.silent", Warning, "the anchor is silent")
|
|
k.Raised = c.now()
|
|
h.Condition(EventRaised, k)
|
|
settle(h, c)
|
|
k.Severity = Urgent
|
|
h.Condition(EventChanged, k)
|
|
settle(h, c)
|
|
c.pass(RemindUrgent + time.Minute)
|
|
h.Tick()
|
|
settle(h, c)
|
|
} else {
|
|
for _, key := range []string{"machine.ace.silent", "machine.shanks.units", "plan.41.stalled"} {
|
|
k := cond(key, Warning, key+" needs a look")
|
|
k.Raised = c.now()
|
|
h.Condition(EventRaised, k)
|
|
settle(h, c)
|
|
}
|
|
}
|
|
// Issue 373: one problem said again replaces its message rather than standing beside it; each of
|
|
// the three messages still has a Details of its own.
|
|
msgs := shownOn(tg)
|
|
if len(msgs) != 3 {
|
|
t.Fatalf("%d messages, want 3", len(msgs))
|
|
}
|
|
for _, m := range msgs {
|
|
toks = append(toks, detailsOf(t, m))
|
|
}
|
|
if toks[0] == toks[1] || toks[1] == toks[2] || toks[0] == toks[2] {
|
|
t.Fatalf("a ticket is shared: %v", toks)
|
|
}
|
|
// The oldest first, as the operator scrolls back.
|
|
for i, tok := range toks {
|
|
before := len(tg.sends)
|
|
out, err := h.Chosen("telegram", choiceOf(tok, "42", c.now()))
|
|
if err != nil || out != "shown" || len(tg.sends) != before+1 ||
|
|
!strings.HasPrefix(tg.sends[len(tg.sends)-1].Title, "Details: ") {
|
|
t.Fatalf("message %d: Details %q %v", i+1, out, err)
|
|
}
|
|
}
|
|
// Once each: a second tap (a new answer, not a redelivery) is refused in words, and shows nothing.
|
|
for i, tok := range toks {
|
|
c2 := choiceOf(tok, "42", c.now())
|
|
c2.ID += "-again"
|
|
before := len(tg.sends)
|
|
out, err := h.Chosen("telegram", c2)
|
|
if err == nil || !strings.Contains(err.Error(), "already chosen once") ||
|
|
strings.HasPrefix(tg.sends[len(tg.sends)-1].Title, "Details: ") || len(tg.sends) > before+1 {
|
|
t.Errorf("message %d: replayed Details %q %v", i+1, out, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Tickets are bounded: at most MostDetails per record and kind, none older than TokenFor; one pushed out
|
|
// is refused in words that send the operator to the newest message.
|
|
func TestDetailsTicketsAreBoundedInNumberAndTime(t *testing.T) {
|
|
h, tg, _, c, _ := newHolder(t)
|
|
h.Desk = nil
|
|
k := cond("machine.ace.silent", Warning, "the home server is silent")
|
|
k.Raised = c.now()
|
|
h.Condition(EventRaised, k)
|
|
settle(h, c)
|
|
r := h.open[k.Key]
|
|
first := detailsOf(t, tg.sends[0])
|
|
var toks []string
|
|
for i := 0; i < MostDetails; i++ {
|
|
c.pass(time.Second)
|
|
toks = append(toks, ticketFor(Message{Actions: h.choices(r, "telegram")}, DetailsLabel))
|
|
}
|
|
if n := len(r.Tokens); n != MostDetails {
|
|
t.Fatalf("%d tickets kept, want %d", n, MostDetails)
|
|
}
|
|
if _, err := h.Chosen("telegram", choiceOf(first, "42", c.now())); err == nil ||
|
|
!strings.Contains(err.Error(), "tap Details on the newest one") {
|
|
t.Errorf("the pushed-out ticket: %v", err)
|
|
}
|
|
if out, err := h.Chosen("telegram", choiceOf(toks[0], "42", c.now())); err != nil || out != "shown" {
|
|
t.Errorf("the oldest kept ticket: %q %v", out, err)
|
|
}
|
|
// A day on, the rest are refused as old, and forgotten at the next message.
|
|
c.pass(TokenFor + time.Minute)
|
|
if _, err := h.Chosen("telegram", choiceOf(toks[1], "42", c.now())); err == nil ||
|
|
!strings.Contains(err.Error(), "more than a day old") {
|
|
t.Errorf("an expired ticket: %v", err)
|
|
}
|
|
h.choices(r, "desktop")
|
|
if n := len(r.Tokens); n != 1 {
|
|
t.Errorf("%d tickets kept after a day, want the one just made", n)
|
|
}
|
|
}
|
|
|
|
// A change of the condition's actions leaves every shown message its Details: Details names no action.
|
|
func TestDetailsOutlivesAChangeOfTheActions(t *testing.T) {
|
|
h, tg, _, c, _ := newHolder(t)
|
|
h.Desk = nil
|
|
k := cond("machine.ace.silent", Warning, "the home server is silent")
|
|
k.Raised = c.now()
|
|
h.Condition(EventRaised, k)
|
|
settle(h, c)
|
|
tok := detailsOf(t, tg.sends[0])
|
|
k.Actions = []Action{{Label: "Restart", Verb: "node-service-manager.restart"}}
|
|
h.Condition(EventChanged, k)
|
|
if out, err := h.Chosen("telegram", choiceOf(tok, "42", c.now())); err != nil || out != "shown" {
|
|
t.Errorf("Details after the actions changed: %q %v", out, err)
|
|
}
|
|
}
|
|
|
|
// Details is never an approval: chosen on a message whose ask is open, it shows and performs nothing, and
|
|
// the ask stays open with no warrant; an older message's Details does the same.
|
|
func TestDetailsOnAMessageWithAnOpenAskNeverAnswersIt(t *testing.T) {
|
|
h, _, dt, c, store, w := newRouter(t)
|
|
link(h, store, c, "42", c.now().Add(-2*time.Hour))
|
|
k := cond("delivery.hq.held", Warning, "the delivery of hq is held")
|
|
k.Raised = c.now()
|
|
h.Condition(EventRaised, k)
|
|
settle(h, c)
|
|
older := detailsOf(t, dt.sends[0])
|
|
a := ackAsk(c, "d-1")
|
|
a.About = "delivery.hq.held"
|
|
submit(t, h, "mesh-controller", a)
|
|
settle(h, c)
|
|
var shown Message
|
|
for _, m := range dt.edits {
|
|
shown = m
|
|
}
|
|
for _, tok := range []string{older, detailsOf(t, shown)} {
|
|
if out, err := h.Chosen("desktop", choiceOf(tok, "g14", c.now())); err != nil || out != "shown" {
|
|
t.Fatalf("Details: %q %v", out, err)
|
|
}
|
|
}
|
|
if got := w.of("mesh-controller"); len(got) != 0 {
|
|
t.Errorf("Details issued a warrant: %+v", got)
|
|
}
|
|
if st := store.asks["mesh-controller.d-1"].State; st != StateOpen {
|
|
t.Errorf("Details ended the ask: %s", st)
|
|
}
|
|
}
|
|
|
|
// A channel that stays unready makes no ticket at its tries: composing a message makes its Details ticket,
|
|
// and tickets made at every try would push out those of the messages already shown.
|
|
func TestAnUnreadyChannelPushesOutNoShownDetails(t *testing.T) {
|
|
h, tg, _, c, _ := newHolder(t)
|
|
h.Desk = nil
|
|
k := cond("machine.ace.silent", Warning, "the home server is silent")
|
|
k.Raised = c.now()
|
|
h.Condition(EventRaised, k)
|
|
settle(h, c)
|
|
tok := detailsOf(t, tg.sends[0])
|
|
k.Severity = Urgent
|
|
h.Condition(EventChanged, k)
|
|
tg.notReady = fmt.Errorf("telegram is away")
|
|
for i := 0; i < 3*MostDetails; i++ {
|
|
c.pass(2 * time.Minute)
|
|
h.Flush()
|
|
}
|
|
if n := len(h.open[k.Key].Tokens); n != 1 {
|
|
t.Errorf("%d tickets after the tries, want the one shown", n)
|
|
}
|
|
tg.notReady = nil
|
|
c.pass(2 * time.Minute)
|
|
h.Flush()
|
|
msgs := shownOn(tg)
|
|
if len(msgs) != 2 {
|
|
t.Fatalf("%d messages, want 2", len(msgs))
|
|
}
|
|
for _, tk := range []string{tok, detailsOf(t, msgs[1])} {
|
|
if out, err := h.Chosen("telegram", choiceOf(tk, "42", c.now())); err != nil || out != "shown" {
|
|
t.Errorf("Details: %q %v", out, err)
|
|
}
|
|
}
|
|
}
|