Files
mesh-delivery/modules/mesh-delivery-sdk/steprun/pull_seat_test.go
T
jschoubben 7033d5e80d BG2, BG3: the puller refuses a kind its asker may not ask and pulls the named worker
An ask's asker is the last token of its accept subject; the holder checks the kind against the callers the build seat's
definition lists (Puller.Callers, CallersFromDefinitions), refuses an ask of another kind answering error naming the kind
and the asker and ending it failed, takes only a build and a run on main's subject, and gives back an ask the seat list
does not judge yet. A second worker per queue (AskNamedWorker, filtered to accept.<queue>.*) is pulled after main's; the
holder says `named worker not bound` while its membership lists none.

Builds: design 47 G §543, §544, §481, A-163, A-170

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 05:44:38 +01:00

180 lines
7.1 KiB
Go

package steprun
import (
"context"
"encoding/json"
"fmt"
"strings"
"sync"
"testing"
"time"
nodebuildgo "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/nodebuildgo"
sdkstep "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// 1887/A11: a bundle's source pulls the kind's worker by its seat's name and settles each ask by the runtime's id.
func TestSeatAsksPullsTheKindsWorkerAndSettlesByID(t *testing.T) {
var pulled []string
var settled []string
src := &SeatAsks{Language: sdkstep.AskGo,
Pull: func(worker string, wait time.Duration) (*stdio.Pulled, error) {
pulled = append(pulled, worker)
if worker != "SEAT_NODE_BUILD_GO_CHECK_worker" {
return nil, nil
}
return &stdio.Pulled{ID: "p9", Worker: worker, Body: []byte(`{"v":1}`), Delivered: 2}, nil
},
Settle: func(id, fate string) error { settled = append(settled, id+" "+fate); return nil }}
if p, err := src.Next(context.Background(), sdkstep.AskDelivery, 0); err != nil || p != nil {
t.Fatalf("delivery = %+v, %v", p, err)
}
p, err := src.Next(context.Background(), sdkstep.AskCheck, time.Second)
if err != nil || p == nil || string(p.Data) != `{"v":1}` || p.Delivered != 2 {
t.Fatalf("check = %+v, %v", p, err)
}
_ = p.InProgress()
_ = p.Ack()
if len(pulled) != 3 || pulled[0] != "SEAT_NODE_BUILD_GO_DELIVERY_worker" || pulled[1] != "SEAT_NODE_BUILD_GO_DELIVERY_named_worker" {
t.Errorf("pulled %v", pulled)
}
if len(settled) != 2 || settled[0] != "p9 progress" || settled[1] != "p9 ack" {
t.Errorf("settled %v", settled)
}
}
// pauseKV is a pauses state in memory: its put, load, delete and keys.
type pauseKV struct {
mu sync.Mutex
m map[string][]byte
}
func (k *pauseKV) put(key string, v any) (uint64, error) {
raw, _ := v.(json.RawMessage)
k.mu.Lock()
defer k.mu.Unlock()
if k.m == nil {
k.m = map[string][]byte{}
}
k.m[key] = raw
return 1, nil
}
func (k *pauseKV) load(key string) ([]byte, error) {
k.mu.Lock()
defer k.mu.Unlock()
return k.m[key], nil
}
func (k *pauseKV) forget(key string) error {
k.mu.Lock()
defer k.mu.Unlock()
delete(k.m, key)
return nil
}
func (k *pauseKV) keys() ([]string, error) {
k.mu.Lock()
defer k.mu.Unlock()
var out []string
for key := range k.m {
out = append(out, key)
}
return out, nil
}
// ADR 0664 §5 (issue 1748): the seat's verb drop. An ask dropped before it is given is refused at pull, naming who dropped it
// and why, its key deleted, and never run; one that runs when it is dropped has its step's context cancelled and ends
// refused the same way, acknowledged; the start deletes drops older than DroppedKeep.
func TestADroppedAskIsRefusedAtPullAndARunningOneIsEnded(t *testing.T) {
for _, running := range []bool{false, true} {
src, st, kv := NewFakeAsks(), newSteps(), &pauseKV{}
a := testAsk(sdkstep.AskDelivery)
started := make(chan struct{})
ran := false
var heard func(stdio.StateChange) error
p := &Puller{Source: src, Language: sdkstep.AskGo, Node: "shanks", Put: st.put, PausePut: kv.put, Load: kv.load, ForgetPause: kv.forget, PauseKeys: kv.keys,
WatchPauses: func(_ string, h func(stdio.StateChange) error) error { heard = h; return nil },
Capacity: func() int { return 1 }, PullWait: 10 * time.Millisecond,
Run: func(ctx context.Context, _ sdkstep.StepAsk) (any, error) {
ran = true
close(started)
<-ctx.Done()
return nil, ctx.Err()
}}
if !running {
if err := p.Drop(a.ID, "mesh-delivery-builder", "superseded"); err != nil {
t.Fatal(err)
}
}
_ = src.PublishAsk(a)
ctx, cancel := context.WithCancel(context.Background())
go p.Pull(ctx)
if running {
<-started
// Another node's holder served the verb: its write reaches this node by the watch of the shared pauses state.
raw, _ := json.Marshal(nodebuildgo.Drop{V: int64(sdkstep.Version), By: "mesh-delivery-builder", Why: "superseded", At: ptr(time.Now())})
kv.put(sdkstep.DropKey(a.ID), json.RawMessage(raw))
if err := heard(stdio.StateChange{Key: sdkstep.DropKey(a.ID), Op: "put", Value: raw}); err != nil {
t.Fatal(err)
}
}
waitFor(t, "the ask ended", func() bool { ack, tm, _ := src.Fates(); return len(ack)+len(tm) == 1 })
cancel()
key, _ := sdkstep.AskStepKey("shanks", a.ID)
s, ok := st.get(key)
if !ok || s.State != sdkstep.StepDropped || !strings.Contains(s.Reason, "dropped by mesh-delivery-builder: superseded") || ran != running {
t.Errorf("running %v: step %+v (%v), ran %v", running, s, ok, ran)
}
waitFor(t, fmt.Sprint("the drop key deleted, running ", running), func() bool { kv.mu.Lock(); defer kv.mu.Unlock(); _, held := kv.m[sdkstep.DropKey(a.ID)]; return !held })
}
// The start deletes a drop older than DroppedKeep and keeps a newer one.
kv := &pauseKV{}
old, _ := json.Marshal(nodebuildgo.Drop{V: int64(sdkstep.Version), Why: "old", At: ptr(time.Now().Add(-DroppedKeep - time.Hour))})
fresh, _ := json.Marshal(nodebuildgo.Drop{V: int64(sdkstep.Version), Why: "fresh", At: ptr(time.Now())})
kv.put(sdkstep.DropKey("old"), json.RawMessage(old))
kv.put(sdkstep.DropKey("fresh"), json.RawMessage(fresh))
// Review 1: a module that sets no watch of the drops says so (a dropped ask running here is then not ended).
var said []string
(&Puller{Node: "shanks", Logf: func(f string, a ...any) { said = append(said, fmt.Sprintf(f, a...)) }}).watchDrops()
if len(said) != 1 || !strings.Contains(said[0], "no watch of the drops") {
t.Errorf("a puller with no watch said %q", said)
}
p := &Puller{Load: kv.load, ForgetPause: kv.forget, PauseKeys: kv.keys}
p.sweepDropped()
if keys, _ := kv.keys(); len(keys) != 1 || keys[0] != sdkstep.DropKey("fresh") {
t.Errorf("keys after the sweep = %v, want the fresh drop alone", keys)
}
}
func ptr[T any](v T) *T { return &v }
// Design 47 G §544 (BG3): a holder pulls main's worker, then the queue's named worker; while its membership does not list the
// named worker it takes main's alone and says `named worker not bound` once.
func TestSeatAsksPullsTheNamedWorkerAndSaysWhenItIsNotBound(t *testing.T) {
var said []string
bound := false
src := &SeatAsks{Language: sdkstep.AskGo, Logf: func(f string, a ...any) { said = append(said, fmt.Sprintf(f, a...)) },
Pull: func(worker string, wait time.Duration) (*stdio.Pulled, error) {
if worker != "SEAT_NODE_BUILD_GO_CHECK_named_worker" {
return nil, nil
}
if !bound {
return nil, fmt.Errorf("the module pulls no work from %s", worker)
}
return &stdio.Pulled{ID: "n1", Worker: worker, Subject: "mesh.seat.node-build-go.accept.check.mesh-pipeline-runner", Body: []byte(`{"v":1}`), Delivered: 1}, nil
},
Settle: func(string, string) error { return nil }}
for range 2 {
if p, err := src.Next(context.Background(), sdkstep.AskCheck, 0); err != nil || p != nil {
t.Fatalf("unbound: %+v, %v", p, err)
}
}
if len(said) != 1 || !strings.Contains(said[0], "named worker not bound") {
t.Fatalf("said %q, want `named worker not bound` once", said)
}
bound = true
p, err := src.Next(context.Background(), sdkstep.AskCheck, 0)
if err != nil || p == nil || !strings.HasSuffix(p.Subject, ".mesh-pipeline-runner") {
t.Fatalf("bound: %+v, %v", p, err)
}
}