Let a tool runner's catalog-half reads through its gate while that half is unread (A-353)

A runner serving the indexer's tools refused every call until its own catalog half was read, and that read is a call of
the same seat verbs through the same gate: G could not start. The four seat verbs the catalog half is read by now run
unmarked while it is unread; every other call waits and is refused as before.

Builds: design 47 G lead answers A-353, A-359

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-13 17:28:58 +01:00
parent cf6bc7612f
commit d6ca425737
2 changed files with 70 additions and 2 deletions
+53
View File
@@ -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)
}
}
+17 -2
View File
@@ -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 `<seat>.<verb>`: 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)
}