Files
mesh-delivery/contracts/go/clients/meshwantsclient/assignment_test.go
T
jschoubben 18c954ddcd CT10: meshwantsclient holds a part's view of the assignment half, a first view then assignment-revision
The typed first view and the following of each half sit in the seats' client packages, built on go/views
(design 47 G §245), so no part follows the assignment half by a follower of its own: a newer trunk revision
heard is waited for, said in the status, asked and held; a candidate's, or one held, asks nothing.

Builds: design 47 G §245, §365

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 11:45:50 +01:00

117 lines
3.4 KiB
Go

package meshwantsclient
import (
"context"
"encoding/json"
"fmt"
"sync"
"testing"
"time"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
)
// fakeWants answers the verb revision with its newest trunk revision, and counts each ask.
type fakeWants struct {
mu sync.Mutex
newest int64
asks int
}
func (f *fakeWants) call(key string, body any) (json.RawMessage, error) {
if key != RevisionVerb {
return nil, fmt.Errorf("asked %s", key)
}
if in := body.(seat.RevisionInput); in != (seat.RevisionInput{}) {
return nil, fmt.Errorf("asked %+v", in)
}
f.mu.Lock()
defer f.mu.Unlock()
f.asks++
r := seat.AssignmentRevision{Revision: f.newest, Kind: kindTrunk, Placements: "p",
Catalogs: []seat.CatalogOf{{Branch: "main", Revision: 10 + f.newest}},
Entries: []seat.NodeEntryRef{{Node: "anchor", Module: "nats", Digest: fmt.Sprintf("d%d", f.newest)}}}
return json.Marshal(seat.RevisionAnswer{Revision: r})
}
func (f *fakeWants) set(newest int64) {
f.mu.Lock()
defer f.mu.Unlock()
f.newest = newest
}
func (f *fakeWants) asked() int {
f.mu.Lock()
defer f.mu.Unlock()
return f.asks
}
func revisionEvent(rev int64, kind string) []byte {
b, _ := json.Marshal(seat.AssignmentRevisionEvent{V: 1, Revision: rev, Kind: kind})
return b
}
// CT10 (design 47 G §245): a part takes a first view of the assignment half by the verb revision, then follows its
// event assignment-revision: a newer trunk revision is waited for, said in the status, and held; a candidate's, or one
// held, asks nothing; what is no such event is refused.
func TestTheAssignmentHalfIsAFirstViewThenItsEvents(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
held := make(chan seat.AssignmentRevision, 8)
next := func() seat.AssignmentRevision {
t.Helper()
select {
case r := <-held:
return r
case <-ctx.Done():
t.Fatal("no revision held")
return seat.AssignmentRevision{}
}
}
f := &fakeWants{newest: 3}
a := NewAssignment(f.call)
a.Follower.Wait = time.Nanosecond
a.Held = func(r seat.AssignmentRevision) { held <- r }
if _, ok := a.Revision(); ok {
t.Fatal("a revision before its first view")
}
if err := a.Start(ctx); err != nil {
t.Fatal(err)
}
if r := next(); r.Revision != 3 || len(r.Catalogs) != 1 || r.Catalogs[0].Revision != 13 {
t.Fatalf("first view holds %+v", r)
}
if v, ok := a.Follower.Get("anchor.nats"); !ok || string(v) == "" {
t.Errorf("the picture lacks anchor.nats: %s", v)
}
for _, ev := range [][]byte{revisionEvent(3, kindTrunk), revisionEvent(2, kindTrunk), revisionEvent(9, "candidate")} {
if err := a.Hear(ctx, ev); err != nil {
t.Fatal(err)
}
}
if n := f.asked(); n != 1 {
t.Errorf("a revision held, an older one or a candidate's asked again: %d asks", n)
}
f.set(4)
if err := a.Hear(ctx, revisionEvent(4, kindTrunk)); err != nil {
t.Fatal(err)
}
if r := next(); r.Revision != 4 {
t.Fatalf("after assignment-revision 4 the view holds %d", r.Revision)
}
if s := a.Follower.Status(); s.View != "4" || s.WaitsFor != "" {
t.Errorf("status view %q, waits for %q", s.View, s.WaitsFor)
}
if r, ok := a.Revision(); !ok || r.Revision != 4 {
t.Errorf("Revision = %d, %v", r.Revision, ok)
}
for _, data := range []string{`{}`, `not json`, `{"kind":"trunk"}`} {
if err := a.Hear(ctx, []byte(data)); err == nil {
t.Errorf("Hear(%s) took it", data)
}
}
}