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

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:
2026-10-11 00:30:32 +02:00
parent 7534167fd0
commit b7b34ddad7
5 changed files with 281 additions and 0 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 |
| `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 |
+9
View File
@@ -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.",
+113
View File
@@ -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"])
}
}
+1
View File
@@ -103,6 +103,7 @@
"messenger_status",
"messenger_recent",
"messenger_test",
"messenger_preview",
"messenger_check",
"messenger_identities",
"messenger_unlink"