Compose each machine in the merge gate with its engine's contract, so moved modules are handed there as on the mesh (issue 969)

This commit is contained in:
fix-969
2026-10-16 23:12:16 +02:00
parent 1cdf12df00
commit 2f2a8bb57e
5 changed files with 61 additions and 1 deletions
+6
View File
@@ -287,6 +287,12 @@ func gatherFacts(ctx context.Context, open *stores, busVersion string) (snapshot
return snapshot.Facts{}, err
}
m.Architecture, m.Kernel = reported.Architecture, reported.Kernel
// Whether its node-engine takes handed files is its contract (issue 969).
if stated, had, err := inv.HealthOf(ctx, n.Name); err != nil {
return snapshot.Facts{}, err
} else if had {
m.Contract = stated.Contract
}
outward, err := inv.OutwardLinksOf(ctx, n.Name)
if err != nil {
return snapshot.Facts{}, err
+16
View File
@@ -1017,6 +1017,13 @@ func raiseFromFacts(ctx context.Context, open *stores, f snapshot.Facts, shelf m
"kernel": m.Kernel, "capabilities": caps}); err != nil {
return notes, err
}
// What its node-engine takes, as the mesh composes it today (issue 969): without the statement the gate
// composes a moved module the old way on a machine the mesh hands it to.
if stated, ok := standInHealth(m, f.Taken); ok {
if _, err := inv.RecordHealth(ctx, stated); err != nil {
return notes, err
}
}
for _, record := range []func(context.Context, string, string) error{inv.RecordSealingKey, inv.RecordOverlayKey} {
key, err := standInKey()
if err != nil {
@@ -1355,3 +1362,12 @@ func firstOr(items, otherwise []string) string {
}
return "it says nothing more"
}
// standInHealth is the statement the gate gives a machine of the snapshot so that it composes as the mesh does:
// only its contract, which says whether it takes handed files (issue 969). False for a snapshot that carries none.
func standInHealth(m snapshot.Machine, taken time.Time) (inventory.NodeHealth, bool) {
if m.Contract == 0 {
return inventory.NodeHealth{}, false
}
return inventory.NodeHealth{Node: m.Name, Contract: m.Contract, SaidAt: taken, HeardAt: taken}, true
}
@@ -0,0 +1,28 @@
package main
import (
"testing"
"time"
snapshot "github.com/novox/mesh-controller/internal/facts"
"github.com/novox/mesh-controller/internal/link"
)
// Issue 969: the gate composes a machine as the mesh does, so a moved module is handed where the machine takes
// handed files and composed the old way where it does not.
func TestIssue969TheGateGivesAMachineItsContractSoItTakesHandedFiles(t *testing.T) {
taken := time.Date(2026, 10, 16, 12, 0, 0, 0, time.UTC)
stated, ok := standInHealth(snapshot.Machine{Name: "f35", Contract: link.HandedContract}, taken)
if !ok || !takesHanded(stated, ok) {
t.Fatalf("a machine whose engine takes handed files must be given that in the gate: %+v, %v", stated, ok)
}
old, ok := standInHealth(snapshot.Machine{Name: "e12", Contract: link.HandedContract - 1}, taken)
if !ok || takesHanded(old, ok) {
t.Fatalf("a machine whose engine is older must not take handed files: %+v, %v", old, ok)
}
none, ok := standInHealth(snapshot.Machine{Name: "d01"}, taken)
if ok || takesHanded(none, ok) {
t.Fatalf("a snapshot that carries no contract states nothing: %+v, %v", none, ok)
}
}
+7 -1
View File
@@ -1126,7 +1126,13 @@ func engineTakesHanded(ctx context.Context, inv *inventory.Inventory, node strin
if err != nil {
return false, err
}
return had && stated.Contract >= link.HandedContract, nil
return takesHanded(stated, had), nil
}
// takesHanded is whether a machine's newest statement (had: it ever stated one) is of an engine that writes a
// filled file on the provisioner client's word alone.
func takesHanded(stated inventory.NodeHealth, had bool) bool {
return had && stated.Contract >= link.HandedContract
}
// provisionerAccount is the account the provisioner's client runs as on this machine (novox/hq ADR 0349 §4): the user