Files
mesh-delivery/modules/mesh-delivery-builder/buildrun/standing_test.go
T
jschoubben 6d9a7b8aae Checker follows the assignments of the holder of the seat mesh-module-assigner found at run time; FollowAssignments, AssignerModule and AssignmentsBucket go
buildrun gains AssignerHolder, AssignmentsOf and SleepFor (moved from the builder's program, which uses them), so
the builder and the checker follow the holder's first-view tool, views family and state alike; the checker's
manifest consumes the seat's views assignments and reads its assignments state, its composition reads the
picture once followed. The assigner's callers list for the first view gains the checker.

Builds: design 47 G A-82, A-119, A-126, A-131
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 01:38:34 +01:00

120 lines
4.2 KiB
Go

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/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
}