mesh-knowledge: give a never-reviewed notice its resolved line when raised, and test a refused clear and local time in a set zone (review of #219)
mesh/merge-gate pass: builds mesh-knowledge → 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 failed: its walk failed, carrying its own commit: a gate on a first machine (what it carried put back), a build, a machine

The operator's channel edits a cleared message with the resolved line it was raised with, so one sent only with
the clear was never shown.
This commit is contained in:
2026-10-11 19:41:45 +02:00
parent d45761678a
commit c394ecd9fd
6 changed files with 67 additions and 10 deletions
@@ -356,7 +356,7 @@ func (k *KB) tellTopic(ctx context.Context, topic string) {
summary := fmt.Sprintf("The clauses of %d prose decisions on %s are drafted and checked: %d confirmed, %d still drafted.", records, topic, confirmed, drafted)
explanation := "A second judgement confirmed each clause against its record; a clause still drafted is never served as in force. " +
"You may send any record back with the knowledge base's return, and its clauses are drafted again. Nothing needs you otherwise."
if err := k.Notify("knowledge.clauses."+strings.ReplaceAll(topic, " ", "-"), summary, "Clauses drafted for "+topic, explanation); err != nil {
if err := k.Notify("knowledge.clauses."+strings.ReplaceAll(topic, " ", "-"), summary, "Clauses drafted for "+topic, explanation, ""); err != nil {
k.logf("[mesh-knowledge] the operator could not be told about %s's clauses: %v", topic, err)
}
}
@@ -64,8 +64,9 @@ type Reserver func(kind, title string) (int, error)
// the structured answer and who judged.
type Judgement func(question string, schema map[string]any, model string) (json.RawMessage, string, error)
// Notifier tells the operator's channel something (operator-channel.notify).
type Notifier func(key, summary, headline, explanation string) error
// Notifier tells the operator's channel something (operator-channel.notify). Resolved is the line its message says
// once it is cleared: the channel keeps the one given when it is raised (novox/hq issue 497); "" for none.
type Notifier func(key, summary, headline, explanation, resolved string) error
// Clearer tells the operator's channel that what it was told under key is over, in one resolved line.
type Clearer func(key, resolved string) error
@@ -400,6 +400,7 @@ type world struct {
// toldKeys are the keys told under, and cleared what was cleared: key, then its resolved line.
toldKeys []string
cleared [][2]string
resolved []string // the resolved line given with each notice raised
clock time.Time
}
@@ -410,8 +411,8 @@ func newWorld(t *testing.T, s Store, records Records) *world {
w.k = &KB{Store: s, Records: records, Reserve: w.tracker.reserve, Judge: w.judge.judge, Asks: w.router,
Emit: w.said.emit, Settings: func() (Settings, error) { return Settings{}, nil }, Now: func() time.Time { return w.clock },
Logf: func(f string, a ...any) { t.Logf(f, a...) },
Notify: func(key, summary, headline, explanation string) error {
w.told, w.toldKeys = append(w.told, summary), append(w.toldKeys, key)
Notify: func(key, summary, headline, explanation, resolved string) error {
w.told, w.toldKeys, w.resolved = append(w.told, summary), append(w.toldKeys, key), append(w.resolved, resolved)
return nil
},
Clear: func(key, resolved string) error { w.cleared = append(w.cleared, [2]string{key, resolved}); return nil }}
@@ -225,9 +225,13 @@ func busJudgement(node string) Judgement {
}
// busNotifier tells the operator's channel, as a warning: it is the one severity below urgent.
func busNotifier(key, summary, headline, explanation string) error {
_, err := stdio.Ask("seat:operator-channel.notify", map[string]any{"key": key, "subject": "the knowledge base",
"severity": "warning", "summary": summary, "headline": headline, "explanation": explanation})
func busNotifier(key, summary, headline, explanation, resolved string) error {
body := map[string]any{"key": key, "subject": "the knowledge base",
"severity": "warning", "summary": summary, "headline": headline, "explanation": explanation}
if resolved != "" {
body["resolved"] = resolved
}
_, err := stdio.Ask("seat:operator-channel.notify", body)
return err
}
@@ -335,7 +335,9 @@ func (k *KB) tellUnreviewed(e *Entry) {
w.Key(), w.Judging.Tries, w.Judging.Reason)
explanation := "No review that could not be had counts as an approval. Its author proposes it again to have it reviewed again; " +
"until then the knowledge base's health names it. The knowledge base's get and history show it."
if err := k.Notify(key, summary, "A proposal was never reviewed: "+w.Key().String(), explanation); err != nil {
// The channel edits a cleared message with the resolved line given here, not the one sent with the clear.
resolved := w.Key().String() + " no longer waits for its review"
if err := k.Notify(key, summary, "A proposal was never reviewed: "+w.Key().String(), explanation, resolved); err != nil {
k.logf("[mesh-knowledge] the operator could not be told that %s was never reviewed: %v; the module's health still names it", w.Key(), err)
}
}
@@ -2,6 +2,7 @@ package main
import (
"context"
"errors"
"fmt"
"strings"
"testing"
@@ -14,6 +15,7 @@ import (
// loss; and after its last try the history says so and the operator is told once. It is never an approval.
func TestAReviewOwedOutlivesARestartAndItsLastTryIsSaid(t *testing.T) {
eachStore(t, func(t *testing.T, s Store) {
inZone(t)
w := imported(t, s)
n := proposed(t, w, "author@g14", false)
key := Key{KindDecision, n}
@@ -51,6 +53,9 @@ func TestAReviewOwedOutlivesARestartAndItsLastTryIsSaid(t *testing.T) {
if try < MostReviewTries {
due = w.clock.Add(time.Duration(try) * 10 * time.Minute)
when := "asked again at " + due.Local().Format("2006-01-02 15:04 MST")
if utc := due.UTC().Format("15:04"); strings.Contains(when, utc) || !strings.Contains(when, "CEST") {
t.Fatalf("the time is not said locally: %q (UTC %s)", when, utc)
}
if !strings.Contains(why, when) {
t.Fatalf("try %d: the history does not say when it is asked again (%q): %q", try, when, why)
}
@@ -107,7 +112,8 @@ func TestProposedAgainAfterTheLastTryClearsTheNotice(t *testing.T) {
runOut(1)
e := mustGet(t, s, key)
first := fmt.Sprintf("knowledge.unreviewed.decision-%d.v%d", n, e.CurrentVersion)
if len(w.toldKeys) != 1 || w.toldKeys[0] != first || len(w.cleared) != 0 {
if len(w.toldKeys) != 1 || w.toldKeys[0] != first || len(w.cleared) != 0 ||
w.resolved[0] != key.String()+" no longer waits for its review" {
t.Fatalf("after the last try: told %v, cleared %v", w.toldKeys, w.cleared)
}
// Another version written while it waits (as a link change on it writes one) tells nothing again.
@@ -148,3 +154,46 @@ func TestProposedAgainAfterTheLastTryClearsTheNotice(t *testing.T) {
}
})
}
// inZone sets the local zone to one that is not UTC for one test, so a time said locally is proven to be, wherever
// the test runs.
func inZone(t *testing.T) {
t.Helper()
was := time.Local
time.Local = time.FixedZone("CEST", 2*60*60)
t.Cleanup(func() { time.Local = was })
}
// novox/hq issue 497: a clear the operator's channel refused keeps its notice as told, and is sent again at the next
// step until it is taken.
func TestAClearThatFailsIsSentAgain(t *testing.T) {
eachStore(t, func(t *testing.T, s Store) {
w := imported(t, s)
n := proposed(t, w, "author@g14", false)
w.judge.reviewFail = true
for try := 1; try <= MostReviewTries; try++ {
w.reviewAll(t)
w.clock = w.clock.Add(time.Duration(try) * ReviewPause)
}
if len(w.toldKeys) != 1 {
t.Fatalf("after the last try: told %v", w.toldKeys)
}
prefix := fmt.Sprintf("knowledge.unreviewed.decision-%d.", n)
refused := 0
w.k.Clear = func(key, resolved string) error { refused++; return errors.New("the channel is down") }
if _, err := w.k.Propose("author@g14", valid(n)); err != nil {
t.Fatal(err)
}
w.reviewAll(t)
if told, err := s.ToldOf(context.Background(), prefix); err != nil || len(told) != 1 || refused == 0 {
t.Fatalf("a refused clear: told %v (%v), refused %d", told, err, refused)
}
w.k.Clear = func(key, resolved string) error { w.cleared = append(w.cleared, [2]string{key, resolved}); return nil }
w.clock = w.clock.Add(time.Second)
w.reviewAll(t)
if told, err := s.ToldOf(context.Background(), prefix); err != nil || len(told) != 0 || len(w.cleared) != 1 ||
w.cleared[0][0] != w.toldKeys[0] {
t.Fatalf("the next step: told %v (%v), cleared %v", told, err, w.cleared)
}
})
}