Files
mesh-delivery/modules/mesh-delivery-builder/internal/build/retake_test.go
T
jschoubben 50cca4309f BL17c: the operator is asked once a key's event builds are spent; Build once more builds it as a person's build, Leave it nothing
The builder's asker on the seat operator-channel (level approve, about the key, state asked), the teller's three bus
adapters (publish, the router's record through the seat's holder on the seat list, decided heard through the hub's
events handler); the manifest uses the seat and consumes decided.

Builds: design 47 G §568, A-74
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-11 23:25:00 +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/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)
}
}
}