package buildrun import ( "bytes" "context" "errors" "log" "strings" "testing" "time" "github.com/nats-io/nats.go/jetstream" "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient" "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" meshwants "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" ) // placing is a value of the state `assignments` placing modules on node. func placing(t *testing.T, node string, modules ...string) (string, []byte) { t.Helper() p := meshmoduleassigner.Assignments{Node: node} for _, m := range modules { p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m}) } values, err := meshmoduleassignerclient.AssignmentsValues(p) if err != nil { t.Fatal(err) } key, _ := meshmoduleassignerclient.NodeKey(node) return key, values[key] } // The placements the builder counts nodes by (ADR 0624 §1): a settled assignments names the nodes its modules stand on, a // revision of the assignment half the same; they hold no toolchain (design 47 G §546: a toolchain is forgotten on the // node-engine seat's stopped, never on a placement). func TestPlacedNodes(t *testing.T) { old := time.Date(2026, 10, 23, 6, 29, 18, 0, time.UTC) three := []string{"g14", "novox", "shanks"} t.Run("the assignments names the counted nodes once it is settled", func(t *testing.T) { gate := NewPlacedNodes(func(string, ...any) {}) for _, node := range three { k, v := placing(t, node, testNode) if gate.Assignments(k, v, false, old) { t.Fatal("an assignments before it is settled changed the counted nodes") } } gate.Settled() if got := strings.Join(gate.Nodes(), ","); got != "g14,novox,shanks" { t.Fatalf("nodes = %q", got) } k, v := placing(t, "ace", testGo) if !gate.Assignments(k, v, false, old) || len(gate.Nodes()) != 4 { t.Fatalf("ace placed: nodes %v", gate.Nodes()) } if !gate.Assignments(k, nil, true, old.Add(time.Hour)) || len(gate.Nodes()) != 3 { t.Fatalf("ace's assignments deleted: nodes %v", gate.Nodes()) } }) t.Run("the assignment half: the first revision settles, a later one moves the nodes", func(t *testing.T) { gate := NewPlacedNodes(func(string, ...any) {}) revision := func(n int64, nodes ...string) meshwants.AssignmentRevision { at := old.Add(time.Duration(n) * time.Hour) r := meshwants.AssignmentRevision{Revision: n, Kind: "trunk", At: &at} for _, node := range nodes { r.Entries = append(r.Entries, meshwants.NodeEntryRef{Node: node, Module: testNode, Digest: "d-" + node}) } return r } if !gate.Revision(revision(7, three...)) || len(gate.Nodes()) != 3 { t.Fatalf("the first revision: nodes %v", gate.Nodes()) } if gate.Revision(revision(7, three...)) { t.Fatal("the same revision again changed the counted nodes") } if !gate.Revision(revision(9, "novox", "shanks")) || len(gate.Nodes()) != 2 { t.Fatalf("g14 taken off: nodes %v", gate.Nodes()) } }) t.Run("assignments the bus refuses is said, and the part goes on", func(t *testing.T) { var logs bytes.Buffer logger := log.New(&logs, "", 0) refused := errors.New("nats: permissions violation for publish to $JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_assignments") gate := NewPlacedNodes(logger.Printf) for _, bus := range []refusingBus{{err: refused}, {refuseOpen: true, err: refused}} { if _, err := FollowAssignmentsIn(context.Background(), bus, "acme-assigner_assignments", gate, func(context.Context) {}, logger.Printf); err != nil { t.Errorf("a refused assignments stopped the start: %v", err) } } if !strings.Contains(logs.String(), "cannot be watched") || !strings.Contains(logs.String(), "cannot be read") { t.Errorf("the refusals were not said: %q", logs.String()) } }) } // refusingBus is a bus that refuses: every open (refuseOpen), or every watch of a bucket it opens, with err. type refusingBus struct { refuseOpen bool err error } func (r refusingBus) KeyValue(context.Context, string) (jetstream.KeyValue, error) { if r.refuseOpen { return nil, r.err } return refusedWatch{err: r.err}, nil } type refusedWatch struct { jetstream.KeyValue err error } func (r refusedWatch) WatchAll(context.Context, ...jetstream.WatchOpt) (jetstream.KeyWatcher, error) { return nil, r.err }