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
+16 -86
View File
@@ -3,7 +3,6 @@ package approval
import (
"encoding/json"
"errors"
"sort"
"strings"
"sync"
"testing"
@@ -15,83 +14,14 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/seatdef"
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
)
// mem is the runner's state approvals as a fixture; fail makes every read and write fail, as a bus refusing them.
type mem struct {
mu sync.Mutex
vals map[string]json.RawMessage
revs map[string]uint64
n uint64
fail error
}
// mem is the runner's state approvals as a fixture (meshtest.State).
type mem = mt.State
func newMem() *mem { return &mem{vals: map[string]json.RawMessage{}, revs: map[string]uint64{}} }
func (m *mem) Get(k string) (*stdio.StateEntry, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.fail != nil {
return nil, m.fail
}
v, ok := m.vals[k]
if !ok {
return nil, nil
}
return &stdio.StateEntry{Key: k, Value: v, Revision: m.revs[k]}, nil
}
func (m *mem) Create(k string, v any) (uint64, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.fail != nil {
return 0, m.fail
}
if _, ok := m.vals[k]; ok {
return 0, stdio.ErrChanged
}
return m.put(k, v)
}
func (m *mem) Update(k string, v any, rev uint64) (uint64, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.fail != nil {
return 0, m.fail
}
if rev == 0 || m.revs[k] != rev {
return 0, stdio.ErrChanged
}
return m.put(k, v)
}
func (m *mem) Put(k string, v any) (uint64, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.fail != nil {
return 0, m.fail
}
return m.put(k, v)
}
func (m *mem) Keys() ([]string, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.fail != nil {
return nil, m.fail
}
var out []string
for k := range m.vals {
out = append(out, k)
}
sort.Strings(out)
return out, nil
}
func (m *mem) put(k string, v any) (uint64, error) {
raw, ok := v.(json.RawMessage)
if !ok {
raw, _ = json.Marshal(v)
}
m.n++
m.vals[k], m.revs[k] = raw, m.n
return m.n, nil
}
func newMem() *mem { return mt.NewState() }
// world is a gate on a fixture mesh: the catalog half, the router's records as the router writes them (mesh-catalog's
// messenger, asks.go), the asks published, the clock.
@@ -262,11 +192,11 @@ func theActAndItsAsk(t *testing.T) {
{strings.Repeat("x", 50), "n1", "Run " + strings.Repeat("x", 50) + "?"},
{strings.Repeat("y", 80), "n1", "Run " + strings.Repeat("y", 54) + "…?"},
} {
if got := headlineOf(tc.name, tc.node, true); got != tc.want || len([]rune(got)) > 60 {
if got := headlineOf("Run", tc.name, tc.node, true); got != tc.want || len([]rune(got)) > 60 {
t.Errorf("headline %q, want %q", got, tc.want)
}
}
if got := headlineOf("wipe", "n1", false); got != "Run wipe on n1? Part shown" {
if got := headlineOf("Run", "wipe", "n1", false); got != "Run wipe on n1? Part shown" {
t.Errorf("headline %q", got)
}
}
@@ -314,9 +244,9 @@ func TestTheRefusalsAndAnswers(t *testing.T) {
t.Errorf("explanation %q", a.Explanation)
}
var k operatorchannelclient.Kept
_ = json.Unmarshal(w.state.vals["n1.ask."+a.ID], &k)
if k.Act["arg.dir"] != "scratch" || string(k.Arguments) != `{"dir":"scratch"}` || string(w.state.vals["n1.act."+d]) != `"`+a.ID+`"` {
t.Errorf("kept %+v, act key %s", k, w.state.vals["n1.act."+d])
_ = json.Unmarshal(w.state.Vals["n1.ask."+a.ID], &k)
if k.Act["arg.dir"] != "scratch" || string(k.Arguments) != `{"dir":"scratch"}` || string(w.state.Vals["n1.act."+d]) != `"`+a.ID+`"` {
t.Errorf("kept %+v, act key %s", k, w.state.Vals["n1.act."+d])
}
}},
{"§12: with no record within 5 seconds, Not run, asked; not yet taken by the router", func(t *testing.T, w *world) {
@@ -385,10 +315,10 @@ func TestTheRefusalsAndAnswers(t *testing.T) {
rec.Warrant.AskDigest = operatorchannelclient.Digest(a)
w.records["shell."+a.ID] = rec
var k operatorchannelclient.Kept
_ = json.Unmarshal(w.state.vals["n1.ask."+a.ID], &k)
_ = json.Unmarshal(w.state.Vals["n1.ask."+a.ID], &k)
k.Ask = a
raw, _ := json.Marshal(k)
w.state.vals["n1.ask."+a.ID] = raw
w.state.Vals["n1.ask."+a.ID] = raw
if _, err := w.do(call("wipe", `{"dir":"scratch"}`)); err == nil || w.runs != 0 || !strings.Contains(err.Error(), "nothing is done") {
t.Fatalf("%v, runs %d", err, w.runs)
}
@@ -495,7 +425,7 @@ func TestTheRefusalsAndAnswers(t *testing.T) {
t.Fatalf("recorded %v, findings %v, bounds %v", w.recorded, w.findings.raised, w.bounds.keys)
}
var k operatorchannelclient.Kept
_ = json.Unmarshal(w.state.vals["n1.ask."+a.ID], &k)
_ = json.Unmarshal(w.state.Vals["n1.ask."+a.ID], &k)
if !k.Recorded || !k.Acted {
t.Fatalf("kept %+v", k)
}
@@ -517,7 +447,7 @@ func TestTheRefusalsAndAnswers(t *testing.T) {
t.Fatalf("%v", err)
}
w.g.d.Publish = pub
w.state.vals = map[string]json.RawMessage{}
w.state.Vals = map[string]json.RawMessage{}
_, _ = w.do(call("wipe", `{}`))
w.g.d.Record = func(string, string) (*stdio.StateEntry, error) { return nil, errors.New("timeout") }
if _, err := w.do(call("wipe", `{}`)); err == nil || !strings.Contains(err.Error(), "the router's record of its ask") || w.runs != 0 {
@@ -546,7 +476,7 @@ func TestTheRefusalsAndAnswers(t *testing.T) {
{"§19: the state failing refuses, raises approvals-unreadable after five minutes, and clears when read again", func(t *testing.T, w *world) {
stop, _ := w.g.WatchBounds()
defer stop()
w.state.fail = errors.New("the bus is unreachable")
w.state.Fail = errors.New("the bus is unreachable")
if _, err := w.do(call("wipe", `{}`)); err == nil || !strings.Contains(err.Error(), "cannot be read") {
t.Fatalf("%v", err)
}
@@ -557,7 +487,7 @@ func TestTheRefusalsAndAnswers(t *testing.T) {
if w.findings.raised[keys.EscapeName("n1.approvals")] != KindApprovalsUnreadable {
t.Fatalf("findings %v", w.findings.raised)
}
w.state.fail = nil
w.state.Fail = nil
w.g.StateAgain() // a membership change heard, no further call
if len(w.findings.raised) != 0 || len(w.bounds.keys) != 0 {
t.Fatalf("findings %v, bounds %v", w.findings.raised, w.bounds.keys)