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)
mesh/delivery merged: source.main moved to novox/mesh-sdk@47b1dcf08f85
mesh/delivery-group group fix/1126-shadow-all merged: every member is merged
mesh/check checking e32d77753024 on d48365aed010 | novox/mesh-sdk#110 in group fix/1126-shadow-all

This commit is contained in:
2026-10-27 23:32:31 +01:00
parent 472d5f3144
commit e32d777530
4 changed files with 16 additions and 8 deletions
@@ -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; // 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, // 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. // 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() ctx := context.Background()
image := "alpine@sha256:" + strings.Repeat("a", 64) image := "alpine@sha256:" + strings.Repeat("a", 64)
manifest := func(module string) []byte { 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 // 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). // "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{ in := compose.Input{Catalogue: map[string]catalogue.Manifest{
"distribution": {Module: "distribution", Serves: map[string]map[string]any{catalogue.ArtifactStoreProvision: {"port": 5000}}}}, "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"}}}} Nodes: []compose.Node{{Name: "g14", Assigned: []string{"adwaita"}}, {Name: "novox", Assigned: []string{"distribution"}}}}
+4
View File
@@ -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 // 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. // composes; a node reads only its own config values; what cannot be composed is refused, naming it.
func TestInput(t *testing.T) { 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() ctx := context.Background()
xterm, err := os.ReadFile("../../catalogue/testdata/catalogue-835bf22b-xterm.module.json") xterm, err := os.ReadFile("../../catalogue/testdata/catalogue-835bf22b-xterm.module.json")
if err != nil { if err != nil {
+6 -4
View File
@@ -134,8 +134,9 @@ type CandidateAnswer struct {
Refused string `json:"refused,omitempty"` 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 // 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 // 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 // (issue 1126: the seed's revert). Same, Differs, Added and Removed then compare the controller's compositions at
// with the candidate, and judges nothing of what the controller composes. // 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"` 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 { 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") 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) { if a.Returns != "" && (a.Refused != "" || len(a.Refusals) > 0) {
return fmt.Errorf("delivery: a candidate the node returns to the controller's compositions on names what it composed") 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 { for _, d := range a.Differs {
if d.Module == "" || d.Why == "" { if d.Module == "" || d.Why == "" {
+4 -2
View File
@@ -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 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, // 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). // 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, n := HaveNode{Composed: "sha256:" + strings.Repeat("a", 64), Sealed: []string{"box"}, Candidates: answers,
Composes: ComposesOwn, ComposesWhy: "the catalogue places it (placement.n1)"} 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"}}}, "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"}}}, "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"}}, "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"}, "returns refused": {Head: "h", Verdict: CandidateRefused, Returns: ReturnsToController, Refused: "x"},
} { } {
if _, err := EncodeHaveNode(HaveNode{Candidates: map[string]CandidateAnswer{"g": a}}); err == nil { if _, err := EncodeHaveNode(HaveNode{Candidates: map[string]CandidateAnswer{"g": a}}); err == nil {