diff --git a/go/approval/approval_test.go b/go/approval/approval_test.go index 29ca678e..a0ed8ca6 100644 --- a/go/approval/approval_test.go +++ b/go/approval/approval_test.go @@ -2,10 +2,13 @@ package approval import ( "encoding/json" + "errors" "strings" "testing" + "time" "git.novox.be/novox/mesh-sdk/go/operatorask" + "git.novox.be/novox/mesh-sdk/go/seatdef" ) // call is a fixture call of the marked tool wipe. @@ -78,3 +81,53 @@ func TestTheActAndItsAsk(t *testing.T) { t.Errorf("headline %q", got) } } + +// unread is a catalog half whose first view is not read yet. +type unread struct{} + +func (unread) Ready() error { return errors.New("the registrations are not answered") } +func (unread) Revision() string { return "" } +func (unread) Served(string) (Entry, bool, string) { return Entry{}, false, "not read" } +func (unread) Newest(string) (Entry, bool) { return Entry{}, false } +func (unread) Definitions() map[string]seatdef.Definition { return nil } +func (unread) Entries() []Entry { return nil } + +// Design 47 G lead answers A-353, A-359: while its catalog half is unread, the gate lets through the four seat verbs that +// half is read by, told from the call's seat and verb, and holds every other call as before. +func TestAnUnreadCatalogHalfLetsItsOwnReadsThrough(t *testing.T) { + waited := 0 + g := New(Deps{Node: "n1", Runner: "node-tools", Catalog: unread{}, Wait: func(time.Duration) error { + waited++ + return errors.New("still unread") + }}) + for _, tc := range []struct { + seat, name string + runs bool + }{ + {"mesh-catalog", "registrations", true}, + {"mesh-catalog", "revision", true}, + {"mesh-catalog", "entry", true}, + {"mesh-delivery", "source", true}, + {"mesh-catalog", "register", false}, + {"mesh-delivery", "registrations", false}, + {"", "registrations", false}, // a module's tool of that name is no seat verb + {"", "revision", false}, + } { + ran := false + address := tc.seat + "." + tc.name + if tc.seat == "" { + address = "mesh-delivery-indexer." + tc.name + } + _, err := g.Run(Call{Address: address, Module: "mesh-delivery-indexer", Seat: tc.seat, Name: tc.name, Fingerprint: "fp1", + Caller: "node-tools"}, func() (any, error) { ran = true; return "ok", nil }) + if ran != tc.runs || (err == nil) != tc.runs { + t.Errorf("%s: ran %v (%v), want ran %v", address, ran, err, tc.runs) + } + if !tc.runs && (err == nil || !strings.Contains(err.Error(), "are not read yet")) { + t.Errorf("%s: refused with %v, want the marks not read yet", address, err) + } + } + if waited != 4 { + t.Errorf("the other calls waited for the catalog half %d times, want 4 (the four start reads wait for nothing)", waited) + } +} diff --git a/go/approval/run.go b/go/approval/run.go index 5cefae6f..42a701d4 100644 --- a/go/approval/run.go +++ b/go/approval/run.go @@ -3,6 +3,7 @@ package approval import ( "encoding/json" "fmt" + "slices" "strings" "git.novox.be/novox/mesh-sdk/go/operatorask" @@ -67,13 +68,27 @@ func (g *Gate) Run(c Call, run func() (any, error)) (any, error) { // Binding is a Store that says whether the runner's membership binds the state now (§13). type Binding interface{ Bound() bool } -// marksOf is the module's marks as the catalog half has them now; before its first view, the call waits for it within -// RecordWithin and is then judged, refused naming mesh_first_views when it is still unread (§19). +// startReads are the seat verbs the catalog half is read by, as `.`: while it is unread the gate lets them +// through, so the runner that serves them answers the reads its own catalog half waits on (design 47 G lead answers +// A-353, A-359). +var startReads = []string{"mesh-catalog.registrations", "mesh-catalog.revision", "mesh-catalog.entry", "mesh-delivery.source"} + +// startRead says the call is one of startReads, told from its seat and verb. +func startRead(c Call) bool { + return c.Seat != "" && slices.Contains(startReads, c.Seat+"."+c.Name) +} + +// marksOf is the module's marks as the catalog half has them now; before its first view, a call of startReads runs +// unmarked (A-353, A-359) and any other waits for it within RecordWithin and is then judged, refused naming +// mesh_first_views when it is still unread (§19). func (g *Gate) marksOf(c Call) (Marks, error) { if g.d.Catalog == nil { return Marks{}, g.refused(c, "", "the tool runner reads no catalog half, so its marks are not read") } if err := g.d.Catalog.Ready(); err != nil { + if startRead(c) { + return Marks{Module: c.Module}, nil + } if g.d.Wait != nil { err = g.d.Wait(RecordWithin) }