Files
mesh-controller/internal/conditions/sayable_test.go
T
jochen 8e8712e352 Look twice before saying a probe failed, and say conditions in machine names (hq issue 277)
D2 raised a resolver urgent on one query that timed out while its machine was
loaded, and its summary carried the resolver's address and socket text, so the
operator channel withheld the whole alert.

- D2 asks every question up to three times, all at once; a resolver that
  answers nothing is held for the next run and raised urgent when two runs
  in a row find it silent. A wrong answer is still raised at once.
- Findings a single look can be wrong about carry Confirm: raised on the
  second look in a row, kept while open, never cleared-and-reraised. Used by
  D2 silence, D3 (also asks discovery twice), D6 behind, D9, D13 unmeasured,
  probe-failed of the doctor, and blind watchdog rows.
- Probe seat asks (D8, D13) are asked again when the bus brought no answer.
- Summaries name machines and say things in words; addresses, paths,
  domains and raw errors move to the evidence (D2, D5, D8, D9, D13, S12).
- internal/outward mirrors the messenger's content rule, allowing the mesh's
  machine names; the keeper rewords a summary that would be withheld and keeps
  it whole in the evidence; a TestMain lint fails the suite on any raised or
  linted finding that would be withheld.
2026-10-06 18:40:33 +02:00

47 lines
2.0 KiB
Go

package conditions
import (
"strings"
"testing"
"github.com/novox/mesh-controller/internal/outward"
)
// **A summary the operator's channel would withhold is said in words, and kept whole in the evidence**
// (novox/hq issue 277, ADR 0234 §6): the condition that carried a resolver's address reached the
// operator as "its words are withheld" instead of the alert.
func TestASummaryCarryingAnAddressIsSaidInWordsAndKeptInTheEvidence(t *testing.T) {
k, _, _, _ := keeper(t)
before := Unsayable
var told []string
Unsayable = func(o Observation, field string, r outward.Refusal) { told = append(told, field+": "+r.What) }
t.Cleanup(func() { Unsayable = before })
raw := "AAAA for anchor.internal: no answer from 10.77.0.1: read udp 10.77.0.3:41234->10.77.0.1:53: i/o timeout"
c, err := k.Observe(t.Context(), Observation{Scope: ScopeSeat, ID: "mesh-dns-resolver.anchor", Token: "wrong",
Kind: "resolver-wrong", Machine: "anchor", Severity: Urgent, Source: "D2",
Summary: "the mesh's resolver on anchor does not answer: " + raw})
if err != nil {
t.Fatal(err)
}
if r, ok := outward.Check(c.Summary, "anchor"); !ok {
t.Fatalf("the summary kept is still withheld (%s): %q", r, c.Summary)
}
if !strings.HasPrefix(c.Summary, "the mesh's resolver on anchor does not answer: ") {
t.Errorf("the summary's words were not kept: %q", c.Summary)
}
if !strings.Contains(c.Evidence[0].Said, raw) {
t.Errorf("the evidence lost what the summary carried: %q", c.Evidence[0].Said)
}
if len(told) != 1 || !strings.HasPrefix(told[0], "summary") {
t.Errorf("the producer was not named to the test: %v", told)
}
// A summary that may leave is kept as it is said, machine names and all.
ok, err := k.Observe(t.Context(), Observation{Scope: ScopeMachine, ID: "anchor", Kind: "silent", Machine: "anchor",
Severity: Warning, Source: "S1", Summary: "anchor has not been heard from since 12:00 UTC (bound 3m0s)"})
if err != nil || ok.Summary != "anchor has not been heard from since 12:00 UTC (bound 3m0s)" {
t.Fatalf("%q %v", ok.Summary, err)
}
}