The watchman raises what a node keeps of a module as it runs, for a manifest that does not yet say it, from the module's have, and clears it once the have says none (ADR 0601 §4; review round 1, finding 3)

This commit is contained in:
2026-11-03 16:44:47 +01:00
parent a41d2851c7
commit 0fa00d5e6c
3 changed files with 58 additions and 3 deletions
@@ -72,6 +72,29 @@ func TestTheWatchmansFindings(t *testing.T) {
}
w.ModuleVerdict("nats", value(""), Put)
}, map[string]string{}},
{"ADR 0601 §4: what a node keeps of a module as it runs, for a manifest that does not say it yet, is a warning until the have says none", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) {
have := func(kept ...string) []byte {
b, err := delivery.EncodeHave(delivery.Have{FP: "src2:" + strings.Repeat("a", 64), Phase: delivery.PhaseRunning, Health: delivery.HealthOK, Kept: kept})
if err != nil {
t.Fatal(err)
}
return b
}
k := FindingKey(ScopeModule, "runner-x.ace", "kept")
w.Have("ace.runner-x", have("runner-x runs the tool runner's process here"), Put)
f := o.findings[k]
if f.Kind != KindKeptAsRuns || f.Severity != Warning || !strings.Contains(f.Summary, "the tool runner's process") {
t.Fatalf("findings = %v", o.findings)
}
c.t = c.t.Add(time.Minute)
w.Have("ace.runner-x", have("runner-x runs the tool runner's process here"), Put)
if !o.findings[k].Since.Equal(f.Since) {
t.Errorf("a have written again moved the finding's since: %v, was %v", o.findings[k].Since, f.Since)
}
w.Have("ace.prov-y", have("prov-y carries its own secrets sealed"), Put)
w.Have("ace.runner-x", have(), Put) // its manifest says tool-runner now
w.Have("ace.prov-y", nil, Delete)
}, map[string]string{}},
{"WM4: a stuck key not moved is its finding, and its move deletes it", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) {
w.Step("go", "n1.ask1", runningStep("ask1", t0), Put)
expire(w, c, pulseKeyOf(t, checksKey, "ask1"), 120*time.Second)
@@ -199,6 +199,7 @@ const (
KindUnhealthy = "module-unhealthy"
KindNeedsOperator = "needs-operator"
KindDeadLetters = "max-deliveries"
KindKeptAsRuns = "kept-as-it-runs"
)
func stamp(t time.Time) string { return t.UTC().Format("2006-01-02 15:04 MST") }
@@ -476,11 +477,13 @@ func (w *Watchman) haveFindings(key string, node, module string, h *delivery.Hav
w.unsignal("unhealthy-confirm", module+"."+node)
w.unsignal("unhealthy", module+"."+node)
w.unfind(unhealthyKey)
w.keptFinding(key, node, module, nil)
w.applyCheck(node)
return
}
w.haveState[key] = *h
now := w.now()
w.keptFinding(key, node, module, h.Kept)
// S21: a have refused or applied in part says phase failed, with the node's words in its detail.
if h.Phase == delivery.PhaseFailed {
@@ -531,6 +534,33 @@ func (w *Watchman) haveFindings(key string, node, module string, h *delivery.Hav
}
}
// keptFinding raises, as a warning, what a node keeps of a module as it runs because its manifest does not yet say what shared
// code no longer infers (its have's Kept, novox/hq ADR 0601 §4: the tool runner, the core's credentials), naming each; and
// clears it once the have says none, or is gone. Its since is the first have that said it, so a have written again moves
// nothing.
func (w *Watchman) keptFinding(key, node, module string, kept []string) {
id := module + "." + node
fkey := FindingKey(ScopeModule, id, "kept")
if len(kept) == 0 {
delete(w.keptSince, key)
w.unfind(fkey)
return
}
if w.keptSince == nil {
w.keptSince = map[string]time.Time{}
}
since, ok := w.keptSince[key]
if !ok {
since = w.now()
w.keptSince[key] = since
}
words := strings.Join(kept, "; ")
w.find(Finding{Scope: ScopeModule, ID: id, Token: "kept", Kind: KindKeptAsRuns, Machine: node, Severity: Warning,
Source: "node-engine", Since: since, Summary: fmt.Sprintf("%s on %s is kept as it runs: %s", module, node, words),
Said: firstLine(words), Headline: fmt.Sprintf("%s kept as it runs on %s", module, node),
Needs: "a build of " + module + " whose manifest says it (tool-runner, core-credentials)"})
}
// ControlNodeModule is the module whose have names a control node: the bus module, so the control node is the bus node,
// the one stably reachable node every part depends on (novox/hq ADR 0590, issue 1126: the controller that named it ended).
// The same name the bus module's events are heard under (SlowConsumerSubject).
@@ -60,9 +60,11 @@ type Watchman struct {
presence map[string]presence
pulses map[string]*pulse // live pulses, by pulse key
stuck map[string]delivery.Stuck
wants map[string]string // the fingerprint of each want that stands, by want key
haves map[string]string // the fingerprint each node runs of a module, by have key
bound time.Duration
// keptSince is, by have key, when its have first said what the node keeps of the module as it runs (keptFinding).
keptSince map[string]time.Time
wants map[string]string // the fingerprint of each want that stands, by want key
haves map[string]string // the fingerprint each node runs of a module, by have key
bound time.Duration
// findings is every finding written, by key, as written; seededFindings those read at the start and not yet observed
// again.
findings map[string][]byte