diff --git a/internal/compose/frombus/frombustest/frombustest_test.go b/internal/compose/frombus/frombustest/frombustest_test.go index 4264ff3..b6b8cdf 100644 --- a/internal/compose/frombus/frombustest/frombustest_test.go +++ b/internal/compose/frombus/frombustest/frombustest_test.go @@ -228,7 +228,11 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) { for _, row := range []struct { own, placed bool // the account read from the running own composition, else the client's running process; the client placed userless bool // the client placed and not held, its process naming no user: it composes no account - }{{true, true, false}, {false, true, false}, {true, false, false}, {false, false, false}, {true, true, true}} { + // filledHeld holds every module with a filled file (restic, its build not done) and not the client: the node still + // names the account (issue 774, ace 2026-11-04: its build modules and ssh-client held, no filled file, no account) + filledHeld bool + }{{true, true, false, false}, {false, true, false, false}, {true, false, false, false}, {false, false, false, false}, + {true, true, true, false}, {true, true, false, true}} { own := row.own runs := running if !own { // what runs names no account of its own: the client's running process says it @@ -262,6 +266,8 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) { built.Manifest = mod.Manifest bb.Builds["mesh-provisioner-client "+fps["mesh-provisioner-client"]] = &built want = "" + } else if row.filledHeld { + bb.Builds["restic "+fps["restic"]] = &delivery.BuilderBuild{State: delivery.BuildBuilding} } else if row.placed { // held: its build not done while it runs bb.Builds["mesh-provisioner-client "+fps["mesh-provisioner-client"]] = &delivery.BuilderBuild{State: delivery.BuildBuilding} @@ -273,13 +279,16 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) { want = "" } after, _, held, err := frombus.Compose(context.Background(), bb, frombus.Mesh{}, frombus.Scope{Node: Node, Running: runs}, fps) - if _, isHeld := held["mesh-provisioner-client"]; err != nil || isHeld != (row.placed && !row.userless) { + if _, isHeld := held["mesh-provisioner-client"]; err != nil || isHeld != (row.placed && !row.userless && !row.filledHeld) { t.Fatalf("%+v: held %v: %v", row, held, err) } if got := after[ownKey].Own.Provisioner; got != want { t.Errorf("%+v: the node's own names %q, want %q", row, got, want) } - if row.placed && !row.userless && !reflect.DeepEqual(after[resticKey].Filled, before[resticKey].Filled) { + if _, resticHeld := held["restic"]; row.filledHeld && !resticHeld { + t.Errorf("%+v: restic held %v", row, resticHeld) + } + if row.placed && !row.userless && !row.filledHeld && !reflect.DeepEqual(after[resticKey].Filled, before[resticKey].Filled) { t.Errorf("%+v: restic's filled files %v, want %v", row, after[resticKey].Filled, before[resticKey].Filled) } } diff --git a/internal/compose/node.go b/internal/compose/node.go index 0aff1dc..9238ed0 100644 --- a/internal/compose/node.go +++ b/internal/compose/node.go @@ -115,6 +115,11 @@ func (d Declared) Body() ([]byte, error) { } if len(d.Filled) > 0 { envelope["filled_files"] = d.Filled + } + // The provisioner client's account is the node's, named whenever a client runs here, filled files or not: a node + // whose every filled-file module is held this pass names none of its files, and still keeps them, so the client's + // writes of them must be accepted (issue 774, ace 2026-11-04: its build modules and ssh-client held, no account). + if d.Provisioner != "" { envelope["provisioner_account"] = d.Provisioner } // An empty declaration is deliberate: the node owns nothing the mesh put there (novox/hq issue 127), and the host refuses an @@ -202,9 +207,7 @@ func (c *composer) declaration(nodeName string, mode Mode) (Declared, error) { Filled: composed.Filled, LocalNetwork: resolution.LocalNetworkPorts(rules, with.LocalNetworks), CoreBoxesWanted: rendered.CoreBoxesWanted, Said: rendered.Said, Sequence: node.Sequence, Epoch: node.Epoch, Generation: node.Generation} - if len(composed.Filled) > 0 { - out.Provisioner = with.Provisioner - } + out.Provisioner = with.Provisioner // A handed module only pulled in by another keeps what requires it, for its value of its own in the assignments mirror (issue 765). for module := range composed.Plans { if by := resolution.RequiredBy(plan, module); by != "" {