Files
mesh-catalog/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/seat_test.go
T

345 lines
13 KiB
Go

package main
import (
"bytes"
"errors"
"slices"
"strings"
"sync"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// **The gate acts only while it holds the seat, and carries today's state over once** (novox/hq ADR 0450 §6 with its
// insight of 2026-10-20, ADR 0437 §7, issue 1347). Fixture sources: the keys of mesh-controller.assignments and
// node-engine.have are `<node>.<module>` (mesh-controller internal/inventory/chain_assignments.go assignmentKey,
// mesh-sdk go/delivery HaveKey); a delivery is the module's own Delivery, written as ports.go kvStore writes it.
// fakeOccupancy is where today's module stands, as the test says.
type fakeOccupancy struct {
mu sync.Mutex
who []string
err error
}
func (o *fakeOccupancy) set(who ...string) { o.mu.Lock(); o.who = who; o.mu.Unlock() }
func (o *fakeOccupancy) Occupants() ([]string, error) {
o.mu.Lock()
defer o.mu.Unlock()
return slices.Clone(o.who), o.err
}
// mapBucket is a bucket over a map, shared with the store a test reads.
type mapBucket struct {
mu *sync.Mutex
m map[string][]byte
}
func (b mapBucket) Keys() ([]string, error) {
b.mu.Lock()
defer b.mu.Unlock()
var keys []string
for k := range b.m {
keys = append(keys, k)
}
return keys, nil
}
func (b mapBucket) Get(key string) ([]byte, error) {
b.mu.Lock()
defer b.mu.Unlock()
return b.m[key], nil
}
func (b mapBucket) Put(key string, raw []byte) error {
b.mu.Lock()
defer b.mu.Unlock()
b.m[key] = raw
return nil
}
// seatTest is a world whose holder asks where today's module stands.
func seatTest(t *testing.T, who ...string) (*world, *fakeOccupancy) {
t.Helper()
w := newWorld(t)
occ := &fakeOccupancy{who: who}
w.h.Seat = occ
return w, occ
}
// anEvent is the runtime handing the holder a pull request's head, as handle takes it.
func anEvent(t *testing.T, w *world) {
t.Helper()
hear(t, w, "gitea.pull.updated", pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
}
// nothingWritten says the forge, the controller and the store were left alone.
func nothingWritten(t *testing.T, w *world) {
t.Helper()
w.forge.mu.Lock()
forge := len(w.forge.notes) + len(w.forge.views) + len(w.forge.statuses) + w.forge.statusWrites + len(w.forge.calls) + len(w.forge.comments)
w.forge.mu.Unlock()
w.ctl.mu.Lock()
ctl := len(w.ctl.checks) + w.ctl.asked + len(w.ctl.delivers) + len(w.ctl.stops)
w.ctl.mu.Unlock()
w.store.mu.Lock()
keys := len(w.store.deliveries) + len(w.store.groups)
w.store.mu.Unlock()
if forge != 0 || ctl != 0 || keys != 0 {
t.Fatalf("a gate that does not hold the seat wrote: %d on the forge, %d to the controller, %d keys", forge, ctl, keys)
}
if n := len(w.h.deliveries); n != 0 {
t.Fatalf("a gate that does not hold the seat took the event: %d deliveries", n)
}
}
func toolNamed(t *testing.T, h *Holder, name string) stdio.Tool {
t.Helper()
for _, tool := range tools(h, &listening{}) {
if tool.Name == name {
return tool
}
}
t.Fatalf("no tool %s", name)
return stdio.Tool{}
}
// AC1: with the module assigned on a machine, or its have key present, an event changes no key and a verb answers that
// the gate is not the holder.
func TestTheGateIsSilentWhileTodaysModuleIsAssignedOrHasAHaveKey(t *testing.T) {
for _, who := range []string{"assigned on novox", "the node-engine of ace has it"} {
t.Run(who, func(t *testing.T) {
w, _ := seatTest(t, who)
if err := w.h.SeatChanged(); err != nil {
t.Fatal(err)
}
if w.h.Holding() {
t.Fatalf("the gate holds the seat while %s", who)
}
anEvent(t, w)
nothingWritten(t, w)
// Every verb of the seat answers that it is not the holder, and none changes anything.
for _, name := range []string{"mesh-delivery.deliveries", "mesh-delivery.release", "mesh-delivery.stop", "mesh-delivery.recheck"} {
_, err := toolNamed(t, w.h, name).Run(map[string]any{"id": "novox/app@111111111111", "why": "a test"})
if err == nil || !strings.Contains(err.Error(), "not the holder") || !strings.Contains(err.Error(), who) {
t.Fatalf("%s answered %v, wanted not the holder, naming %q", name, err, who)
}
}
nothingWritten(t, w)
// Its own status still answers, and says why it is silent.
out, err := toolNamed(t, w.h, "delivery_status").Run(nil)
if err != nil {
t.Fatal(err)
}
status := out.(map[string]any)
if status["holds-the-seat"] != false || !strings.Contains(status["seat-waits"].(string), who) {
t.Fatalf("status %v", status)
}
})
}
}
// The seat free, the gate holds it and acts; asked again with the module back, it is silent again.
func TestTheGateActsOnceTodaysModuleIsGoneAndStopsIfItComesBack(t *testing.T) {
w, occ := seatTest(t)
if err := w.h.SeatChanged(); err != nil {
t.Fatal(err)
}
if !w.h.Holding() {
t.Fatal("the seat is free and the gate does not hold it")
}
anEvent(t, w)
if len(w.h.deliveries) != 1 {
t.Fatalf("the gate that holds the seat took %d deliveries, wanted 1", len(w.h.deliveries))
}
if _, err := toolNamed(t, w.h, "mesh-delivery.deliveries").Run(map[string]any{}); err != nil {
t.Fatalf("a verb of the holder: %v", err)
}
occ.set("assigned on novox")
if err := w.h.SeatChanged(); err != nil {
t.Fatal(err)
}
if w.h.Holding() {
t.Fatal("the module is assigned again and the gate still holds")
}
hear(t, w, "gitea.pull.updated", pr("novox/app", 8, "2222222222222222222222222222222222222222", "feat/y", "src/b.go"))
if len(w.h.deliveries) != 1 {
t.Fatalf("an event was taken while the gate was silent: %d deliveries", len(w.h.deliveries))
}
}
// Nothing owed is flushed, and no look is taken, while the gate is silent.
func TestASilentGateFlushesNothingItOwes(t *testing.T) {
w, occ := seatTest(t)
if err := w.h.SeatChanged(); err != nil {
t.Fatal(err)
}
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go")) // owes a note, a view and a status
occ.set("assigned on novox")
if err := w.h.SeatChanged(); err != nil {
t.Fatal(err)
}
counts := func() [3]int {
w.forge.mu.Lock()
defer w.forge.mu.Unlock()
w.store.mu.Lock()
defer w.store.mu.Unlock()
return [3]int{len(w.forge.notes) + len(w.forge.views) + len(w.forge.statuses) + w.forge.statusWrites + len(w.forge.calls), w.store.puts, len(w.said)}
}
before := counts()
w.h.React()
if after := counts(); after != before {
t.Fatalf("a silent gate flushed what it owes: forge, store and events %v before, %v after", before, after)
}
}
// A seat that cannot be read leaves a gate that did not hold silent, and says so.
func TestTheGateStaysSilentWhenTheSeatCannotBeRead(t *testing.T) {
w, occ := seatTest(t)
occ.err = errors.New("the bus is away")
if err := w.h.SeatChanged(); err == nil {
t.Fatal("a seat that cannot be read was taken as free")
}
if w.h.Holding() {
t.Fatal("holding without knowing")
}
}
// --- AC2: the carry ---------------------------------------------------------------------------------
func aDelivery(id string, number int, ready *ReadyRun) []byte {
return mustJSON(&Delivery{ID: id, Repository: "novox/app", Commit: strings.Repeat("1", 40), Number: number, Base: "main",
State: Proposed, Ready: ready})
}
type carryFixture struct {
oldD, oldG, oldP map[string][]byte
mu sync.Mutex
own map[string]mapBucket
w *world
occ *fakeOccupancy
approvedAt time.Time
}
// aCarry is a world whose holder carries from these buckets into its store's: the old module's deliveries, one with an ask
// waiting, one approved and one the gate already has under another value; one group; one pull key.
func aCarry(t *testing.T) *carryFixture {
f := &carryFixture{oldD: map[string][]byte{}, oldG: map[string][]byte{}, oldP: map[string][]byte{},
approvedAt: time.Date(2026, 10, 20, 9, 0, 0, 0, time.UTC)}
f.w, f.occ = seatTest(t)
ask := &asks.Ask{ID: "merge-7-aaaaaaaaaaaa-1"}
f.oldD["novox/app@aaaaaaaaaaaa"] = aDelivery("novox/app@aaaaaaaaaaaa", 7, &ReadyRun{Head: strings.Repeat("a", 40), AskID: "merge-7-aaaaaaaaaaaa-1",
Ask: ask, Heads: map[string]string{"novox/app@aaaaaaaaaaaa": strings.Repeat("a", 40)}, Asks: 1})
f.oldD["novox/app@bbbbbbbbbbbb"] = aDelivery("novox/app@bbbbbbbbbbbb", 8, &ReadyRun{Head: strings.Repeat("b", 40), AskID: "merge-8-bbbbbbbbbbbb-1",
Ask: &asks.Ask{ID: "merge-8-bbbbbbbbbbbb-1"}, Heads: map[string]string{"novox/app@bbbbbbbbbbbb": strings.Repeat("b", 40)},
Approved: &f.approvedAt, Warrant: "the operator on the phone"})
f.oldD["novox/app@cccccccccccc"] = aDelivery("novox/app@cccccccccccc", 9, &ReadyRun{Head: strings.Repeat("c", 40), AskID: "merge-9-cccccccccccc-1", Ask: ask})
f.oldG["feat_x"] = []byte(`{"id":"feat/x","members":["novox/app@aaaaaaaaaaaa"]}`)
f.oldP["novox_app_7"] = []byte(`{"v":1,"number":7,"head":"` + strings.Repeat("a", 40) + `"}`)
// The gate already has one of the old module's deliveries, otherwise.
f.w.store.deliveries["novox/app@cccccccccccc"] = aDelivery("novox/app@cccccccccccc", 9, &ReadyRun{Head: strings.Repeat("c", 40)})
ownG, ownP := map[string][]byte{}, map[string][]byte{}
f.own = map[string]mapBucket{
"deliveries": {mu: &f.w.store.mu, m: f.w.store.deliveries},
"groups": {mu: &f.mu, m: ownG},
"pulls": {mu: &f.mu, m: ownP},
}
old := map[string]mapBucket{"deliveries": {mu: &f.mu, m: f.oldD}, "groups": {mu: &f.mu, m: f.oldG}, "pulls": {mu: &f.mu, m: f.oldP}}
f.w.h.Carry = func() (CarryReport, error) {
return CarryOver(func(b string) Bucket { return old[b] }, func(b string) Bucket { return f.own[b] }, f.approvedAt)
}
return f
}
// AC2: when both go, the gate carries once (old ask ids cleared, keys it already has untouched) and then acts; a restart
// copies nothing.
func TestTheGateCarriesTodaysStateOnceWhenItFirstHoldsTheSeat(t *testing.T) {
f := aCarry(t)
before := slices.Clone(f.w.store.deliveries["novox/app@cccccccccccc"])
f.occ.set("assigned on novox")
if err := f.w.h.SeatChanged(); err != nil {
t.Fatal(err)
}
if len(f.w.store.deliveries) != 1 || f.w.h.Holding() {
t.Fatalf("the gate carried while the module was still assigned: %d keys", len(f.w.store.deliveries))
}
f.occ.set()
if err := f.w.h.SeatChanged(); err != nil {
t.Fatal(err)
}
if !f.w.h.Holding() {
t.Fatal("the gate does not hold the seat after the module went")
}
d := f.w.h.deliveries
for _, id := range []string{"novox/app@aaaaaaaaaaaa", "novox/app@bbbbbbbbbbbb", "novox/app@cccccccccccc"} {
if d[id] == nil {
t.Fatalf("%s was not carried and read back: %v", id, f.w.store.deliveries)
}
}
// The ask nobody answered is cleared, so the gate asks again; the one answered keeps its answer.
if r := d["novox/app@aaaaaaaaaaaa"].Ready; r.AskID != "" || r.Ask != nil || r.Heads != nil || r.Asks != 1 {
t.Fatalf("the unanswered ask was not cleared: %+v", r)
}
if r := d["novox/app@bbbbbbbbbbbb"].Ready; r.Approved == nil || r.AskID == "" || r.Warrant == "" {
t.Fatalf("an answered Approve lost its answer: %+v", r)
}
// A key the gate had is untouched.
if !bytes.Equal(f.w.store.deliveries["novox/app@cccccccccccc"], before) {
t.Fatal("a key the gate already had was overwritten")
}
for bucket, key := range map[string]string{"groups": "feat_x", "pulls": "novox_app_7"} {
if got, _ := f.own[bucket].Get(key); got == nil {
t.Fatalf("%s %s was not carried", bucket, key)
}
}
if got, _ := f.own["deliveries"].Get(carriedKey); got == nil {
t.Fatal("the key carried was not written")
}
// A restart (a holder that holds again, or starts again) copies nothing.
f.oldD["novox/app@dddddddddddd"] = aDelivery("novox/app@dddddddddddd", 10, nil)
report, err := f.w.h.Carry()
if err != nil || !report.Already {
t.Fatalf("a second carry: %+v %v", report, err)
}
if got, _ := f.own["deliveries"].Get("novox/app@dddddddddddd"); got != nil {
t.Fatal("a second carry copied a key")
}
}
// A carry that fails leaves the gate silent and writes no mark, so it is tried again.
func TestAFailedCarryLeavesTheGateSilentAndUnmarked(t *testing.T) {
f := aCarry(t)
f.w.h.Carry = func() (CarryReport, error) { return CarryReport{}, errors.New("the old bucket cannot be read") }
if err := f.w.h.SeatChanged(); err == nil {
t.Fatal("a failed carry was not returned")
}
if f.w.h.Holding() {
t.Fatal("the gate holds the seat on state it did not carry")
}
if got, _ := f.own["deliveries"].Get(carriedKey); got != nil {
t.Fatal("a failed carry wrote its mark")
}
}
// Review round 1, try failure: the carry copies a group, a pull key and a delivery without an unanswered ask byte for byte; what
// the gate writes after it holds the seat and acts is its own (a group's said state), not the carry's.
func TestTheCarryCopiesWhatItDoesNotClearByteForByte(t *testing.T) {
f := aCarry(t)
group := []byte(`{"id":"feat/x","members":["novox/app@aaaaaaaaaaaa"],"created":"2026-10-09T07:00:00Z","updated":"2026-10-09T07:00:00Z","said":"delivering"}`)
f.oldG["feat_x"] = group
plain := aDelivery("novox/app@eeeeeeeeeeee", 11, nil)
f.oldD["novox/app@eeeeeeeeeeee"] = plain
if err := f.w.h.SeatChanged(); err != nil {
t.Fatal(err)
}
for _, c := range []struct {
bucket, key string
want []byte
}{{"groups", "feat_x", group}, {"pulls", "novox_app_7", f.oldP["novox_app_7"]}, {"deliveries", "novox/app@eeeeeeeeeeee", plain}} {
if got, _ := f.own[c.bucket].Get(c.key); !bytes.Equal(got, c.want) {
t.Fatalf("%s %s changed in the carry:\n got %s\nwant %s", c.bucket, c.key, got, c.want)
}
}
}