Merge pull request 'messenger: read a condition's message before it is sent (issue 422)' (#192) from fix/422-a-condition-message-read-before-it-is-sent into main

This commit was merged in pull request #192.
This commit is contained in:
2026-10-10 22:54:34 +00:00
9 changed files with 497 additions and 31 deletions
+1
View File
@@ -183,6 +183,7 @@ ends takes them away everywhere; its expiry is said with its day when it is not
| `messenger_status` | whether the operator can be reached and why not, per channel kind |
| `messenger_recent` | the recent sends, edits, digests, answers and failures |
| `messenger_test` | a test message now, to one kind or every one |
| `messenger_preview` | what a condition's message would say, tracking line and all, without sending it: the words a send would carry, reduced where the content rule refuses its own |
| `messenger_check` | whether some words may leave the mesh |
| `messenger_identities` | the accounts linked as the operator, and from when each approves |
| `messenger_unlink` | unlink a kind's account, said on every channel |
+42 -22
View File
@@ -874,19 +874,9 @@ func (h *Holder) askLines(rec *AskRecord) []string {
// until what remains may leave. The refusal is recorded and stated once per record.
func (h *Holder) words(r *Record, what string) Message {
now := h.Now()
if r.Refused != "" {
// Refused before: its words stay withheld in every later message too.
for w := 1; w <= 3; w++ {
m := h.composeTracked(r, what, now, w)
if _, ok := Check(m.Text()); ok {
return m
}
}
}
m := h.composeTracked(r, what, now, 0)
refusal, ok := Check(m.Text())
if ok {
return m
said, _, refusal, ok := h.allowedWords(r, what, now)
if ok || r.Refused != "" {
return said
}
h.mu.Lock()
r.Refused = refusal.What
@@ -907,26 +897,56 @@ func (h *Holder) words(r *Record, what string) Message {
h.Logf("[messenger] could not state the refusal on the bus: %v", err)
}
}
for w := 1; w <= 3; w++ {
m := h.composeTracked(r, what, now, w)
if _, ok := Check(m.Text()); ok {
return m
}
}
return withheld(refusal.What, r.Severity == Urgent)
return said
}
// composeTracked is compose with the line on the issues that track the condition (tracking.go), so the content
// rule judges the message as it is sent.
func (h *Holder) composeTracked(r *Record, what string, now time.Time, withhold int) Message {
func (h *Holder) composeTracked(r *Record, what string, now time.Time, withhold int) (Message, string) {
m := compose(r, what, now, h.zone(), withhold)
if !h.tracksAsked() || withhold >= 2 {
return m
return m, ""
}
issues, known := h.trackedBy(r.Key)
return withTracking(m, r, what, issues, known, now, h.zone())
}
// allowedWords is the message the content rule leaves of a record: its own words when they may leave the
// mesh, else the same message composed again with less of it until what remains may, else the withheld
// one. It answers the tracking line it carries, what its own words carried, and whether those words are
// what it says. **It changes nothing**: `words` is this walk with the refusal recorded and stated once,
// and `Preview` (preview.go) is this walk alone, so a read of a wording cannot alter what a send says.
func (h *Holder) allowedWords(r *Record, what string, now time.Time) (Message, string, Refusal, bool) {
if r.Refused != "" {
// Refused before: its words stay withheld in every later message too.
for w := 1; w <= 3; w++ {
if m, line := h.composeTracked(r, what, now, w); allowed(m) {
return m, line, Refusal{What: r.Refused}, false
}
}
}
m, line := h.composeTracked(r, what, now, 0)
refusal, ok := Check(m.Text())
if ok {
return m, line, Refusal{}, true
}
// Composed from a copy carrying the reason, because a withheld message says what it carried.
reduced := *r
reduced.Refused = refusal.What
for w := 1; w <= 3; w++ {
if m, line := h.composeTracked(&reduced, what, now, w); allowed(m) {
return m, line, refusal, false
}
}
return withheld(refusal.What, r.Severity == Urgent), "", refusal, false
}
// allowed says whether a message's words may leave the mesh.
func allowed(m Message) bool {
_, ok := Check(m.Text())
return ok
}
// deliver sends a record's message where its severity routes it.
func (h *Holder) channels() []Channel {
@@ -66,6 +66,8 @@ func (f *fakeChannel) Edit(id string, m Message) error {
type memStore struct {
recs map[string]Record
recent []Sent
// puts counts the writes, so a write of bytes identical to what is stored is still seen (issue 422's review).
puts int
}
func (m *memStore) Put(r Record) error {
@@ -73,6 +75,7 @@ func (m *memStore) Put(r Record) error {
m.recs = map[string]Record{}
}
m.recs[r.Key] = r
m.puts++
return nil
}
func (m *memStore) Delete(k string) error { delete(m.recs, k); return nil }
+11
View File
@@ -603,6 +603,17 @@ func tools(h *Holder, l *listening) []stdio.Tool {
"text": str("the words (default: a line saying it is a test)"),
},
Run: func(a map[string]any) (any, error) { return h.Test(strArg(a, "channel"), strArg(a, "text")), nil }},
{Name: "messenger_preview",
Description: "What a condition's message would say, without sending it: the title, the words, the " +
"line on the issues that track it, and whether the content rule would let them leave the mesh — " +
"where they may not, the reduced message a send would carry instead. Composed for a condition this " +
"router holds open, by the walk a send composes with; a clearing as if it cleared now, a silence " +
"only while there is one. It sends, queues and records nothing.",
Input: map[string]any{
"key": str("the condition's key, as the controller names it, e.g. module.openrazer.laptop.relogin-needed"),
"what": str("which message about it: raised (default), reminder, quiet-reminder, escalated, reopened, cleared, silenced"),
},
Run: func(a map[string]any) (any, error) { return h.Preview(strArg(a, "key"), strArg(a, "what")) }},
{Name: "messenger_check",
Description: "Would these words be allowed to leave the mesh? Runs the content rule — no address, path or " +
"secret — and answers allowed, or the class of what it carried. Sends nothing.",
+137
View File
@@ -0,0 +1,137 @@
package main
// What a condition's message would say, read before it is sent (novox/hq issue 422). The words a
// condition reaches the operator with are composed here, in this router, from the condition's own plain
// words and the line on the issues that track it (tracking.go). Until this tool, the only way to read
// them was to wait for a send and look at the operator's screen: the channels keep the text of nothing
// they sent (`Sent` holds a key and an outcome, never words), and a notification still on the desk is
// not in the notifier's history. So a wording could not be proven without nagging the operator.
//
// It is the sending path's own walk and nothing beside it: `allowedWords` (holder.go), which composes
// the message, carries the tracking line into it and reduces it where the content rule refuses its
// words — the same function `words` calls for a send, so a preview cannot drift from one. It records no
// refusal, states nothing on the bus, queues nothing, sends nothing and changes no record: the walk runs
// against a copy.
//
// What it does not say, because a channel adds it where a condition carries an open ask (`sayOn`): who
// asks, what each answer does, and the answers themselves. A preview of such a condition says that its
// message carries more, rather than letting the words read as the whole of it.
//
// Two messages are about a moment that has not come. A clearing is composed as if the condition cleared
// now, which the answer says, because a clearing composed from an open record would say "after a moment"
// for a condition open four hours. A silence is refused unless the condition is under one: a silence
// message says until when, and a condition nobody silenced has no such moment to say.
import (
"sort"
"strings"
)
// The messages a condition is said with, as `what` names them through the sending path (holder.go).
var previewable = []string{"raised", "reminder", "quiet-reminder", "escalated", "reopened", "cleared", "silenced"}
// Preview is the message for a condition this router holds, composed as it would be sent, and sent
// nowhere. `what` is which of the messages about it (previewable, default `raised`).
func (h *Holder) Preview(key, what string) (map[string]any, error) {
key = strings.TrimSpace(key)
if key == "" {
return nil, errorf("key is required: the condition's key, as the controller names it — `mesh-controller.conditions` says which are open")
}
what = strings.TrimSpace(what)
if what == "" {
what = "raised"
}
if !previewableWhat(what) {
return nil, errorf("what %q is not one of the messages a condition is said with: %s", what, strings.Join(previewable, ", "))
}
// Asked outside the work, as every tick that composes a message warms it: a preview on a cold
// tracking word would say the tracking could not be checked, which is true of this instant and not
// of the send it is read for.
h.warmTracking()
now := h.Now()
h.mu.Lock()
held, holding := h.open[key]
var r Record
if holding {
r = *held // the walk runs against a copy: a preview changes no record this router holds
}
var conditions []string
for k, rec := range h.open {
if rec.Origin == "condition" {
conditions = append(conditions, k)
}
}
h.mu.Unlock()
if !holding || r.Origin != "condition" {
sort.Strings(conditions)
switch {
case holding && len(conditions) == 0:
return nil, errorf("%s is not a condition but this router's own %s record, which is said as part of an ask, never on its own; it holds no condition",
key, r.Origin)
case holding:
return nil, errorf("%s is not a condition but this router's own %s record, which is said as part of an ask, never on its own; it holds the conditions %s",
key, r.Origin, strings.Join(conditions, ", "))
case len(conditions) == 0:
return nil, errorf("no condition %s is open here, and this router holds none: `mesh-controller.conditions` says what the controller holds open", key)
default:
return nil, errorf("no condition %s is open here; it holds %s", key, strings.Join(conditions, ", "))
}
}
if what == "silenced" && !r.silenced(now) {
return nil, errorf("%s is under no silence, and a silence message says until when it is quiet: there is no such message to read", key)
}
if what == "cleared" {
// As if it cleared now: the clearing says how long it was open, which an open record cannot say.
r.Cleared = now
}
asking := h.openAsk(&r) != nil // askOf takes the same lock, so this is asked outside it
m, line, refusal, ownWords := h.allowedWords(&r, what, now)
if r.Answered != "" {
m = h.answeredWords(&r, m)
}
out := map[string]any{
"key": key,
"what": what,
"title": m.Title,
"body": m.Body,
"sent": "nothing: this composes the words and sends nowhere",
}
if m.Urgent {
out["urgent"] = true
}
if m.Quiet {
out["quiet"] = true
}
if line != "" {
out["tracking"] = line
}
if what == "cleared" {
out["as-if"] = "it cleared now: how long it was open is said from this moment"
}
if asking {
out["carries-more"] = "this condition has an open ask, so a channel's message also carries who asks, what each answer does, and the answers themselves"
}
out["allowed"] = ownWords
if !ownWords {
// What stands above is what would leave; its own words would not, and this says why.
out["carried"] = refusal.What
if refusal.Class != "" {
out["class"] = refusal.Class
}
out["instead"] = "its own words may not leave the mesh, so this is the message as reduced for sending"
}
return out, nil
}
func previewableWhat(what string) bool {
for _, w := range previewable {
if w == what {
return true
}
}
return false
}
@@ -0,0 +1,288 @@
package main
import (
"reflect"
"strings"
"testing"
"time"
)
// A condition's message can be read before it is sent (novox/hq issue 422): the words a send would
// carry, the tracking line among them, and nothing of this router moved while they were read.
// previewHolder is a router holding one urgent condition — one a send goes out for at once, so a preview
// that sends, queues or records anything is caught — with the issues given tracking its key.
func previewHolder(t *testing.T, issues []TrackedIssue) (*Holder, *fakeChannel, *fakeChannel, *clock, *memStore, Condition) {
t.Helper()
h, tg, dt, c, st := newHolder(t)
h.Tracking = func() (map[string][]TrackedIssue, error) {
return map[string][]TrackedIssue{"mesh.hand-acts.rolled-back.healer-wanted": issues}, nil
}
k := cond("mesh.hand-acts.rolled-back.healer-wanted", Urgent, "rolled-back was repaired by hand 4 times in 14 days")
k.Explanation = "Nothing for you to do. A person repaired rolled-back by hand 4 times in 14 days, so an automatic repair is wanted for it. Nothing is broken now."
k.Needs = ""
k.Raised = c.now()
h.Condition(EventRaised, k)
settle(h, c)
if len(tg.sends) == 0 && len(dt.sends) == 0 {
t.Fatal("the condition this is read from was never sent, so a preview sending something would not show")
}
return h, tg, dt, c, st, k
}
// unmoved fails when anything a send would move has moved: the sends, the queue, the history, what is
// stored, and the record itself.
type unmoved struct {
sends, queued, recent, refusals, puts int
stored Record
held Record
}
func mark(t *testing.T, h *Holder, tg, dt *fakeChannel, st *memStore, key string) unmoved {
t.Helper()
h.mu.Lock()
defer h.mu.Unlock()
queued := 0 // the outbox is held per channel, never per condition
for _, list := range h.outbox {
queued += len(list)
}
return unmoved{sends: len(tg.sends) + len(dt.sends), queued: queued, recent: len(h.recent),
refusals: len(h.refusals), puts: st.puts, stored: st.recs[key], held: *h.open[key]}
}
func (was unmoved) check(t *testing.T, h *Holder, tg, dt *fakeChannel, st *memStore, c *clock, key string) {
t.Helper()
// A queued message waits for a tick, so the queue is read and then given one: either would catch it.
now := mark(t, h, tg, dt, st, key)
if now.sends != was.sends {
t.Errorf("a preview sent %d message(s)", now.sends-was.sends)
}
if now.queued != was.queued {
t.Errorf("a preview queued %d message(s)", now.queued-was.queued)
}
if now.recent != was.recent {
t.Errorf("a preview recorded %d send(s)", now.recent-was.recent)
}
if now.refusals != was.refusals {
t.Errorf("a preview recorded %d refusal(s): only a send does that", now.refusals-was.refusals)
}
if now.puts != was.puts {
t.Errorf("a preview wrote to the store %d time(s), even if the bytes were the same", now.puts-was.puts)
}
if !reflect.DeepEqual(now.stored, was.stored) {
t.Errorf("a preview wrote the record: %+v", now.stored)
}
if !reflect.DeepEqual(now.held, was.held) {
t.Errorf("a preview changed the record held: %+v", now.held)
}
settle(h, c)
if after := mark(t, h, tg, dt, st, key); after.sends != was.sends {
t.Errorf("a preview queued a message, which the next tick sent: %d", after.sends-was.sends)
}
}
func TestAConditionsMessageIsReadWithoutBeingSent(t *testing.T) {
h, tg, dt, c, st, k := previewHolder(t, []TrackedIssue{{Number: 352, Status: "located", Needs: "decision"}})
was := mark(t, h, tg, dt, st, k.Key)
out, err := h.Preview(k.Key, "")
if err != nil {
t.Fatal(err)
}
if out["what"] != "raised" {
t.Errorf("what %v; a preview with none asked is the first message", out["what"])
}
if out["title"] != "Urgent: mesh hand-acts rolled-back healer-wanted" {
t.Errorf("title %q", out["title"])
}
body, _ := out["body"].(string)
if !strings.Contains(body, "Tracked as issue 352: nobody is working on it; it waits for a decision first.") {
t.Errorf("the body says nothing of the issue that tracks it: %q", body)
}
if out["tracking"] != "Tracked as issue 352: nobody is working on it; it waits for a decision first." {
t.Errorf("tracking %q", out["tracking"])
}
if out["allowed"] != true {
t.Errorf("allowed %v, carried %v: these words may leave the mesh", out["allowed"], out["carried"])
}
// The verdict is left out while nobody works on the issue (ADR 0285 §4), as in a send.
if strings.Contains(body, "Nothing for you to do.") {
t.Errorf("the preview reassures where nothing is being done: %q", body)
}
keyless(t, Message{Title: out["title"].(string), Body: body})
was.check(t, h, tg, dt, st, c, k.Key)
}
func TestAPreviewSaysTheWordsTheChannelWasGiven(t *testing.T) {
issues := []TrackedIssue{{Number: 386, Status: "fixing", Worker: "fix-386@g14", Since: at(21, 10)}}
h, tg, dt, c, st, k := previewHolder(t, issues)
sent := dt.sends[len(dt.sends)-1] // the message the desk was actually handed
was := mark(t, h, tg, dt, st, k.Key)
out, err := h.Preview(k.Key, "")
if err != nil {
t.Fatal(err)
}
if out["title"] != sent.Title || out["body"] != sent.Body {
t.Fatalf("preview %q / %q; the channel was given %q / %q", out["title"], out["body"], sent.Title, sent.Body)
}
if !strings.Contains(sent.Body, "Tracked as issue 386: fix-386 on g14 is fixing it since ") {
t.Fatalf("the message this is compared against carries no tracking line: %q", sent.Body)
}
was.check(t, h, tg, dt, st, c, k.Key)
}
func TestAClearingIsPreviewedAsIfItClearedNow(t *testing.T) {
h, tg, dt, c, st, k := previewHolder(t, []TrackedIssue{{Number: 352, Status: "located"}})
c.pass(4 * time.Hour)
was := mark(t, h, tg, dt, st, k.Key)
out, err := h.Preview(k.Key, "cleared")
if err != nil {
t.Fatal(err)
}
// Composed from the open record, this said "after a moment": the clearing's own time is zero, so the
// time it was open came out negative.
if out["title"] != "Resolved: mesh hand-acts rolled-back healer-wanted, after 4.0 h" {
t.Errorf("title %q; a clearing says how long it was open", out["title"])
}
if out["as-if"] == nil {
t.Errorf("the answer does not say the clearing is composed as if it cleared now: %v", out)
}
if _, said := out["tracking"]; said {
t.Errorf("a clearing says a tracking line: %v", out["tracking"])
}
was.check(t, h, tg, dt, st, c, k.Key)
}
func TestASilenceIsPreviewedOnlyWhileThereIsOne(t *testing.T) {
h, tg, dt, c, st, k := previewHolder(t, []TrackedIssue{{Number: 352, Status: "located"}})
// Under no silence, a silence message would say a moment that does not exist.
_, err := h.Preview(k.Key, "silenced")
if err == nil || !strings.Contains(err.Error(), "under no silence") {
t.Fatalf("a silence previewed without one: %v", err)
}
h.mu.Lock()
h.open[k.Key].SilencedTill = c.now().Add(7 * 24 * time.Hour)
h.open[k.Key].SilencedWhy = "the operator knows"
h.mu.Unlock()
was := mark(t, h, tg, dt, st, k.Key)
out, err := h.Preview(k.Key, "silenced")
if err != nil {
t.Fatal(err)
}
title, _ := out["title"].(string)
if !strings.HasPrefix(title, "Silenced until ") || strings.Contains(title, "Jan 1 ") || strings.Contains(title, " 1 Jan ") {
t.Errorf("title %q; a silence says until when it is quiet", title)
}
if _, said := out["tracking"]; said {
t.Errorf("a silence says a tracking line: %v", out["tracking"])
}
was.check(t, h, tg, dt, st, c, k.Key)
}
func TestAPreviewOfWhatThisRouterDoesNotHoldIsRefusedByName(t *testing.T) {
h, _, _, _, _, k := previewHolder(t, nil)
_, err := h.Preview("machine.ace.silent", "")
if err == nil {
t.Fatal("a condition this router does not hold was previewed")
}
if !strings.Contains(err.Error(), "machine.ace.silent") || !strings.Contains(err.Error(), k.Key) {
t.Errorf("the refusal says neither what was asked nor what is held: %v", err)
}
if _, err := h.Preview("", ""); err == nil || !strings.Contains(err.Error(), "key is required") {
t.Errorf("no key: %v", err)
}
if _, err := h.Preview(k.Key, "shouted"); err == nil || !strings.Contains(err.Error(), "shouted") {
t.Errorf("an unknown message: %v", err)
}
// An ask's own record is held here too, and is said as part of an ask, never as a condition.
h.mu.Lock()
h.open["ask.someone.1"] = &Record{Key: "ask.someone.1", Origin: "ask", Severity: Urgent, Summary: "a question"}
h.mu.Unlock()
_, err = h.Preview("ask.someone.1", "")
if err == nil || !strings.Contains(err.Error(), "not a condition") || !strings.Contains(err.Error(), k.Key) {
t.Errorf("an ask's record previewed as a condition: %v", err)
}
// A router holding an ask and no condition says what that key is, not that it holds nothing.
bare, _, _, _, _ := newHolder(t)
bare.mu.Lock()
bare.open["ask.someone.2"] = &Record{Key: "ask.someone.2", Origin: "ask", Severity: Urgent, Summary: "a question"}
bare.mu.Unlock()
_, err = bare.Preview("ask.someone.2", "")
if err == nil || !strings.Contains(err.Error(), "not a condition") {
t.Errorf("the only record held is an ask's: %v", err)
}
}
func TestAPreviewAsksTheTrackerWhenItsWordWentCold(t *testing.T) {
asked := 0
h, _, _, c, _, k := previewHolder(t, []TrackedIssue{{Number: 352, Status: "located"}})
h.Tracking = func() (map[string][]TrackedIssue, error) {
asked++
return map[string][]TrackedIssue{k.Key: {{Number: 352, Status: "located"}}}, nil
}
c.pass(TrackingKept + time.Minute) // older than an answer is used for
out, err := h.Preview(k.Key, "quiet-reminder")
if err != nil {
t.Fatal(err)
}
if asked != 1 {
t.Fatalf("the tracker was asked %d times; a preview on a cold word asks once", asked)
}
if out["tracking"] != "Tracked as issue 352: nobody is working on it." {
t.Errorf("tracking %q; a preview that asked should say what it heard", out["tracking"])
}
}
func TestAPreviewSaysTheReducedWordsAndWhyWhenItsOwnMayNotLeave(t *testing.T) {
h, tg, dt, c, st, _ := previewHolder(t, nil)
stated := 0
h.Emit = func(string, any) error { stated++; return nil }
// A warning that needs nothing is held, not sent, so this refusal is one no send has met yet.
k := cond("machine.ace.unreadable", Warning, "a file on it could not be read")
k.Explanation = "Nothing for you to do. A file at /home/someone/secrets.txt could not be read."
k.Needs = ""
k.Raised = c.now()
h.Condition(EventRaised, k)
settle(h, c)
was := mark(t, h, tg, dt, st, k.Key)
out, err := h.Preview(k.Key, "")
if err != nil {
t.Fatal(err)
}
if out["allowed"] != false || out["carried"] == nil {
t.Fatalf("allowed %v, carried %v; the content rule refuses a path", out["allowed"], out["carried"])
}
// The words said are the ones that would go out, not the refused ones.
body, _ := out["body"].(string)
if strings.Contains(body, "/home/") {
t.Errorf("the preview says words no send would carry: %q", body)
}
if !strings.Contains(body, "withheld") {
t.Errorf("body %q; a reduced message says that part of it was withheld", body)
}
if !strings.Contains(body, "a file path") {
t.Errorf("body %q; a reduced message says what it carried", body)
}
if out["instead"] == nil {
t.Errorf("the answer does not say these are the reduced words: %v", out)
}
// Reading it recorded no refusal, stated nothing on the bus, and left the record's own words alone.
if stated != 0 {
t.Errorf("a preview stated %d fact(s) on the bus", stated)
}
if was.held.Refused != "" {
t.Fatalf("the record was already refused before the preview: %q", was.held.Refused)
}
was.check(t, h, tg, dt, st, c, k.Key)
}
+5 -3
View File
@@ -159,9 +159,11 @@ const (
// withTracking is a condition's message with the line on the issues that track it, and its verdict changed where
// an issue waits for the operator or nobody works on any of them. Cleared and silenced messages, and one withheld
// whole, say nothing of it.
func withTracking(m Message, r *Record, what string, issues []TrackedIssue, known bool, now time.Time, loc *time.Location) Message {
// It answers the line it put in the message, empty when the message carries none, so whoever composed it
// never has to work out the rule a second time (novox/hq issue 422).
func withTracking(m Message, r *Record, what string, issues []TrackedIssue, known bool, now time.Time, loc *time.Location) (Message, string) {
if r.Origin != "condition" || what == "cleared" || what == "silenced" || m.Title == "A mesh message was withheld" {
return m
return m, ""
}
line, verdict := trackingWords(issues, known, now, loc)
// since is the last line, which never holds a line break; the explanation is everything before it.
@@ -184,7 +186,7 @@ func withTracking(m Message, r *Record, what string, issues []TrackedIssue, know
}
}
m.Body = strings.Join(parts, "\n")
return m
return m, line
}
// needsAnswer is the "Needs you:" sentence for the issues that wait for the operator's answer.
@@ -26,7 +26,8 @@ func trackedMessage(issues []TrackedIssue, known bool) Message {
Explanation: "Nothing for you to do. A person repaired rolled-back by hand 4 times in 14 days, so an automatic repair is wanted for it. Nothing is broken now.",
Raised: trackNow.Add(-26 * time.Hour)}
m := compose(r, "quiet-reminder", trackNow, time.UTC, 0)
return withTracking(m, r, "quiet-reminder", issues, known, trackNow, time.UTC)
out, _ := withTracking(m, r, "quiet-reminder", issues, known, trackNow, time.UTC)
return out
}
func TestAConditionsMessageSaysWhereTheIssueThatTracksItStands(t *testing.T) {
@@ -129,7 +130,7 @@ func TestNothingForYouToDoIsNeverSaidWhileTheIssueWaitsOnTheOperatorOrNobodyWork
func TestAVerdictThatNeedsTheOperatorStands(t *testing.T) {
r := &Record{Key: "module.mounts.shanks.unhealthy", Origin: "condition", Severity: Urgent, Headline: "mounts not working on shanks",
Explanation: "Needs you: give the share's login. mounts on shanks is not healthy.", Raised: trackNow.Add(-time.Hour)}
m := withTracking(compose(r, "raised", trackNow, time.UTC, 0), r, "raised",
m, _ := withTracking(compose(r, "raised", trackNow, time.UTC, 0), r, "raised",
[]TrackedIssue{{Number: 386, Status: "open", Needs: "operator"}}, true, trackNow, time.UTC)
want := "Needs you: give the share's login. mounts on shanks is not healthy.\nTracked as issue 386: it waits for your answer.\nSince 18:30."
if m.Body != want {
@@ -144,14 +145,16 @@ func TestAClearingASilenceAndANoticeSayNoIssue(t *testing.T) {
Cleared: trackNow, SilencedTill: trackNow.Add(time.Hour)}
for _, what := range []string{"cleared", "silenced"} {
m := compose(r, what, trackNow, time.UTC, 0)
if got := withTracking(m, r, what, issues, true, trackNow, time.UTC); got.Body != m.Body {
t.Errorf("%s: %q", what, got.Body)
got, line := withTracking(m, r, what, issues, true, trackNow, time.UTC)
if got.Body != m.Body || line != "" {
t.Errorf("%s: %q, line %q", what, got.Body, line)
}
}
n := &Record{Key: "n", Origin: "notify", Headline: "a notice", Raised: trackNow}
m := compose(n, "raised", trackNow, time.UTC, 0)
if got := withTracking(m, n, "raised", issues, true, trackNow, time.UTC); got.Body != m.Body {
t.Errorf("notice: %q", got.Body)
got, line := withTracking(m, n, "raised", issues, true, trackNow, time.UTC)
if got.Body != m.Body || line != "" {
t.Errorf("notice: %q, line %q", got.Body, line)
}
}
+1
View File
@@ -103,6 +103,7 @@
"messenger_status",
"messenger_recent",
"messenger_test",
"messenger_preview",
"messenger_check",
"messenger_identities",
"messenger_unlink"