Files
mesh-delivery/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/toolchains_test.go
T
jschoubben 71a35a6376 MV1: move contracts and sdk under modules/, importers follow
The delivery repository's contracts and SDK live under modules/ as
mesh-delivery-contracts and mesh-delivery-sdk, their Go module paths
renamed to match the folders (the TypeScript names already did), and
every importer in mesh-delivery, mesh-catalog, mesh-host, mesh-tools
and mesh-sdk follows in the same group: a design as it should be, one
path per module, checked by the group's check building every importer.

Builds: design 47 G §427, §88

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 05:09:56 +01:00

154 lines
5.4 KiB
Go

package main
import (
"context"
"encoding/json"
"io"
"log"
"sort"
"sync"
"testing"
"time"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshbuilds"
)
// memState is the state toolchains-heard in memory.
type memState struct {
mu sync.Mutex
m map[string][]byte
}
func (s *memState) Put(k string, v []byte) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.m == nil {
s.m = map[string][]byte{}
}
s.m[k] = v
return nil
}
func (s *memState) Delete(k string) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.m, k)
return nil
}
func (s *memState) Get(k string) ([]byte, error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.m[k], nil
}
func (s *memState) Keys() ([]string, error) {
s.mu.Lock()
defer s.mu.Unlock()
var out []string
for k := range s.m {
out = append(out, k)
}
sort.Strings(out)
return out, nil
}
// msg is a delivered message as the builder's consumer hands it.
type msg struct {
jetstream.Msg
subject string
data []byte
}
func (m msg) Subject() string { return m.subject }
func (m msg) Data() []byte { return m.data }
func (m msg) Ack() error { return nil }
type heardRig struct {
h *toolchainsHeard
state *memState
said []meshbuilds.ToolchainAgreed
changed int
}
func newHeardRig(t *testing.T, state *memState) *heardRig {
t.Helper()
r := &heardRig{state: state}
r.h = newToolchainsHeard(&buildrun.ToolchainSet{Seats: testSeats()}, func(context.Context) { r.changed++ },
func(_ context.Context, e meshbuilds.ToolchainAgreed) error { r.said = append(r.said, e); return nil }, log.New(io.Discard, "", 0))
r.h.bind(context.Background(), state)
return r
}
func (r *heardRig) report(t *testing.T, seat, node, language, version, digest string) {
t.Helper()
data, _ := json.Marshal(map[string]string{"node": node, "language": language, "version": version, "recipe": "1", "digest": digest})
if !r.h.Reported(context.Background(), msg{subject: "mesh.seat." + seat + ".event.toolchain-reported." + node, data: data}) {
t.Fatal("a toolchain-reported was not taken")
}
}
func (r *heardRig) running(t *testing.T, node, module, fingerprint string, at time.Time) {
t.Helper()
data, _ := json.Marshal(nodeengine.Running{Node: node, Module: module, Fingerprint: fingerprint, At: at})
if !r.h.Running(context.Background(), msg{subject: nodeengine.SubjectRunning(node, module), data: data}) {
t.Fatal("a running was not taken")
}
}
// Design 47 G §492, §604, §472, §574, §546: the builder agrees from the toolchain-reported it heard, keeps them in
// toolchains-heard, says toolchain-agreed with the digests only when the agreed version changes, and forgets a node's on
// the node-engine seat's stopped.
func TestTheBuilderKeepsTheToolchainsItHeardAndForgetsThemOnStopped(t *testing.T) {
da, db := "sha256:a", "sha256:b"
state := &memState{}
r := newHeardRig(t, state)
r.report(t, "node-build-go", "ace", "go", "1.26.0", da)
r.report(t, "node-build-go", "g14", "go", "1.26.0", da)
r.report(t, "node-build-go", "novox", "go", "1.26.0", db)
keys, _ := state.Keys()
if len(keys) != 3 || keys[0] != "ace.go" {
t.Fatalf("the state holds %v, want the three nodes' keys <node>.go", keys)
}
if len(r.said) != 1 || r.said[0].Version != "1.26.0" || r.said[0].Language != "go" {
t.Fatalf("toolchain-agreed said %+v, want one for go 1.26.0 (said only when the version changes)", r.said)
}
// The first report agreed it with one digest; a later digest of the same version is not said again.
if len(r.said[0].Digests) != 1 {
t.Fatalf("the first agreement carried %+v", r.said[0].Digests)
}
// A restarted builder reads the keys back and agrees the same version with no new report; the digests it says are all three nodes'.
again := newHeardRig(t, state)
if len(again.said) != 1 || again.said[0].Version != "1.26.0" || len(again.said[0].Digests) != 2 || again.said[0].Digests[0].Nodes[1] != "g14" {
t.Fatalf("a restarted builder said %+v, want go 1.26.0 with the digests of its three nodes", again.said)
}
// A version change is said; the stopped build module of ace takes ace's key out, a placement taking nothing.
again.report(t, "node-build-go", "ace", "go", "1.27.0", da)
again.report(t, "node-build-go", "g14", "go", "1.27.0", da)
if len(again.said) != 2 || again.said[1].Version != "1.27.0" {
t.Fatalf("a changed agreed version was not said: %+v", again.said)
}
at := time.Now()
again.running(t, "ace", testGo, "src2:x", at) // running: nothing is forgotten
if k, _ := state.Keys(); len(k) != 3 {
t.Fatalf("a running forgot a key: %v", k)
}
again.running(t, "ace", testGo, "", at.Add(time.Second)) // stopped: no fingerprint
if k, _ := state.Keys(); len(k) != 2 || k[0] != "g14.go" {
t.Fatalf("stopped on ace left %v, want g14.go and novox.go", k)
}
// An older running read after (the start's read of the stream kept last per subject) changes nothing.
before := again.changed
again.running(t, "g14", testGo, "", at)
again.running(t, "g14", testGo, "src2:y", at.Add(-time.Hour))
if k, _ := state.Keys(); len(k) != 1 || k[0] != "novox.go" || again.changed != before+1 {
t.Fatalf("after g14 stopped and an older running: %v", k)
}
// A key dropped by a person leaves the set.
again.h.Dropped(context.Background(), "novox.go")
if len(again.h.set.Entries("go")) != 0 {
t.Fatal("a dropped key stayed in the set")
}
}