messenger: read a condition's message before it is sent (issue 422)
mesh/merge-gate pass: builds messenger → 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
mesh/merge-gate pass: builds messenger → 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 words a condition reaches the operator with are composed in the router, and nothing kept them: a send's history row holds a key and an outcome, never text, and a notification still on the desk is not in the notifier's history. So a wording could only be proven by waiting for a send and reading the operator's screen, which is why proving issue 400's tracking line stalled. messenger_preview composes what a send would carry for a condition the router holds open — the same words, the same tracking line, the same verdict — and says whether the content rule would let them out. It sends, queues and records nothing.
This commit is contained in:
@@ -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 |
|
||||
| `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 |
|
||||
|
||||
@@ -603,6 +603,15 @@ 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. " +
|
||||
"Composed exactly as it would be sent, from a condition this router holds open; sends 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.",
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
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 composes exactly what `composeTracked` composes for a send — the same words, the same tracking
|
||||
// line, the same verdict — and sends nothing, queues nothing and records nothing. It also says whether
|
||||
// the content rule would let those words leave the mesh, which is the other half of what a send decides.
|
||||
//
|
||||
// It reads the conditions this router holds open. A key it does not hold is refused by name: a
|
||||
// condition that is not open has no message, and one invented here would prove nothing about the real
|
||||
// one.
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The messages a condition is said with, as `what` names them through the sending path (words.go).
|
||||
// `raised` is the first message about it; `cleared` and `silenced` carry no tracking line (ADR 0285 §3).
|
||||
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 the router asks it on its own tick: a preview on a cold tracking word
|
||||
// would say the tracking could not be checked, which is true of this moment and not of a send.
|
||||
h.warmTracking()
|
||||
|
||||
h.mu.Lock()
|
||||
held, ok := h.open[key]
|
||||
var r Record
|
||||
if ok {
|
||||
r = *held // composed from a copy: a preview changes nothing this router holds
|
||||
}
|
||||
open := make([]string, 0, len(h.open))
|
||||
for k := range h.open {
|
||||
open = append(open, k)
|
||||
}
|
||||
h.mu.Unlock()
|
||||
if !ok {
|
||||
sort.Strings(open)
|
||||
if len(open) == 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)
|
||||
}
|
||||
return nil, errorf("no condition %s is open here; it holds %s", key, strings.Join(open, ", "))
|
||||
}
|
||||
|
||||
now := h.Now()
|
||||
m := h.composeTracked(&r, what, now, 0)
|
||||
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, said := h.trackingLine(&r, what, now); said {
|
||||
out["tracking"] = line
|
||||
}
|
||||
if refusal, allowed := Check(m.Text()); allowed {
|
||||
out["allowed"] = true
|
||||
} else {
|
||||
// A send would say less instead (compose with a withhold level), never these words.
|
||||
out["allowed"] = false
|
||||
out["carried"] = refusal.What
|
||||
out["class"] = refusal.Class
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// trackingLine is the line on the issues that track the condition, and whether this message carries one:
|
||||
// a clearing and a silence say none, and nor does a router that cannot ask the tracker.
|
||||
func (h *Holder) trackingLine(r *Record, what string, now time.Time) (string, bool) {
|
||||
if !h.tracksAsked() || r.Origin != "condition" || what == "cleared" || what == "silenced" {
|
||||
return "", false
|
||||
}
|
||||
issues, known := h.trackedBy(r.Key)
|
||||
line, _ := trackingWords(issues, known, now, h.zone())
|
||||
return line, line != ""
|
||||
}
|
||||
|
||||
func previewableWhat(what string) bool {
|
||||
for _, w := range previewable {
|
||||
if w == what {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A condition's message can be read before it is sent (novox/hq issue 422): the same words a send would
|
||||
// carry, the tracking line among them, and nothing goes out while they are read.
|
||||
|
||||
// previewHolder is a router holding one raised condition that needs nothing, with the issues given
|
||||
// tracking its key.
|
||||
func previewHolder(t *testing.T, issues []TrackedIssue) (*Holder, *fakeChannel, *fakeChannel, *clock, Condition) {
|
||||
t.Helper()
|
||||
h, tg, dt, c, _ := 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", Warning, "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)
|
||||
tg.sends, dt.sends = nil, nil
|
||||
return h, tg, dt, c, k
|
||||
}
|
||||
|
||||
func TestAConditionsMessageIsReadWithoutBeingSent(t *testing.T) {
|
||||
h, tg, dt, _, _ := previewHolder(t, []TrackedIssue{{Number: 352, Status: "located", Needs: "decision"}})
|
||||
|
||||
out, err := h.Preview("mesh.hand-acts.rolled-back.healer-wanted", "")
|
||||
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"] != "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)
|
||||
}
|
||||
if len(tg.sends) != 0 || len(dt.sends) != 0 {
|
||||
t.Fatalf("a preview sent something: telegram %d, desktop %d", len(tg.sends), len(dt.sends))
|
||||
}
|
||||
keyless(t, Message{Title: out["title"].(string), Body: body})
|
||||
}
|
||||
|
||||
func TestAPreviewSaysTheSameWordsTheSendWouldCarry(t *testing.T) {
|
||||
issues := []TrackedIssue{{Number: 386, Status: "fixing", Worker: "fix-386@g14", Since: at(21, 10)}}
|
||||
h, tg, dt, c, k := previewHolder(t, issues)
|
||||
|
||||
out, err := h.Preview(k.Key, "reminder")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The same record, the same moment, through the sending path.
|
||||
h.mu.Lock()
|
||||
r := *h.open[k.Key]
|
||||
h.mu.Unlock()
|
||||
want := h.composeTracked(&r, "reminder", c.now(), 0)
|
||||
if out["title"] != want.Title || out["body"] != want.Body {
|
||||
t.Fatalf("preview %q / %q; a send would say %q / %q", out["title"], out["body"], want.Title, want.Body)
|
||||
}
|
||||
if !strings.Contains(want.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", want.Body)
|
||||
}
|
||||
if len(tg.sends) != 0 || len(dt.sends) != 0 {
|
||||
t.Fatalf("a preview sent something: telegram %d, desktop %d", len(tg.sends), len(dt.sends))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAClearingAndASilencePreviewWithoutATrackingLine(t *testing.T) {
|
||||
h, _, _, _, k := previewHolder(t, []TrackedIssue{{Number: 352, Status: "located"}})
|
||||
for _, what := range []string{"cleared", "silenced"} {
|
||||
out, err := h.Preview(k.Key, what)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, said := out["tracking"]; said {
|
||||
t.Errorf("%s says a tracking line: %v", what, out["tracking"])
|
||||
}
|
||||
if body, _ := out["body"].(string); strings.Contains(body, "issue") {
|
||||
t.Errorf("%s names an issue: %q", what, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPreviewOfAConditionTheRouterDoesNotHoldIsRefusedByName(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)
|
||||
}
|
||||
}
|
||||
|
||||
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 TestAPreviewSaysWhenTheWordsWouldNotBeAllowedOut(t *testing.T) {
|
||||
h, _, _, c, _ := previewHolder(t, nil)
|
||||
k := cond("machine.ace.silent", Warning, "the home server has not been heard for 15 min")
|
||||
k.Explanation = "Needs you: look at /home/someone/secrets.txt on it."
|
||||
k.Raised = c.now()
|
||||
h.Condition(EventRaised, k)
|
||||
|
||||
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"])
|
||||
}
|
||||
}
|
||||
@@ -103,6 +103,7 @@
|
||||
"messenger_status",
|
||||
"messenger_recent",
|
||||
"messenger_test",
|
||||
"messenger_preview",
|
||||
"messenger_check",
|
||||
"messenger_identities",
|
||||
"messenger_unlink"
|
||||
|
||||
Reference in New Issue
Block a user