diff --git a/cmd/mesh-host/nodeagent_integration_test.go b/cmd/mesh-host/nodeagent_integration_test.go index ffc9813..06bc6c8 100644 --- a/cmd/mesh-host/nodeagent_integration_test.go +++ b/cmd/mesh-host/nodeagent_integration_test.go @@ -113,7 +113,7 @@ func (b *itBed) start(trustOperator bool) (stop func()) { } } b.session = &link.Session{Membership: link.Membership{Node: itNode}, Timeout: 2 * time.Second, - Say: b.say} + Say: b.say, Keys: func() link.RecordKeys { return link.RecordKeys{} }} b.session.Attach(context.Background(), link.OverNATS{Conn: b.conn, JS: b.js, Node: itNode}) ctx, cancel := context.WithCancel(context.Background()) done := make(chan struct{}) @@ -316,7 +316,8 @@ func TestNatsTheHandPathAppliesACompositionTheOperatorSigned(t *testing.T) { // Issue 1126 (ADR 0547 §2, the milestone's H8): the node agent started whole against a real bus, once the catalogue // places its node (the placement written after it started), composes a module from its want, the indexer's manifest, its record and its mesh-config value, and applies it, // with no key in the controller's compositions; a changed config value is composed again on its event and applied as a -// revision of what runs. +// revision of what runs. And a catalogue pull request's placement candidate equal to its placement is answered "same" +// in the node's own key, and nothing is applied for it (ADR 0559 §4, subtask 10 of the milestone). func TestNatsANodeThePlacementNamesComposesAndAppliesItsOwn(t *testing.T) { b := newItBed(t) kvOf := func(bucket string) nats.KeyValue { @@ -329,6 +330,11 @@ func TestNatsANodeThePlacementNamesComposesAndAppliesItsOwn(t *testing.T) { placement, modules, manifests := kvOf(delivery.PlacementBucket), kvOf(delivery.ModulesBucket), kvOf(delivery.ManifestsBucket) resolved := kvOf(selfcompose.ResolvedBucket) kvOf(delivery.BuilderBuildsBucket) + candidates := kvOf(delivery.PlacementCandidatesBucket) + // The controller's stream the node's facts go into, so the node's key carries the facts it says. + if _, err := b.js.AddStream(&nats.StreamConfig{Name: "CONTROL", Subjects: []string{link.ReportSubject(itNode)}}); err != nil { + t.Fatal(err) + } path := filepath.Join(b.dir, "web.conf") man := `{"module":"web","version":"1","config":{"greeting":{"type":"string","description":"what the file says","default":"hello"}},` + `"resources":[{"id":"conf","type":"file","path":"` + path + `","mode":"0644","content":"greeting=${config:greeting}\n"}]}` @@ -385,4 +391,37 @@ func TestNatsANodeThePlacementNamesComposesAndAppliesItsOwn(t *testing.T) { h := b.have("web") return h.FP == fp && h.Composed != first && h.Phase == delivery.PhaseRunning }) + + // The node says its facts (its record, as the test wrote it), then a candidate equal to its placement is written: the + // node answers "same" at the candidate's head and revision in its own key, and applies nothing. + b.session.SayFacts(context.Background(), link.Report{Profile: map[string]any{"capabilities": []any{ + map[string]any{"name": "service-manager", "present": true}}}}) + second := b.have("web").Composed + same, _ := delivery.DecodePlacement(place) + craw, err := delivery.EncodePlacementCandidate(delivery.PlacementCandidate{Group: "feat/x", + Pull: delivery.Head{Repository: delivery.CatalogueRepository, Number: 1, Head: "h1"}, Placement: &same}) + if err != nil { + t.Fatal(err) + } + ckey, _ := delivery.PlacementCandidateKey("feat/x", itNode) + crev, err := candidates.Put(ckey, craw) + if err != nil { + t.Fatal(err) + } + var answer delivery.CandidateAnswer + within(t, 10*time.Second, "the node did not answer its candidate", func() bool { + e, err := b.haves.Get(hkey) + if err != nil { + return false + } + n, err := delivery.DecodeHaveNode(e.Value()) + answer = n.Candidates["feat/x"] + return err == nil && answer.Head == "h1" && n.Record != nil + }) + if answer.Verdict != delivery.CandidateSame || answer.Revision != crev || len(answer.Same) != 1 || answer.Same[0] != "web" { + t.Errorf("the answer to a candidate equal to the placement: %+v", answer) + } + if h := b.have("web"); h.Composed != second || b.file("web") != "greeting=salut\n" { + t.Errorf("a candidate moved what runs: %+v, %q", h, b.file("web")) + } } diff --git a/go.mod b/go.mod index dee61a9..5d3caf3 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/novox/mesh-host go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027012259-a37d2622ed56 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027012704-062c29c3a22f github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 golang.org/x/crypto v0.57.0 diff --git a/go.sum b/go.sum index 34f6ef5..c719ba9 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027011927-ba398e2460ef h1:uMqybf/s git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027011927-ba398e2460ef/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027012259-a37d2622ed56 h1:PUc5s3oaWEg0uyVmXhJSSK3x0IXZBXWGqq30U/CSx30= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027012259-a37d2622ed56/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027012704-062c29c3a22f h1:AKzV973UTWe0OYFoIjgGu0f+B26xh8QsAHzCpsTIjM8= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027012704-062c29c3a22f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=