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) } } }