No change to a machine takes effect unseen (hq ADR 0217, to-be 44)
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
mesh/delivery-group group feat/no-change-takes-effect-unseen delivered: every member is delivered

Two incidents had one shape: a change took effect that nobody saw first (hq issues 241, 304).
Three guards, each silent when nothing is at stake:

- settings show [--history], and a set that answers each key it adds, changes and removes, and
  refuses a removal unless --replace; the replaced or cleared layer is kept in settings_history,
  in the same transaction as the write (migration 0078).
- every send keeps a summary of what it sent (no file content), plan <node> --diff compares with
  it, and push with no machine is refused unless --all.
- a push that would give a running container's mount another host directory holds that machine,
  naming the module, mount and both directories, until push <node> --move <module>; a named push's
  cascade is held the same way.

Rebased onto main and fitted to it: the hold runs before the push says what it recreates, a whole
push still says so first and leaves machines waiting for a gate, the verb's push with no machine is
still --behind and a push still needs why. The verbs take plan diff, settings replace and history,
and push move beside a machine, each refused where it cannot take effect.
This commit is contained in:
jochen
2026-10-08 01:44:43 +02:00
parent 5ddc59cd32
commit 4499e85476
18 changed files with 1073 additions and 75 deletions
+11 -11
View File
@@ -203,7 +203,7 @@ func TestANamedPushLeavesAMachineAPolicyHoldsBack(t *testing.T) {
}
compose := composeForPush(open, gens)
d := &recordedDelivery{inv: inv}
if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, compose, d, ""); err != nil {
if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, compose, d, "", nil); err != nil {
t.Fatal(err)
}
if builds, known, err := inv.SentBuilds(ctx, "laptop"); err != nil || !known || builds["resolver"] != "c1c1c1c1c1" {
@@ -221,13 +221,13 @@ func TestANamedPushLeavesAMachineAPolicyHoldsBack(t *testing.T) {
// The change merges; the policy is record. `push anchor` sends the anchor...
aResolver(t, open, "c2c2c2c2c2", asked.Add(time.Minute))
d.declared = nil
if _, err := sendRound(ctx, open, []string{"anchor"}, compose, d, ""); err != nil {
if _, err := sendRound(ctx, open, []string{"anchor"}, compose, d, "", nil); err != nil {
t.Fatal(err)
}
// ...and its cascade leaves the laptop, saying so.
var said bytes.Buffer
d.declared = nil
refused, err := flushBehind(ctx, open, mustNodes(t, open), map[string]bool{"anchor": true}, compose, d, "", &said)
refused, err := flushBehind(ctx, open, mustNodes(t, open), map[string]bool{"anchor": true}, compose, d, "", nil, &said)
if err != nil || len(refused) != 0 {
t.Fatalf("the cascade failed: %v %v", refused, err)
}
@@ -248,13 +248,13 @@ func TestANamedPushLeavesAMachineAPolicyHoldsBack(t *testing.T) {
// is held, and that what else it is owed waits with it.
aThirdMachine(t, open)
d.declared = nil
if _, err := sendRound(ctx, open, []string{"spare"}, compose, d, ""); err != nil {
if _, err := sendRound(ctx, open, []string{"spare"}, compose, d, "", nil); err != nil {
t.Fatal(err)
}
d.declared = nil
said.Reset()
if _, err := flushBehind(ctx, open, mustNodes(t, open), map[string]bool{"anchor": true, "spare": true},
compose, d, "", &said); err != nil {
compose, d, "", nil, &said); err != nil {
t.Fatal(err)
}
if len(d.declared) != 0 || digestOfLaptop() != before {
@@ -271,7 +271,7 @@ func TestANamedPushLeavesAMachineAPolicyHoldsBack(t *testing.T) {
}
said.Reset()
if _, err := flushBehind(ctx, open, mustNodes(t, open), map[string]bool{"anchor": true, "spare": true},
compose, d, "", &said); err != nil {
compose, d, "", nil, &said); err != nil {
t.Fatal(err)
}
if len(d.declared) != 0 || !strings.Contains(said.String(), "has passed no gate yet") {
@@ -284,7 +284,7 @@ func TestANamedPushLeavesAMachineAPolicyHoldsBack(t *testing.T) {
}
said.Reset()
if _, err := flushBehind(ctx, open, mustNodes(t, open), map[string]bool{"anchor": true, "spare": true},
compose, d, "", &said); err != nil {
compose, d, "", nil, &said); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(d.declared, []string{"laptop"}) || digestOfLaptop() == before {
@@ -311,18 +311,18 @@ func TestANamedPushStillSendsAConsequenceNothingHolds(t *testing.T) {
}
compose := composeForPush(open, gens)
d := &recordedDelivery{inv: inv}
if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, compose, d, ""); err != nil {
if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, compose, d, "", nil); err != nil {
t.Fatal(err)
}
// `push spare`, the machine just placed: the others' peers change with it.
aThirdMachine(t, open)
if _, err := sendRound(ctx, open, []string{"spare"}, compose, d, ""); err != nil {
if _, err := sendRound(ctx, open, []string{"spare"}, compose, d, "", nil); err != nil {
t.Fatal(err)
}
d.declared = nil
var said bytes.Buffer
if _, err := flushBehind(ctx, open, mustNodes(t, open), map[string]bool{"spare": true}, compose, d, "", &said); err != nil {
if _, err := flushBehind(ctx, open, mustNodes(t, open), map[string]bool{"spare": true}, compose, d, "", nil, &said); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(d.declared, []string{"anchor", "laptop"}) {
@@ -342,7 +342,7 @@ func TestANamedPushStillSendsAConsequenceNothingHolds(t *testing.T) {
}
d.declared = nil
said.Reset()
if _, err := flushBehind(ctx, open, mustNodes(t, open), map[string]bool{"spare": true}, compose, d, "", &said); err != nil {
if _, err := flushBehind(ctx, open, mustNodes(t, open), map[string]bool{"spare": true}, compose, d, "", nil, &said); err != nil {
t.Fatal(err)
}
// The anchor, the hub, may still be settling from the machine placed above; the laptop is the