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
154 lines
5.4 KiB
Go
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")
|
|
}
|
|
}
|