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