Files
mesh-controller/cmd/mesh-controller/readable_test.go
T
jschoubben fcaad7271a A failure that repeats is said to be stuck
The mesh kept one report per machine, replaced, so a resource nothing can ever apply
looked like a failure that had just happened, every reconcile interval, for ever.
The row now keeps when the current failure began and how many reports in a row have
said it — the same outcome, refusal and failed resources; anything different starts
again and a clean apply clears it. Three make the machine stuck, and status says so
beside the failure, in words and in JSON (novox/hq 04-ISSUES/065, ADR 0090).
2026-09-21 17:43:24 +02:00

170 lines
6.3 KiB
Go

package main
import (
"encoding/json"
"strings"
"testing"
"time"
"github.com/novox/mesh-controller/internal/inventory"
)
// The shape something other than a person reads.
//
// Hard to change once anything is built against it, so it stays close to what the domain already
// calls things and carries no summary field that would have to be kept true.
func statusOf(t *testing.T, wrong []inventory.Doing, nodes, quiet []inventory.Node,
behind map[string][]string, sources map[string]inventory.Source) map[string]any {
t.Helper()
body, err := statusAsJSON(answers{
wrong: wrong, nodes: nodes, quiet: quiet, behind: behind, sources: sources})
if err != nil {
t.Fatal(err)
}
var parsed map[string]any
if err := json.Unmarshal(body, &parsed); err != nil {
t.Fatalf("what a board would read is not JSON: %v", err)
}
return parsed
}
func TestRefusedAndFailedStayDistinctAllTheWayOut(t *testing.T) {
// They are fixed in different places, so one word for both would send half the readers of a
// page to the wrong one.
got := statusOf(t,
[]inventory.Doing{
{Node: "one", Outcome: inventory.OutcomeRefused, Refused: "a file needs a path"},
{Node: "two", Outcome: inventory.OutcomeFailed, Applied: 3,
Failed: []inventory.FailedResource{{ID: "shell.pkg", Error: "target not found"}}},
},
[]inventory.Node{{Name: "one"}, {Name: "two"}}, nil, nil, nil)
wrong, _ := got["wrong"].([]any)
if len(wrong) != 2 {
t.Fatalf("got %v", got["wrong"])
}
first, _ := wrong[0].(map[string]any)
if first["outcome"] != inventory.OutcomeRefused || first["refused"] == nil {
t.Fatalf("a refusal did not survive: %v", first)
}
second, _ := wrong[1].(map[string]any)
if second["outcome"] != inventory.OutcomeFailed {
t.Fatalf("a failure did not survive: %v", second)
}
if second["applied"] != float64(3) {
// "Three of eight" and "none of eight" are different machines.
t.Fatalf("what did apply was not carried: %v", second)
}
}
func TestAMachineThatNeverSpokeSaysSoByOmission(t *testing.T) {
// Never heard from and quiet for a while are different situations, and a zero time would read
// as a date in 1970 on any page that formatted it.
got := statusOf(t, nil,
[]inventory.Node{{Name: "silent"}, {Name: "away", LastSeen: time.Now().Add(-3 * time.Hour)}},
[]inventory.Node{{Name: "silent"}, {Name: "away", LastSeen: time.Now().Add(-3 * time.Hour)}},
nil, nil)
quiet, _ := got["quiet"].([]any)
if len(quiet) != 2 {
t.Fatalf("got %v", got["quiet"])
}
never, _ := quiet[0].(map[string]any)
if _, said := never["lastSeen"]; said {
t.Fatalf("a machine that never spoke carries a time: %v", never)
}
away, _ := quiet[1].(map[string]any)
if _, said := away["lastSeen"]; !said {
t.Fatalf("a machine that has been quiet carries no time: %v", away)
}
}
func TestNothingWrongIsAnEmptyListRatherThanNothing(t *testing.T) {
// A page distinguishing "no machines are wrong" from "this field is missing" would have to
// handle both, and null is the one that gets forgotten.
got := statusOf(t, nil, []inventory.Node{{Name: "a", LastSeen: time.Now()}}, nil, nil, nil)
for _, key := range []string{"wrong", "quiet", "behind"} {
list, ok := got[key].([]any)
if !ok {
t.Fatalf("%q is %T, not a list", key, got[key])
}
if len(list) != 0 {
t.Fatalf("%q is not empty: %v", key, list)
}
}
// And how many machines there are, so a reader can tell "none wrong" from "none at all".
if got["machines"] != float64(1) {
t.Fatalf("got %v", got["machines"])
}
}
func TestWhatIsBehindNamesTheMachinesRunningTheOldOne(t *testing.T) {
// A module being out of date is a fact about the catalogue; machines running the old one is
// the thing with consequences.
got := statusOf(t, nil, nil, nil,
map[string][]string{"shell": {"workstation", "laptop"}},
map[string]inventory.Source{"shell": {BuiltFrom: "aaaaaaa1", Head: "bbbbbbb2"}})
behind, _ := got["behind"].([]any)
if len(behind) != 1 {
t.Fatalf("got %v", got["behind"])
}
row, _ := behind[0].(map[string]any)
if row["module"] != "shell" || row["builtFrom"] != "aaaaaaa1" || row["head"] != "bbbbbbb2" {
t.Fatalf("got %v", row)
}
on, _ := row["on"].([]any)
if len(on) != 2 {
t.Fatalf("the machines running the old one are not named: %v", row)
}
}
func TestNoSecretIsInWhatABoardReads(t *testing.T) {
// Everything here comes from the mesh's own records, which hold no readable secret — but a
// shape a page is built against is exactly where one would eventually be added for
// convenience, so this says it out loud.
body, err := statusAsJSON(answers{
wrong: []inventory.Doing{{Node: "a", Outcome: inventory.OutcomeRefused,
Refused: "resource \"x\": a file needs a path"}},
nodes: []inventory.Node{{Name: "a"}},
refused: map[string]string{"a": "nothing provides \"database\", wanted by web"},
})
if err != nil {
t.Fatal(err)
}
for _, word := range []string{"password", "secret", "sealed", "credential", "token"} {
if strings.Contains(strings.ToLower(string(body)), word) {
t.Fatalf("what a board reads carries a %q field:\n%s", word, body)
}
}
}
// A failure that has been reported identically enough times is said to be stuck, with when it
// began and how many times — so a board can tell a machine looping on something that will never
// apply from one that failed a minute ago (novox/hq 04-ISSUES/065, ADR 0090).
func TestAMachineFailingTheSameWayIsSaidToBeStuck(t *testing.T) {
began := time.Date(2026, 9, 21, 9, 0, 0, 0, time.UTC)
got := statusOf(t,
[]inventory.Doing{
{Node: "looping", Outcome: inventory.OutcomeFailed, Since: &began, Times: inventory.StuckAfter,
Failed: []inventory.FailedResource{{ID: "img", Error: "no such image"}}},
{Node: "once", Outcome: inventory.OutcomeFailed, Since: &began, Times: 1,
Failed: []inventory.FailedResource{{ID: "img", Error: "no such image"}}},
},
[]inventory.Node{{Name: "looping"}, {Name: "once"}}, nil, nil, nil)
wrong, _ := got["wrong"].([]any)
looping, _ := wrong[0].(map[string]any)
if looping["stuck"] != true || looping["times"] != float64(inventory.StuckAfter) {
t.Fatalf("three identical failures are stuck: %v", looping)
}
if since, _ := looping["since"].(string); !strings.HasPrefix(since, "2026-09-21T09:00:00") {
t.Fatalf("when it began was not carried: %v", looping)
}
once, _ := wrong[1].(map[string]any)
if once["stuck"] != false || once["times"] != float64(1) {
t.Fatalf("one failure is not stuck: %v", once)
}
}