Take a registered module's new done key in, compose it and grant it; at start delete the plans' buckets and compose every node and the replaced controller (issue 1945, B3, B5, B13)

With no plan left to take a done key in, a write of the builder's keys registers the new fingerprint, composes the
module's nodes and the bus's user list, and writes its grant, so the assigner's rules can name it (ADR 0539 clauses
5 and 7). The new controller composes every node at its start, and the controller it replaced at its own
fingerprint, the operator's way back (clause 10); the merge window's, silent sends' and building plans' buckets are
no longer asserted and are deleted once, each said as the event bucket-deleted (clause 13). The assigner may read
the compositions' digests, and a node-engine hears its backup holder's backed-up.
This commit is contained in:
2026-10-24 17:11:01 +02:00
parent c40e15268e
commit 66650e315c
20 changed files with 711 additions and 239 deletions
@@ -0,0 +1,72 @@
package main
import (
"context"
"encoding/json"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/compose"
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/nats-io/nats.go/jetstream"
"github.com/novox/mesh-controller/internal/broker"
)
// fakeBuckets is the bus's buckets by name, for deleting.
type fakeBuckets map[string]bool
func (f fakeBuckets) DeleteKeyValue(_ context.Context, bucket string) error {
if !f[bucket] {
return jetstream.ErrBucketNotFound
}
delete(f, bucket)
return nil
}
// 1945/AC14 (unit half): the first start deletes the merge window's, the silent sends' and the building plans' buckets
// and says each once; a later start finds none and says nothing; a bucket of anything else is left alone.
func TestTheFirstStartDeletesThePlansBucketsAndSaysEachOnce(t *testing.T) {
bus := fakeBuckets{"mesh-controller_merge-window": true, "mesh-controller_rollout-silent": true,
"mesh-controller_plans-building": true, "mesh-controller_grants": true}
var said []string
deleted, err := deletePlanStateBuckets(context.Background(), bus, func(b string) { said = append(said, b) })
if err != nil || len(deleted) != 3 || strings.Join(said, " ") != strings.Join(broker.PlanStateBuckets, " ") {
t.Fatalf("first start: deleted %v, said %v, %v", deleted, said, err)
}
if !bus["mesh-controller_grants"] || len(bus) != 1 {
t.Errorf("the bus holds %v after the first start", bus)
}
said = nil
if deleted, err := deletePlanStateBuckets(context.Background(), bus, func(b string) { said = append(said, b) }); err != nil || len(deleted)+len(said) != 0 {
t.Errorf("a later start deleted %v and said %v (%v)", deleted, said, err)
}
}
// 1945/AC6 (unit half): the controller this one replaced is composed on its node from its own build's manifest at its
// own fingerprint: the composition holds what that manifest declares, not what the controller registered now declares.
func TestTheReplacedControllerIsComposedFromItsOwnManifest(t *testing.T) {
now := catalogue.Manifest{Module: controllerModule, Version: "6", Resources: []map[string]any{
{"id": "settings", "type": "file", "path": "/etc/mesh-controller/settings", "content": "six\n"}}}
before := catalogue.Manifest{Module: controllerModule, Version: "5", Resources: []map[string]any{
{"id": "settings", "type": "file", "path": "/etc/mesh-controller/settings", "content": "five\n"}}}
in := compose.Input{Catalogue: map[string]catalogue.Manifest{controllerModule: now},
Nodes: []compose.Node{{Name: "anchor", Assigned: []string{controllerModule}, Stated: true}}}
fp := sdkdelivery.SourcePrefix2 + strings.Repeat("5", 64)
c, err := replacedComposition(in, replacedController{node: "anchor", fp: fp, manifest: before})
if err != nil {
t.Fatal(err)
}
if c.Module != controllerModule || c.FP != fp || c.Node != "anchor" || len(c.Resources) != 1 {
t.Fatalf("composed %s at %s on %s with %d resources", c.Module, c.FP, c.Node, len(c.Resources))
}
var f struct{ Content string }
_ = json.Unmarshal(c.Resources[0], &f)
if f.Content != "five\n" {
t.Errorf("the replaced controller's composition holds %q, its own manifest says five", f.Content)
}
if in.Catalogue[controllerModule].Version != "6" {
t.Errorf("composing the replaced controller changed the Input's catalogue")
}
}