Record a push that only moves recorded builds as the person's word, not a repair (hq issue 301)
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user