Files
mesh-delivery/modules/mesh-delivery-builder/internal/build/retake_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

131 lines
5.4 KiB
Go

package build
import (
"context"
"encoding/json"
"fmt"
"sync"
"testing"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
)
type actedStore struct {
mu sync.Mutex
vals map[string][]byte
revs map[string]uint64
}
func (s *actedStore) Get(_ context.Context, k string) ([]byte, uint64, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
v, ok := s.vals[k]
return v, s.revs[k], ok, nil
}
func (s *actedStore) Create(_ context.Context, k string, v []byte) (uint64, error) {
return s.Update(context.Background(), k, v, 0)
}
func (s *actedStore) Update(_ context.Context, k string, v []byte, _ uint64) (uint64, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.vals == nil {
s.vals, s.revs = map[string][]byte{}, map[string]uint64{}
}
s.vals[k], s.revs[k] = v, s.revs[k]+1
return s.revs[k], nil
}
func (s *actedStore) KeysOf(context.Context, string) ([]string, error) { return nil, nil }
func (s *actedStore) Delete(_ context.Context, k string) error { delete(s.vals, k); return nil }
// Design 47 G §568, ADR 0673: a key failed on a transport fault is built again on the node-engine seat's events that say its
// cause passed, once per event, counted in event-builds; after three the next event asks the operator once.
func TestRetakeOnAnEvent(t *testing.T) {
r := newRig(t, goBundle)
// The first look creates the key and its note meets the fault; a retake's claim is an update that succeeds and the note after
// it is the second.
fault := fmt.Errorf("the key: %w", context.DeadlineExceeded)
failOnce := func() { r.st.before = func() { r.st.before = func() { r.st.faults = []error{fault} } } }
r.st.faults = []error{fault}
if v := r.consider(t, "modules/m"); !builds.FailedOnTransportFault(v.Error) {
t.Fatalf("the key = %+v, want failed on a transport fault", v)
}
var asked []string
rt := &Retaker{Builder: r.b, Acted: &actedStore{},
Entry: func(context.Context, string) (meshcatalog.CatalogEntry, error) {
return meshcatalog.CatalogEntry{Serves: []string{"node-build-go.steps"}}, nil
},
Ask: func(_ context.Context, key string, _ builds.BuilderBuild, _ uint64) error {
asked = append(asked, key)
return nil
}}
running, _ := json.Marshal(nodeengine.Running{Module: "mesh-build-go", Node: "g14", Fingerprint: "f1"})
started, _ := json.Marshal(nodeengine.ManifestReadsChanged{Node: "ace", Why: "started"})
heard := func(seq uint64, subject string, data []byte) {
t.Helper()
if ok, err := rt.Heard(context.Background(), subject, data, seq); !ok || err != nil {
t.Fatalf("heard %s: %v, %v", subject, ok, err)
}
r.b.Wait()
}
runningSubject := "mesh.seat.node-engine.event.running.g14.mesh-build-go"
readsSubject := "mesh.seat.node-engine.event.manifest-reads-changed.ace"
// A `started` builds nothing, and the same message again builds nothing.
heard(1, readsSubject, started)
if v := valueOf(t, r.st, r.key); v.EventBuilds != 0 || v.State != builds.BuildFailed {
t.Fatalf("a started: the key = %+v", v)
}
for i, ev := range []struct {
seq uint64
subject string
data []byte
}{{2, runningSubject, running}, {3, runningSubject + ".2", running}, {4, runningSubject + ".3", running}} {
failOnce()
heard(ev.seq, ev.subject, ev.data)
if v := valueOf(t, r.st, r.key); v.EventBuilds != i+1 || !builds.FailedOnTransportFault(v.Error) {
t.Fatalf("event %d: the key = %+v, want event-builds %d failed on a transport fault", i+1, v, i+1)
}
}
heard(4, runningSubject+".3", running) // a sequence heard is not acted on again
if v := valueOf(t, r.st, r.key); v.EventBuilds != 3 || len(asked) != 0 {
t.Fatalf("a sequence heard again: the key = %+v, asked %v", v, asked)
}
heard(5, runningSubject, running)
heard(6, runningSubject, running)
if v := valueOf(t, r.st, r.key); v.EventBuilds != 3 || len(asked) != 1 || asked[0] != r.key {
t.Fatalf("the fourth event: the key = %+v, asked %v, want nothing built and the operator asked once", v, asked)
}
// A person's build starts the count again.
if res := r.b.BuildAgain(context.Background(), "m", fp1, "Build once more", "operator", 2e9); res.Refused != "" {
t.Fatalf("built again by hand: %+v", res)
}
r.b.Wait()
if v := valueOf(t, r.st, r.key); v.EventBuilds != 0 {
t.Fatalf("after a person's build: the key = %+v, want event-builds 0", v)
}
}
// A manifest-reads-changed with why returned builds again the keys whose step ran on its node, and no other's.
func TestRetakeOnANodeReturned(t *testing.T) {
r := newRig(t, goBundle)
r.consider(t, "modules/m")
seed(t, r.st, builds.BuilderBuild{State: builds.BuildFailed, By: "novox.builder", Manifest: mdigest('a'),
Error: "transport fault: connection, node-build-go ace, dial tcp: connection refused"})
rt := &Retaker{Builder: r.b, Acted: &actedStore{}}
for seq, c := range []struct {
node, why string
built bool
}{{"g14", "returned", false}, {"ace", "started", false}, {"ace", "returned", true}} {
data, _ := json.Marshal(nodeengine.ManifestReadsChanged{Node: c.node, Why: c.why})
if _, err := rt.Heard(context.Background(), "mesh.seat.node-engine.event.manifest-reads-changed."+c.node, data, uint64(seq+1)); err != nil {
t.Fatal(err)
}
r.b.Wait()
if v := valueOf(t, r.st, r.key); (v.State == builds.BuildDone) != c.built {
t.Fatalf("%s %s: the key = %+v, want built %v", c.node, c.why, v, c.built)
}
}
}