S15: a hand act a person decides by design is no repair, read from the verb that recorded it

Two planned bus upgrades raised healer-wanted, though ADR 0236 never lets
the mesh roll the bus. Instead of naming one more cause, the hand-act
verbs are one table saying which record a person's decision (retire
approve/reject, cleanup delete, bus upgrade, upgrade release-backlog, and
secret rotate after a leak); S15 and `hand-acts` skip those, push and the
other repairs keep counting. Conditions already open for them clear on the
next tick.
This commit is contained in:
jochen
2026-10-06 20:13:42 +02:00
parent 4635341a9d
commit b1016e5c66
3 changed files with 202 additions and 19 deletions
+129 -7
View File
@@ -3,6 +3,9 @@ package main
import (
"context"
"errors"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"testing"
@@ -161,16 +164,135 @@ func TestARepeatedHandActNamesItsCauseAndItsHealer(t *testing.T) {
}
}
// **Approving a retirement and deleting what was retired are a person's decision by design** (ADR
// 0230): repeated, they are not a healer wanted.
func TestARetirementDecisionRepeatedWantsNoHealer(t *testing.T) {
// actOf is an act in the log recorded by a verb, with its cause.
func actOf(at time.Time, verb, cause string) link.HandAct {
a := actByHand(at, cause)
a.Verb, a.Args = verb, nil
return a
}
// **An act a person decides by design is no repair** (handActVerbs): approving or rejecting a
// retirement and deleting what was retired (ADR 0230), a bus upgrade and a backlog released (ADR 0236),
// and a rotation after a leak — repeated, none is a healer wanted, whatever cause it gives.
func TestAPersonsDecisionRepeatedWantsNoHealer(t *testing.T) {
now := time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC)
for _, c := range []struct{ verb, cause string }{
{"retire approve", kindRetireWaiting}, {"retire reject", kindRetireWaiting},
{"cleanup delete", kindCleanupWaiting}, {"bus upgrade", "bus-upgrade"},
{"bus upgrade", "a word the person chose"}, {"upgrade release-backlog", "upgrade release-backlog"},
{"secret rotate", causeLeakedInLogs},
} {
f := calm(now)
f.handActs = []link.HandAct{actOf(now.Add(-26*time.Hour), c.verb, c.cause),
actOf(now.Add(-time.Hour), c.verb, c.cause)}
if got := watchHandActs(f); len(got) != 0 {
t.Errorf("%s (cause %s) repeated asked for a healer: %+v", c.verb, c.cause, got)
}
}
}
// **What repairs still counts**: a push by hand above all (ADR 0236 exists to end it), a rotation for
// any cause but a leak, an act recorded outside the mesh whatever cause it names, and a verb the table
// does not know.
func TestARepairRepeatedStillWantsAHealer(t *testing.T) {
now := time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC)
for _, c := range []struct{ verb, cause string }{
{"push", "push"}, {"plans close", "plans close"}, {"conditions silence", "consumer-behind"},
{"secret rotate", "stopped-working"}, {"hand-act record", "bus-upgrade"},
{"hand-act record", kindCleanupWaiting}, {"a verb nobody listed", "x"},
} {
f := calm(now)
f.handActs = []link.HandAct{actOf(now.Add(-26*time.Hour), c.verb, c.cause),
actOf(now.Add(-time.Hour), c.verb, c.cause)}
if got := watchHandActs(f); len(got) != 1 || got[0].Kind != "healer-wanted" {
t.Errorf("%s (cause %s) repeated wanted no healer: %+v", c.verb, c.cause, got)
}
}
// A decision does not make up the second of a cause a repair recorded once.
f := calm(now)
f.handActs = []link.HandAct{actByHand(now.Add(-2*time.Hour), kindRetireWaiting),
actByHand(now.Add(-time.Hour), kindRetireWaiting), actByHand(now.Add(-time.Hour), kindCleanupWaiting),
actByHand(now.Add(-time.Minute), kindCleanupWaiting)}
f.handActs = []link.HandAct{actOf(now.Add(-26*time.Hour), "hand-act record", "bus-upgrade"),
actOf(now.Add(-time.Hour), "bus upgrade", "bus-upgrade")}
if got := watchHandActs(f); len(got) != 0 {
t.Fatalf("a person's decision asked for a healer: %+v", got)
t.Errorf("one repair and one decision asked for a healer: %+v", got)
}
}
// **A healer-wanted already open for a decision clears on the next tick** — the log of 2026-10-06:
// a bus upgrade recorded after the fact and one through its verb, and two rotations after a leak.
func TestAHealerWantedOpenForADecisionClearsOnTheNextTick(t *testing.T) {
now := time.Date(2026, 10, 6, 18, 0, 0, 0, time.UTC)
store := conditions.NewInMemory()
k := conditions.NewKeeper(t.Context(), conditions.Options{Store: store, History: store,
Teller: &conditions.Told{}, Now: func() time.Time { return now }})
defer k.Close(context.Background())
// What the build before this one raised, as it raised it.
var before []conditions.Observation
for _, cause := range []string{"bus-upgrade", causeLeakedInLogs} {
before = append(before, conditions.Observation{Scope: conditions.ScopeMesh, ID: "hand-acts." + cause,
Token: "healer-wanted", Kind: "healer-wanted", Severity: conditions.Warning, Summary: "repaired twice"})
}
if err := k.Reconcile(t.Context(), "S15", before); err != nil {
t.Fatal(err)
}
if open, _ := k.Open(t.Context()); len(open) != 2 {
t.Fatalf("the conditions of the build before were not open: %+v", open)
}
f := calm(now)
f.handActs = []link.HandAct{actOf(now.Add(-11*time.Hour), "hand-act record", "bus-upgrade"),
actOf(now.Add(-8*time.Hour), "secret rotate", causeLeakedInLogs),
actOf(now.Add(-8*time.Hour), "secret rotate", causeLeakedInLogs),
actOf(now.Add(-time.Hour), "bus upgrade", "bus-upgrade")}
w := &watchdogs{keeper: k, started: now.Add(-time.Hour)}
w.see(t.Context(), f)
if open, _ := k.Open(t.Context()); len(open) != 0 {
t.Fatalf("a healer-wanted for a person's decision stayed open: %+v", open)
}
}
// **Every verb that writes the hand-act log is in handActVerbs**, so whether it is a repair is said
// where S15 reads it, not left to a default nobody chose.
func TestEveryVerbThatRecordsAHandActIsInTheTable(t *testing.T) {
listed := map[string]bool{}
for _, v := range handActVerbs {
if listed[v.Verb] {
t.Errorf("%s is in the table twice", v.Verb)
}
listed[v.Verb] = true
if len(v.DecidedFor) > 0 && v.Decision == "" {
t.Errorf("%s limits a decision it does not state", v.Verb)
}
}
files, err := filepath.Glob("*.go")
if err != nil {
t.Fatal(err)
}
literal := regexp.MustCompile(`(?:\.record\(ctx, |HandAct\{Verb: |RetireApproved: |RetireRejected: |RetireDeleted: )"([^"]+)"\s*[,})]`)
composed := regexp.MustCompile(`\.record\(ctx, "([^"]+ )"\s*\+`)
found := 0
for _, name := range files {
if strings.HasSuffix(name, "_test.go") {
continue
}
body, err := os.ReadFile(name)
if err != nil {
t.Fatal(err)
}
for _, m := range literal.FindAllStringSubmatch(string(body), -1) {
found++
if !listed[m[1]] {
t.Errorf("%s records %q, which handActVerbs does not list", name, m[1])
}
}
// "plans " + stop|close, "retire " + approve|reject: some listed verb begins with it.
for _, m := range composed.FindAllStringSubmatch(string(body), -1) {
found++
if !slices.ContainsFunc(handActVerbs, func(v handActVerb) bool { return strings.HasPrefix(v.Verb, m[1]) }) {
t.Errorf("%s records %q…, which no verb of handActVerbs begins with", name, m[1])
}
}
}
if found < 12 {
t.Fatalf("found %d recording verbs, fewer than the table's own: the search no longer sees them", found)
}
}