From 12319ce9ddfb13e24c07460087a24429b3147295 Mon Sep 17 00:00:00 2001 From: mesh-tools Date: Tue, 20 Oct 2026 06:59:38 +0000 Subject: [PATCH] Keep a recorded module at the build a node runs, per node: Node.Kept and Node.KeptBuilds overlay that node's own resolution and the commit it carries, never another node's (review round 1, finding 1; mirrors plan.go keepRecorded and carriedOn). Both tests failed first; the suite is 1251 tests in 1.05 s --- go/compose/input.go | 7 ++++- go/compose/network.go | 2 +- go/compose/node.go | 4 +++ go/compose/node_test.go | 59 +++++++++++++++++++++++++++++++++++++++++ go/compose/resolve.go | 21 +++++++++++++-- go/compose/rest.go | 7 ++--- 6 files changed, 93 insertions(+), 7 deletions(-) diff --git a/go/compose/input.go b/go/compose/input.go index d0508a1..d8f773d 100644 --- a/go/compose/input.go +++ b/go/compose/input.go @@ -29,7 +29,7 @@ import ( // (ADR 0446 clause 2). type Input struct { // Catalogue is the shelf: every module's manifest by name (inv.Catalogue), as it is to be composed. A recorded - // module kept at the build a machine runs (keepRecorded) is already in it, put there by the caller. + // module kept at the build a machine runs (keepRecorded) is that node's Kept, applied to its own resolution only. Catalogue map[string]catalogue.Manifest // Nodes is every node the mesh has (inv.Nodes and inv.Overlays together, one row per node). Nodes []Node @@ -176,6 +176,11 @@ type Node struct { SentBuilds map[string]string SentKnown bool Runs map[string]Run + // Kept is, by module, the manifest of a recorded or core module kept at the build this machine runs (plan.go keepRecorded: + // the caller reads it with recordedKept and ManifestAt), and KeptBuilds the commit of that build. They apply to this node's + // own resolution only, never to another node's, and a declaration carries the kept commit for the module. + Kept map[string]catalogue.Manifest + KeptBuilds map[string]string // Contract is the contract of its node-engine's newest statement (inv.HealthOf); Stated is whether it ever stated one. Contract int Stated bool diff --git a/go/compose/network.go b/go/compose/network.go index bcdc72e..ef01f70 100644 --- a/go/compose/network.go +++ b/go/compose/network.go @@ -219,7 +219,7 @@ func (c *composer) onTheNetwork() []Node { if len(n.Assigned) == 0 { continue } - got, err := catalogue.Resolve(c.in.Catalogue, n.Assigned, + got, err := catalogue.Resolve(c.shelfFor(n), n.Assigned, catalogue.Node{Name: n.Name, Site: n.Place.Site, Capabilities: n.Capabilities}, catalogue.World{Unchecked: true, Holdings: c.in.Holdings}) if err != nil { diff --git a/go/compose/node.go b/go/compose/node.go index d9f82f6..982e5fc 100644 --- a/go/compose/node.go +++ b/go/compose/node.go @@ -247,6 +247,10 @@ func (c *composer) carriedOn(node Node, plan catalogue.Resolution, leftOut map[s continue } out[m.Module] = c.in.CurrentBuilds[m.Module] + // A module kept at the build the machine runs is carried at that build (plan.go carriedOn). + if kept, ok := node.KeptBuilds[m.Module]; ok && kept != "" { + out[m.Module] = kept + } } return out } diff --git a/go/compose/node_test.go b/go/compose/node_test.go index a0b82bf..5a79bf0 100644 --- a/go/compose/node_test.go +++ b/go/compose/node_test.go @@ -323,3 +323,62 @@ func TestIssue263AnIdentityARealMachineNameOverflowsIsARefusal(t *testing.T) { t.Errorf("withheld %+v", d.Withheld) } } + +// Round 1, finding 1 (plan.go keepRecorded): a recorded module kept at the build a machine runs is that machine's own resolution +// and no other's; the commit carried for it is the kept one. +func TestAKeptModuleChangesOnlyTheResolutionOfTheNodeThatKeepsIt(t *testing.T) { + in := aSmallMesh(t) + in.Catalogue["tool"] = catalogue.Manifest{Module: "tool", Version: "2"} + for _, name := range []string{"anchor", "laptop"} { + n := nodeOf(t, &in, name) + n.Assigned = append(n.Assigned, "tool") + } + anchor := nodeOf(t, &in, "anchor") + anchor.Kept = map[string]catalogue.Manifest{"tool": {Module: "tool", Version: "1"}} + versionOn := func(in Input, node string) string { + t.Helper() + got, err := Resolve(in, node) + if err != nil { + t.Fatal(err) + } + for _, m := range got.Plan.Modules { + if m.Module == "tool" { + return m.Version + } + } + t.Fatalf("no tool on %s", node) + return "" + } + if v := versionOn(in, "anchor"); v != "1" { + t.Errorf("the node that keeps it runs version %s", v) + } + if v := versionOn(in, "laptop"); v != "2" { + t.Errorf("another node runs version %s: a kept build leaked", v) + } + if got := in.Catalogue["tool"].Version; got != "2" { + t.Errorf("the Input's catalogue was changed: %s", got) + } + // And the commit carried for it is the kept one. + c := newComposer(in) + plan := catalogue.Resolution{Modules: []catalogue.Manifest{{Module: "tool"}, {Module: "other"}}} + in.CurrentBuilds = map[string]string{"tool": "current", "other": "current-other"} + c = newComposer(in) + node := Node{KeptBuilds: map[string]string{"tool": "kept-commit"}} + if got := c.carriedOn(node, plan, nil); got["tool"] != "kept-commit" || got["other"] != "current-other" { + t.Errorf("carried builds are %v", got) + } +} + +// And the rest of the mesh, as a node composes against it, offers what each other node's kept build offers, not the current one. +func TestTheRestOfTheMeshReadsEachOtherNodesKeptBuild(t *testing.T) { + in := threeNodes(t) + w, _, _, err := TheRestOfTheMesh(in, "laptop") + if err != nil || len(providers(w, "s3-bucket")) != 1 { + t.Fatalf("before: %v %v", providers(w, "s3-bucket"), err) + } + nodeOf(t, &in, "anchor").Kept = map[string]catalogue.Manifest{"objects": {Module: "objects", Version: "0"}} + w, _, _, err = TheRestOfTheMesh(in, "laptop") + if err != nil || len(providers(w, "s3-bucket")) != 0 { + t.Errorf("a node keeping a build that offers nothing still offers: %v %v", providers(w, "s3-bucket"), err) + } +} diff --git a/go/compose/resolve.go b/go/compose/resolve.go index 344666b..9154f45 100644 --- a/go/compose/resolve.go +++ b/go/compose/resolve.go @@ -45,7 +45,7 @@ func (c *composer) planFor(nodeName string) (Resolved, error) { if err != nil { return Resolved{}, err } - shelf := c.in.Catalogue + shelf := c.shelfFor(node) var out Resolved // The credential for each thing this node takes from elsewhere. @@ -195,7 +195,7 @@ func (c *composer) resolution(nodeName string) (catalogue.Resolution, error) { if err != nil { return catalogue.Resolution{}, err } - shelf := c.in.Catalogue + shelf := c.shelfFor(node) world, _, _, err := c.theRestOfTheMesh(nodeName) if err != nil { return catalogue.Resolution{}, err @@ -225,6 +225,23 @@ func (c *composer) resolution(nodeName string) (catalogue.Resolution, error) { return resolved, nil } +// shelfFor is the catalogue as one node composes its own set: the Input's, with each recorded or core module the node keeps at the +// build it runs put in place of the current one (plan.go keepRecorded). A copy: the Input's catalogue, and so every other node's +// resolution, is untouched. +func (c *composer) shelfFor(node Node) map[string]catalogue.Manifest { + if len(node.Kept) == 0 { + return c.in.Catalogue + } + shelf := make(map[string]catalogue.Manifest, len(c.in.Catalogue)+len(node.Kept)) + for name, m := range c.in.Catalogue { + shelf[name] = m + } + for name, m := range node.Kept { + shelf[name] = m + } + return shelf +} + // licencesFor is what this node can be answered with by record, and what it was put on. A mesh with no licences at all // is the ordinary case and not an error. func (c *composer) licencesFor(node string) (map[string][]catalogue.Record, map[string]map[string]catalogue.Record) { diff --git a/go/compose/rest.go b/go/compose/rest.go index f7509e2..fd5aa25 100644 --- a/go/compose/rest.go +++ b/go/compose/rest.go @@ -71,6 +71,7 @@ func (c *composer) theRestOfTheMesh(exclude string) (catalogue.World, Passes, [] node catalogue.Node assigned []string pins map[string]catalogue.Chosen + shelf map[string]catalogue.Manifest } var others []candidate // Every machine's assignments, before the holdings are read by any resolution (novox/hq ADR 0449 clause 4): a seat @@ -94,14 +95,14 @@ func (c *composer) theRestOfTheMesh(exclude string) (catalogue.World, Passes, [] others = append(others, candidate{ node: catalogue.Node{Name: n.Name, Site: n.Place.Site, Capabilities: n.Capabilities, At: onNetwork[n.Name], PublicDomain: n.PublicDomain}, - assigned: theirs, pins: n.Pins}) + assigned: theirs, pins: n.Pins, shelf: c.shelfFor(n)}) } var left []LeftOut offered := map[string][]catalogue.Provider{} var firstHeld []catalogue.Held for _, o := range others { - got, err := catalogue.Resolve(shelf, o.assigned, o.node, catalogue.World{Unchecked: true, Holdings: holdings}) + got, err := catalogue.Resolve(o.shelf, o.assigned, o.node, catalogue.World{Unchecked: true, Holdings: holdings}) if err != nil { // Said, not skipped: a machine dropped here offers nothing and holds nothing. left = append(left, LeftOut{Node: o.node.Name, Err: err}) @@ -138,7 +139,7 @@ func (c *composer) theRestOfTheMesh(exclude string) (catalogue.World, Passes, [] // Each machine is resolved with its own pins, as its plan is. theirs := world theirs.Pinned = o.pins - got, err := catalogue.Resolve(shelf, o.assigned, o.node, theirs) + got, err := catalogue.Resolve(o.shelf, o.assigned, o.node, theirs) if err != nil { // Said only for the whole-mesh view: with one machine excluded, the view is partial by design. if exclude == "" {