E-5 marked waits: a wait on a marked event runs only on the operator's warrant for that one wait

mesh_wait goes through the runner's approval gate (node-tools/internal/approval, Gate.Waited): a wait whose
subjects overlap a marked subject of any module the catalog half lists (the newest entry of every followed
branch, with the served entry of each module this runner serves) is an act of the runner's own module and
build (MESH_FINGERPRINT at its start), asked once on operator-channel as "Wait on <names> on <node>?", and
held, subscribing nothing but its ask's decided, until its timeout, its caller's going or the choice. On a
warrant it subscribes, its timeout running from then, and answers {answer, approval}. Before the marks are
read nothing is asked; a wait from a seat is never marked. The state fixture moves to meshtest so the
console's table can hold a gate.

Builds: design 34 every-event §34, §35, §36, §37; design 47 G lead answer A-168

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 05:41:53 +01:00
parent 4116809162
commit 809288da69
12 changed files with 783 additions and 148 deletions
+230 -1
View File
@@ -10,11 +10,16 @@ import (
"reflect"
"sort"
"strings"
"sync"
"testing"
"time"
"git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
opseat "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
catalogseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
@@ -197,7 +202,7 @@ func TestMeshWaitsTimeoutIsBounded(t *testing.T) {
// a wildcard, of one token, more than 16 names, none, or a from of another word is refused at once naming the rule; a
// wait whose every subscription the bus refuses answers after its timeout, in words naming no wait grant.
func TestMeshWaitRefusesLoudlyAndNeverLoops(t *testing.T) {
s, _ := waitingRuntime(t)
s, mesh := waitingRuntime(t)
var seventeen []any
for i := 0; i < 17; i++ {
seventeen = append(seventeen, fmt.Sprintf("mesh-knowledge.reviewed%d", i))
@@ -274,8 +279,232 @@ func TestMeshWaitRefusesLoudlyAndNeverLoops(t *testing.T) {
!strings.Contains(text, "novox/hq ADR 0670 §1") || strings.Contains(text, "wait grant") || strings.Contains(text, "0367 §2") {
t.Errorf("a wait the bus refused whole: %s", text)
}
// Design 34 every-event §37: a wait whose subjects overlap a marked event's, or a stream's with a `grant`, runs only on the
// operator's warrant for that one wait; with none it subscribes nothing, asks once and answers at its timeout or the choice.
markedWaits(t, s, mesh)
}
// markedWaits are §37's rows, against a gate on the test bus, the router's records written there as the router writes them.
func markedWaits(t *testing.T, s *Surface, mesh *mt.Mesh) {
gw := newGatedWaits(t, s, mesh, "src2:runner")
g := gw.surface
took := func(args map[string]any) (string, bool, time.Duration) {
start := time.Now()
text, isErr := callWait(g, args)
return text, isErr, time.Since(start)
}
// A fixture module's marked event, no warrant: nothing of it subscribed (only its ask's decided), asked once, §12's
// words at its timeout; the same wait again asks nothing new.
before := g.conn.Subscriptions()
go func() {
listening(t, g, before, 1)
mesh.Emit(t, "mesh.mod.mail.event.message", map[string]any{"n": 1})
}()
text, isErr, d := took(map[string]any{"subjects": []any{"mail.message"}, "timeout": 1})
if !isErr || !strings.Contains(text, "this wait needs the operator's approval. Asked on telegram") || gw.asks() != 1 ||
d < time.Second || strings.Contains(text, "heard") {
t.Errorf("a marked event's wait, no warrant: %s after %s, %d asks", text, d, gw.asks())
}
if text, isErr, _ = took(map[string]any{"subjects": []any{"mail.message"}, "timeout": 1}); !isErr ||
!strings.Contains(text, "still waiting") || gw.asks() != 1 {
t.Errorf("the same marked wait again: %s, %d asks", text, gw.asks())
}
// A stream with a `grant` is marked whole; a name with the node after a marked event's name is marked.
for i, name := range []string{"mail.inbox", "mail.message.desk"} {
if text, isErr, _ := took(map[string]any{"subjects": []any{name}, "timeout": 1}); !isErr || !strings.Contains(text, "Not run") || gw.asks() != 2+i {
t.Errorf("%s: %s, %d asks", name, text, gw.asks())
}
}
// The same names from a seat ask nothing.
if text, isErr, _ := took(map[string]any{"subjects": []any{"mail.message"}, "from": "seat", "timeout": 1}); isErr ||
!strings.Contains(text, `"timed-out": true`) || gw.asks() != 3 {
t.Errorf("from a seat: %s, %d asks", text, gw.asks())
}
// The most timeout a wait takes, its ask chosen while it waits: answered at the choice; the same call made then
// subscribes and hears the event published after it, answered with its approval; a warrant for other names, another
// filter or another timeout is refused, asked anew.
long := map[string]any{"subjects": []any{"mail.message"}, "filter": map[string]any{"n": 2}, "timeout": 600}
done := make(chan string, 1)
go func() { text, _, d := took(long); done <- fmt.Sprintf("%s after %s", text, d) }()
mt.Until(t, func() error {
if gw.asks() < 4 {
return os.ErrNotExist
}
return nil
})
gw.approve(t)
select {
case got := <-done:
if !strings.Contains(got, "make this wait again within 30 minutes") {
t.Errorf("a long wait chosen while it waits: %s", got)
}
case <-time.After(10 * time.Second):
t.Fatal("a long marked wait was not answered at the choice")
}
before = g.conn.Subscriptions()
go func() {
listening(t, g, before, 6)
mesh.Emit(t, "mesh.mod.mail.event.message", map[string]any{"n": 2})
}()
if text, isErr, _ := took(long); isErr || !strings.Contains(text, `"approval": "run on the operator's approval`) ||
!strings.Contains(text, `"subject": "mail.message"`) {
t.Errorf("the same wait on its warrant: %s", text)
}
for _, other := range []map[string]any{
{"subjects": []any{"mail.message", "mail.inbox"}, "filter": map[string]any{"n": 2}, "timeout": 1},
{"subjects": []any{"mail.message"}, "filter": map[string]any{"n": 3}, "timeout": 1},
{"subjects": []any{"mail.message"}, "filter": map[string]any{"n": 2}, "timeout": 2},
} {
asked := gw.asks()
if text, isErr, _ := took(other); !isErr || !strings.Contains(text, "Not run") || gw.asks() != asked+1 {
t.Errorf("another wait than the warrant's %v: %s", other, text)
}
}
// Before the catalog half is read nothing is asked: judged when its first view comes within the timeout, refused at
// the timeout when it does not.
gw.cat.mu.Lock()
gw.cat.ready = errors.New("no first view yet")
gw.cat.mu.Unlock()
asked := gw.asks()
if text, isErr, d := took(map[string]any{"subjects": []any{"mail.message"}, "timeout": 1}); !isErr ||
!strings.Contains(text, "the marks are not yet read") || gw.asks() != asked || d < time.Second {
t.Errorf("before the catalog half is read: %s after %s", text, d)
}
gw.cat.came = make(chan chan struct{})
go func() { close(<-gw.cat.came) }()
if text, isErr, _ := took(map[string]any{"subjects": []any{"mail.message"}, "filter": map[string]any{"n": 9}, "timeout": 1}); !isErr ||
!strings.Contains(text, "this wait needs the operator's approval") || gw.asks() != asked+1 {
t.Errorf("the catalog half read within the timeout: %s", text)
}
// While the runner's own fingerprint is unknown a marked wait is refused, saying so.
unknown := newGatedWaits(t, s, mesh, "")
if text, isErr := callWait(unknown.surface, map[string]any{"subjects": []any{"mail.message"}, "timeout": 1}); !isErr ||
!strings.Contains(text, "own fingerprint is unknown") || unknown.asks() != 0 {
t.Errorf("the runner's fingerprint unknown: %s", text)
}
}
// gatedWaits is the surface with the runner's approval gate: the fixture module mail marks its event message and declares
// the stream inbox with a grant; the router takes each ask as it is published, its records on the test bus.
type gatedWaits struct {
surface *Surface
cat *waitCatalog
records nats.KeyValue
mu sync.Mutex
asked []opseat.Ask
say *bus.Conn
}
func newGatedWaits(t *testing.T, s *Surface, mesh *mt.Mesh, build string) *gatedWaits {
gw := &gatedWaits{cat: &waitCatalog{entries: []catalogseat.CatalogEntry{{Module: "mail", Fp: "src2:mail", Manifest: map[string]any{
"emits": []any{"message"}, "needs-approval": []any{"message"},
"streams": []any{map[string]any{"name": "inbox", "events": []any{"inbox.*"}, "grant": map[string]any{"subscribe": []any{"x"}}}}}}}},
records: mesh.Bucket(t, "operator-channel-asks"), say: connect(t, "messenger", "")}
gate := approval.New(approval.Deps{Node: "desk", Runner: "node-tools", Build: build, Catalog: gw.cat, Wait: gw.cat.wait,
State: mt.NewState(), Holder: func(string) (bool, error) { return true, nil },
Hear: func(ctx context.Context, subjects []string, within time.Duration, match func(json.RawMessage) bool) (bool, error) {
_, ok, _, err := s.conn.HearFirst(ctx, subjects, within, match)
return ok, err
},
Publish: func(module, subject string, body any, id string) error {
a, ok := body.(opseat.Ask)
if subject != opseat.AcceptSubjectAsk(module) || !ok {
return nil
}
gw.mu.Lock()
gw.asked = append(gw.asked, a)
gw.mu.Unlock()
gw.write(t, opseat.AsksByAskerIDValue{Asker: module, Key: module + "." + a.ID, State: "open", Headline: a.Headline,
Expires: a.Expires, Opened: operatorchannelclient.FormatTime(time.Now()), OfferedOn: []string{"telegram"}})
return nil
},
Record: func(module, id string) (*stdio.StateEntry, error) {
e, err := gw.records.Get(module + "." + id)
if errors.Is(err, nats.ErrKeyNotFound) {
return nil, nil
} else if err != nil {
return nil, err
}
return &stdio.StateEntry{Key: e.Key(), Value: e.Value(), Revision: e.Revision()}, nil
}})
gw.surface = NewSurface(s.conn, busSource{s.conn}, "desk.node-tools")
gw.surface.Approvals = gateApprovals{gate}
return gw
}
func (gw *gatedWaits) write(t *testing.T, rec opseat.AsksByAskerIDValue) {
raw, _ := json.Marshal(rec)
if _, err := gw.records.Put(rec.Key, raw); err != nil {
t.Error(err)
}
}
func (gw *gatedWaits) asks() int {
gw.mu.Lock()
defer gw.mu.Unlock()
return len(gw.asked)
}
// approve writes the router's record of the newest ask as a person's Approve, and says its decided.
func (gw *gatedWaits) approve(t *testing.T) {
gw.mu.Lock()
a := gw.asked[len(gw.asked)-1]
gw.mu.Unlock()
gw.write(t, opseat.AsksByAskerIDValue{Asker: "node-tools", Key: "node-tools." + a.ID, State: "chosen", Expires: a.Expires,
OfferedOn: []string{"telegram"}, Warrant: &opseat.Warrant{Ask: a.ID, Asker: "node-tools", Outcome: operatorchannelclient.OutcomeChosen,
Option: approval.OptionApprove, Level: operatorchannelclient.Approve, At: operatorchannelclient.FormatTime(time.Now()),
AskDigest: operatorchannelclient.Digest(a), Channel: "telegram",
By: &opseat.Person{Who: operatorchannelclient.Operator, Kind: "telegram", Identity: "42", Verified: "user id verified"}}})
if err := gw.say.Say(opseat.SubjectDecided("node-tools"), []byte(`{"ask":"`+a.ID+`"}`)); err != nil {
t.Fatal(err)
}
}
// gateApprovals is the gate as the surface holds it: mesh_approvals' reads unused here, mesh_wait's gate.
type gateApprovals struct{ *approval.Gate }
func (gateApprovals) Fill() (uint64, int64, error) { return 0, 0, nil }
// waitCatalog is a catalog half as a fixture: its entries, read or not; with came set, its first view comes once a wait
// waits for it (waiting is told on it, then it closes).
type waitCatalog struct {
entries []catalogseat.CatalogEntry
mu sync.Mutex
ready error
came chan chan struct{}
}
func (c *waitCatalog) Ready() error {
c.mu.Lock()
defer c.mu.Unlock()
return c.ready
}
func (c *waitCatalog) wait(within time.Duration) error {
if c.Ready() == nil {
return nil
}
if c.came != nil {
view := make(chan struct{})
c.came <- view
<-view
c.mu.Lock()
c.ready = nil
c.mu.Unlock()
return nil
}
<-time.After(within) // the bound of the runner's own wait for the first view
return c.Ready()
}
func (c *waitCatalog) Revision() string { return "7" }
func (c *waitCatalog) Served(string) (catalogseat.CatalogEntry, bool, string) {
return catalogseat.CatalogEntry{}, false, "not-found"
}
func (c *waitCatalog) Newest(string) (catalogseat.CatalogEntry, bool) {
return catalogseat.CatalogEntry{}, false
}
func (c *waitCatalog) Definitions() map[string]seatdef.Definition { return nil }
func (c *waitCatalog) Entries() []catalogseat.CatalogEntry { return c.entries }
// The mesh MCP server announces seven tools (ADR 0367 v13): ADR 0195's five, mesh_runtimes and mesh_wait, its timeout bounded.
func TestMeshWaitIsAnnouncedAmongSeven(t *testing.T) {
var names []string