From e32d777530247a7f8fda33dcfd3920d7a3982eea Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 27 Oct 2026 23:32:31 +0100 Subject: [PATCH] A deleting head's answer compares the controller's compositions with what runs, and is refused only without returning; fold the shadow's tests into TestInput (issue 1126, review of the shadow, round 1) --- .../frombus/{answer_test.go => answer_rows_test.go} | 4 ++-- go/compose/frombus/frombus_test.go | 4 ++++ go/delivery/placement_candidate.go | 10 ++++++---- go/delivery/placement_test.go | 6 ++++-- 4 files changed, 16 insertions(+), 8 deletions(-) rename go/compose/frombus/{answer_test.go => answer_rows_test.go} (98%) diff --git a/go/compose/frombus/answer_test.go b/go/compose/frombus/answer_rows_test.go similarity index 98% rename from go/compose/frombus/answer_test.go rename to go/compose/frombus/answer_rows_test.go index 1f6ab25..f0aa574 100644 --- a/go/compose/frombus/answer_test.go +++ b/go/compose/frombus/answer_rows_test.go @@ -17,7 +17,7 @@ import ( // Issue 1126 (the seed's shadow, round 2): a node's answer names every module it cannot compose, not the first only; // and a module folded into its own composition that names a build of the old form (de-spiegel on novox, nats, photos, // systemd-resolved: their haves name a build-... id and no fingerprint) is left to the node, carried as it runs. -func TestTheAnswerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { +func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { ctx := context.Background() image := "alpine@sha256:" + strings.Repeat("a", 64) manifest := func(module string) []byte { @@ -138,7 +138,7 @@ func TestTheAnswerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { // A node that reaches no artifact store says what its composition holds of one (issue 1126: ace and g14 answered // "reaches no artifact store", and nothing said why). -func TestNoStoreReachedSaysWhy(t *testing.T) { +func noStoreReachedSaysWhy(t *testing.T) { in := compose.Input{Catalogue: map[string]catalogue.Manifest{ "distribution": {Module: "distribution", Serves: map[string]map[string]any{catalogue.ArtifactStoreProvision: {"port": 5000}}}}, Nodes: []compose.Node{{Name: "g14", Assigned: []string{"adwaita"}}, {Name: "novox", Assigned: []string{"distribution"}}}} diff --git a/go/compose/frombus/frombus_test.go b/go/compose/frombus/frombus_test.go index 20a5ae5..ee7697d 100644 --- a/go/compose/frombus/frombus_test.go +++ b/go/compose/frombus/frombus_test.go @@ -119,6 +119,10 @@ func publicKey(t *testing.T) string { // controller's store fills for compose's own two-machine mesh (compose's aMesh: anchor the hub, laptop), and that Input // composes; a node reads only its own config values; what cannot be composed is refused, naming it. func TestInput(t *testing.T) { + // Issue 1126, the seed's shadow: every refusal named, an old-form module left to the node, and no store reached + // saying why (answer_rows_test.go). + t.Run("every refusal, and an old build left to the node", answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode) + t.Run("no store reached says why", noStoreReachedSaysWhy) ctx := context.Background() xterm, err := os.ReadFile("../../catalogue/testdata/catalogue-835bf22b-xterm.module.json") if err != nil { diff --git a/go/delivery/placement_candidate.go b/go/delivery/placement_candidate.go index e3736bc..6b28569 100644 --- a/go/delivery/placement_candidate.go +++ b/go/delivery/placement_candidate.go @@ -134,8 +134,9 @@ type CandidateAnswer struct { Refused string `json:"refused,omitempty"` // Returns is, for a head that deletes the node's placement file, what the node does then: it returns to the // controller's compositions (ReturnsToController), as a node the catalogue does not place composes none of its own - // (issue 1126: the seed's revert). The verdict is then same, and nothing else is named: the node composes nothing - // with the candidate, and judges nothing of what the controller composes. + // (issue 1126: the seed's revert). Same, Differs, Added and Removed then compare the controller's compositions at + // the node's wants with what it runs, and the verdict is theirs; a want the controller holds no composition for is + // refused (Refused, without Returns). Returns string `json:"returns,omitempty"` } @@ -195,8 +196,9 @@ func (a CandidateAnswer) check() error { if a.Refused != "" && len(a.Same)+len(a.Differs)+len(a.Added)+len(a.Removed)+len(a.Refusals) > 0 { return fmt.Errorf("delivery: a candidate the node could not compose names modules") } - if a.Returns != "" && (a.Refused != "" || len(a.Same)+len(a.Differs)+len(a.Added)+len(a.Removed)+len(a.Refusals) > 0) { - return fmt.Errorf("delivery: a candidate the node returns to the controller's compositions on names what it composed") + if a.Returns != "" && (a.Refused != "" || len(a.Refusals) > 0) { + return fmt.Errorf("delivery: a candidate the node returns to the controller's compositions on is compared, never refused: " + + "a composition the controller holds none of is refused without returning") } for _, d := range a.Differs { if d.Module == "" || d.Why == "" { diff --git a/go/delivery/placement_test.go b/go/delivery/placement_test.go index 29376d0..e0b7810 100644 --- a/go/delivery/placement_test.go +++ b/go/delivery/placement_test.go @@ -231,7 +231,9 @@ func TestPlacement(t *testing.T) { "a value": {Head: "h1", Verdict: CandidateRefused, Same: []string{"node"}, Refusals: []ModuleRefused{{Module: "db", Why: "stands in for db/password"}}}, // A head that deletes the node's placement file: the node returns to the controller's compositions (issue 1126, // the seed's revert, mesh-catalog #526). - "returns": {Head: "h1", Verdict: CandidateSame, Returns: ReturnsToController}, + "returns": {Head: "h1", Verdict: CandidateSame, Returns: ReturnsToController, Same: []string{"app", "node"}}, + "returns, the controller's differs": {Head: "h1", Verdict: CandidateDiffers, Returns: ReturnsToController, Same: []string{"node"}, + Differs: []ModuleDiffers{{Module: "app", Resource: "file:/etc/app", Why: "the resource differs"}}, Removed: []string{"old"}}, } n := HaveNode{Composed: "sha256:" + strings.Repeat("a", 64), Sealed: []string{"box"}, Candidates: answers, Composes: ComposesOwn, ComposesWhy: "the catalogue places it (placement.n1)"} @@ -297,7 +299,7 @@ func TestPlacement(t *testing.T) { "same with an added": {Head: "h", Verdict: CandidateSame, Added: []ModuleAdded{{Module: "a"}}}, "differs refusing": {Head: "h", Verdict: CandidateDiffers, Removed: []string{"a"}, Refusals: []ModuleRefused{{Module: "b", Why: "x"}}}, "refused and modules": {Head: "h", Verdict: CandidateRefused, Refused: "x", Same: []string{"a"}}, - "returns and removes": {Head: "h", Verdict: CandidateDiffers, Returns: ReturnsToController, Removed: []string{"a"}}, + "returns refusing": {Head: "h", Verdict: CandidateRefused, Returns: ReturnsToController, Refusals: []ModuleRefused{{Module: "b", Why: "x"}}}, "returns refused": {Head: "h", Verdict: CandidateRefused, Returns: ReturnsToController, Refused: "x"}, } { if _, err := EncodeHaveNode(HaveNode{Candidates: map[string]CandidateAnswer{"g": a}}); err == nil {