Record a push that only moves recorded builds as the person's word, not a repair (hq issue 301)
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered

A recorded build moves only by a person's push (ADR 0242), so that push is
the word its upgrade policy asks for; S15 counted it as a repair and wanted a
healer for split-dns, words and uplink-verbs. The push now reads what it
carries before it is recorded and says so in its kind, and the ten pushes of
2026-10-07 are named so their three warnings clear on the next tick.
This commit is contained in:
jochen
2026-10-08 00:12:23 +02:00
parent db953e7da8
commit e92a3fe237
5 changed files with 383 additions and 3 deletions
+61
View File
@@ -3,6 +3,7 @@ package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"slices"
@@ -522,3 +523,63 @@ func TestReplay300AMergeAddingAModuleAsksForItsBuild(t *testing.T) {
t.Fatalf("a merge moving nothing the mesh holds opened a plan: %+v %v", plans, err)
}
}
// **R301 — a person's push of a held recorded build is no repair.** On 2026-10-07 the operator approved,
// machine by machine, new builds of modules whose upgrade policy records rather than rolls out — the
// resolver, the FortiClient adapter, the network managers, the packet filter, mail. A recorded build
// moves only by a person's push (ADR 0242), so each was `push <machine>` through the seat with the
// operator's yes as its why and a word for the change as its cause. The hand-act log counted every one
// as a repair, and S15 raised `mesh.hand-acts.split-dns.healer-wanted` ("repaired by hand 4 times in 14
// days … a healer is wanted"), and the same for `words` and `uplink-verbs`. The outcome asserted: a push
// that carries only held recorded builds, recorded as the push records it, wants no healer however
// often the person gives that word — through S15 itself, reading the log as the watchdog reads it.
func TestReplay301APersonsPushOfAHeldRecordedBuildIsNoRepair(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
inv := open.inventory
start := time.Now().Add(-time.Hour)
image := "registry.invalid:5000/resolver/server@sha256:" + strings.Repeat("e", 64)
if _, _, err := takeIn(ctx, inv, aContainerBuild(t, "resolver", "c1111111", catalogue.PolicyRecord, start,
map[string][2]string{"server": {image, ""}})); err != nil {
t.Fatal(err)
}
for _, node := range []string{"anchor", "laptop"} {
if _, err := inv.Assign(ctx, node, "resolver"); err != nil {
t.Fatal(err)
}
if err := inv.RecordSent(ctx, nodeID(t, open, node), "d-"+node, map[string]string{"resolver": "c1111111"}); err != nil {
t.Fatal(err)
}
}
// The merge: the resolver's new build, registered and held for a person's word.
if _, _, err := takeIn(ctx, inv, aContainerBuild(t, "resolver", "c2222222", catalogue.PolicyRecord,
start.Add(time.Minute), map[string][2]string{"server": {image, "runtime"}})); err != nil {
t.Fatal(err)
}
conn := onATestBus(t)
// The operator's yes, machine by machine, as the seat's push records it before it sends.
for _, node := range []string{"anchor", "laptop"} {
set := flag.NewFlagSet("push", flag.ContinueOnError)
why := addHandActFlags(set)
if err := set.Parse([]string{"--why", "operator's yes: the resolver's new build on " + node,
"--cause", "split-dns"}); err != nil {
t.Fatal(err)
}
why.record(ctx, "push", []string{node})
}
acts, err := link.HandActs(ctx, conn, start)
if err != nil {
t.Fatal(err)
}
if len(acts) != 2 {
t.Fatalf("the two pushes were recorded as %d act(s): %+v", len(acts), acts)
}
f := calm(time.Now())
f.handActs = acts
for _, o := range watchHandActs(f) {
if strings.Contains(o.ID, "split-dns") {
t.Fatalf("a person's push of a held recorded build was counted as a repair: %s — %s", o.ID, o.Summary)
}
}
}