The tests G added fold into existing ones as subtests (design 47 G §626, A-306: 171 tests down to 154, integration 9 down to 7). TestMeshWaitRefusesLoudlyAndNeverLoops runs its groups of one-second rows as parallel subtests, each on a runtime of its own, 15 s down to 5 s (A-311: the one-second floor of a wait's timeout stands, no clock is injected). The go.mod no longer requires meshconditionsclient, which no package loads (T4). Node names in the lines G adds are role names (T5). Builds: design 47 G §626, design 34 every-event §19, A-306, A-311; review round 2 findings T2, T3, T4, T5 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
593 lines
30 KiB
Go
593 lines
30 KiB
Go
package approval
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
|
seat "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
|
catalogseat "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
gate "git.novox.be/novox/mesh-sdk/go/approval"
|
|
"git.novox.be/novox/mesh-sdk/go/keys"
|
|
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
|
"git.novox.be/novox/mesh-sdk/go/views"
|
|
|
|
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
|
|
)
|
|
|
|
// mem is the runner's state approvals as a fixture (meshtest.State).
|
|
type mem = mt.State
|
|
|
|
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.
|
|
type world struct {
|
|
g *gate.Gate
|
|
state *mem
|
|
cat *fakeCatalog
|
|
records map[string]seat.AsksByAskerIDValue // by `<module>.<id>`
|
|
takes bool // the router takes an ask as it is published
|
|
asked []seat.Ask
|
|
now time.Time
|
|
runs int
|
|
findings *findings
|
|
bounds *bounds
|
|
// holders is the assigner's view of the seats' holders, by key; holdersErr says it is not answered yet.
|
|
holders map[string][]byte
|
|
holdersErr error
|
|
// warranted is what mesh-hand-acts.warranted answers, and recorded each ask it answered for.
|
|
warranted error
|
|
recorded []string
|
|
// deps is the gate's, made anew by rebuild; publish and record are the bus's publish and the router's record read.
|
|
deps gate.Deps
|
|
publish func(module, subject string, body any, id string) error
|
|
record func(module, id string) (*stdio.StateEntry, error)
|
|
}
|
|
|
|
// rebuild makes the gate anew from w.deps, its module's bundle loaded.
|
|
func (w *world) rebuild() {
|
|
w.g = gate.New(w.deps)
|
|
_ = w.g.Unserved("shell", "src2:1", []string{"wipe", "tidy"})
|
|
}
|
|
|
|
// bounds is the runner's key-ttl state bound as a fixture: expire hands a key's expiry to the watch.
|
|
type bounds struct {
|
|
keys map[string]time.Duration
|
|
expired func(string)
|
|
}
|
|
|
|
func (b *bounds) CreateTTL(key string, _ any, ttl time.Duration) error { b.keys[key] = ttl; return nil }
|
|
func (b *bounds) Delete(key string) error { delete(b.keys, key); return nil }
|
|
func (b *bounds) Watch(_ string, expired func(string)) (func(), error) {
|
|
b.expired = expired
|
|
return func() {}, nil
|
|
}
|
|
func (b *bounds) expire(key string) {
|
|
delete(b.keys, key)
|
|
b.expired(key)
|
|
}
|
|
|
|
func newWorld() *world {
|
|
shell := entry("src2:1", []string{"node-login-shell.execute"}, map[string]any{"needs-approval": []any{"wipe"}})
|
|
w := &world{state: newMem(), records: map[string]seat.AsksByAskerIDValue{}, takes: true, findings: &findings{},
|
|
bounds: &bounds{keys: map[string]time.Duration{}},
|
|
holders: map[string][]byte{"operator-channel": []byte(`{"seat":"operator-channel","scope":"mesh","holders":[{"module":"messenger"}]}`)},
|
|
now: time.Date(2026, 11, 12, 10, 0, 0, 0, time.UTC),
|
|
cat: &fakeCatalog{revision: "7", byFp: map[string]catalogseat.CatalogEntry{"src2:1": shell},
|
|
newest: map[string]catalogseat.CatalogEntry{"shell": shell},
|
|
defs: map[string]seatdef.Definition{"node-login-shell": {Seat: "node-login-shell",
|
|
Verbs: []seatdef.Verb{{Name: "execute", NeedsApproval: true}}}}}}
|
|
w.deps = gate.Deps{Node: "n1", Runner: "node-tools", Catalog: w.cat, State: w.state, Bounds: w.bounds,
|
|
Findings: Findings(w.findings, "n1", "node-tools", func() time.Time { return w.now }),
|
|
Channel: &operatorchannelclient.GateChannel{State: w.state, KeyPrefix: "n1.", Now: func() time.Time { return w.now },
|
|
Publish: func(module, subject string, body any, id string) error { return w.publish(module, subject, body, id) },
|
|
ReadRecord: func(module, id string) (*stdio.StateEntry, error) { return w.record(module, id) }},
|
|
Go: func(f func()) { f() },
|
|
Holder: HoldersIn(func() (map[string][]byte, error) { return w.holders, w.holdersErr }),
|
|
Warranted: func(asker, ask string, open func() (bool, error)) error {
|
|
if still, _ := open(); !still {
|
|
return nil
|
|
}
|
|
if w.warranted == nil {
|
|
w.recorded = append(w.recorded, asker+"."+ask)
|
|
}
|
|
return w.warranted
|
|
},
|
|
Now: func() time.Time { return w.now },
|
|
After: func(d time.Duration) <-chan time.Time {
|
|
w.now = w.now.Add(d)
|
|
c := make(chan time.Time, 1)
|
|
c <- w.now
|
|
return c
|
|
},
|
|
}
|
|
w.publish = func(module, subject string, body any, id string) error {
|
|
if subject != seat.AcceptSubjectAsk(module) {
|
|
return nil
|
|
}
|
|
a := body.(seat.Ask)
|
|
w.asked = append(w.asked, a)
|
|
if w.takes {
|
|
w.records[module+"."+a.ID] = seat.AsksByAskerIDValue{Asker: module, Key: module + "." + a.ID, State: "open",
|
|
Headline: a.Headline, Expires: a.Expires, Opened: operatorchannelclient.FormatTime(w.now), OfferedOn: []string{"telegram"}}
|
|
}
|
|
return nil
|
|
}
|
|
w.record = func(module, id string) (*stdio.StateEntry, error) {
|
|
rec, ok := w.records[module+"."+id]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
raw, _ := json.Marshal(rec)
|
|
return &stdio.StateEntry{Key: module + "." + id, Value: raw}, nil
|
|
}
|
|
w.rebuild()
|
|
return w
|
|
}
|
|
|
|
// decide writes the router's record of the newest ask as the router writes a person's choice: its state and warrant.
|
|
func (w *world) decide(option string, at time.Time) seat.Ask {
|
|
a := w.asked[len(w.asked)-1]
|
|
key := "shell." + a.ID
|
|
rec := w.records[key]
|
|
rec.State = "chosen"
|
|
rec.Warrant = &seat.Warrant{Ask: a.ID, Asker: "shell", Outcome: operatorchannelclient.OutcomeChosen, Option: option,
|
|
Level: operatorchannelclient.Approve, At: operatorchannelclient.FormatTime(at), AskDigest: operatorchannelclient.Digest(a),
|
|
By: &seat.Person{Who: operatorchannelclient.Operator, Kind: "telegram", Identity: "42", Verified: "user id verified"}}
|
|
w.records[key] = rec
|
|
return a
|
|
}
|
|
|
|
// do makes a call through the gate, counting the runs.
|
|
func (w *world) do(c gate.Call) (any, error) {
|
|
return w.g.Run(c, func() (any, error) { w.runs++; return "done", nil })
|
|
}
|
|
|
|
// call is a fixture call of the marked tool wipe, or of another tool of the module.
|
|
func call(name, args string) gate.Call {
|
|
return gate.Call{Address: "shell." + name, Module: "shell", Name: name, Fingerprint: "src2:1", Caller: "agent", Args: json.RawMessage(args),
|
|
Description: "Wipes a scratch directory.\nMore words."}
|
|
}
|
|
|
|
// Design 46 7-approval-mark §22: the refusals and answers of a marked call, on the fixture mesh, the router's record and
|
|
// the warrant written as the router writes them.
|
|
func TestTheRefusalsAndAnswers(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
run func(t *testing.T, w *world)
|
|
}{
|
|
{"§9: a secret in the arguments is refused asking nothing", func(t *testing.T, w *world) {
|
|
_, err := w.do(call("wipe", `{"token":"ghp_0123456789abcdefghijABCDEFGHIJ012345"}`))
|
|
if err == nil || !strings.Contains(err.Error(), "which no ask may carry") || len(w.asked) != 0 {
|
|
t.Fatalf("%v, asked %d", err, len(w.asked))
|
|
}
|
|
}},
|
|
{"§9: more than 16 KiB of arguments is refused asking nothing", func(t *testing.T, w *world) {
|
|
big, _ := json.Marshal(map[string]string{"text": strings.Repeat("a b ", 5000)})
|
|
_, err := w.do(call("wipe", string(big)))
|
|
if err == nil || !strings.Contains(err.Error(), "more than the 16384") || len(w.asked) != 0 {
|
|
t.Fatalf("%v, asked %d", err, len(w.asked))
|
|
}
|
|
}},
|
|
{"§10, §12: the published ask's fields, and Not run with the router's record", func(t *testing.T, w *world) {
|
|
_, err := w.do(call("wipe", `{"dir":"scratch"}`))
|
|
if err == nil || !strings.HasPrefix(err.Error(), "Not run: this needs the operator's approval. Asked on telegram at ") ||
|
|
!strings.Contains(err.Error(), "make the same call again within 30 minutes of the approval; the ask expires at ") {
|
|
t.Fatalf("answer %v", err)
|
|
}
|
|
if len(w.asked) != 1 {
|
|
t.Fatalf("asked %d", len(w.asked))
|
|
}
|
|
a := w.asked[0]
|
|
act, _ := gate.ActOf(call("wipe", `{"dir":"scratch"}`), "n1")
|
|
d, _ := operatorchannelclient.ActDigest(act)
|
|
if a.About != d || a.Who != "operator" || a.OnExpiry != gate.OnExpiry || a.Headline != "Run wipe on n1?" ||
|
|
a.Expires != operatorchannelclient.FormatTime(time.Date(2026, 11, 12, 11, 0, 0, 0, time.UTC)) || len(a.Options) != 2 ||
|
|
a.Options[0].ID != "approve" || a.Options[1].ID != "decline" || a.Options[0].Binds != d || a.Options[1].Binds != d ||
|
|
a.Options[1].Level != "approve" {
|
|
t.Errorf("ask %+v", a)
|
|
}
|
|
if !strings.HasPrefix(a.Explanation, "Wipes a scratch directory.\nOn n1, called through the mesh's tools, which cannot tell who called.\ndir: scratch\nFingerprint ") {
|
|
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."+keys.EscapeName(d)]) != `"`+a.ID+`"` {
|
|
t.Errorf("kept %+v, act key %s", k, w.state.Vals["n1.act."+keys.EscapeName(d)])
|
|
}
|
|
}},
|
|
{"§12: with no record within 5 seconds, Not run, asked; not yet taken by the router", func(t *testing.T, w *world) {
|
|
w.takes = false
|
|
start := w.now
|
|
_, err := w.do(call("wipe", `{}`))
|
|
if err == nil || !strings.HasPrefix(err.Error(), "Not run, asked; not yet taken by the router (ask ") || w.now.Sub(start) > gate.RecordWithin {
|
|
t.Fatalf("answer %v after %s", err, w.now.Sub(start))
|
|
}
|
|
}},
|
|
{"§10, §17: a second identical call publishes nothing and is told it still waits", func(t *testing.T, w *world) {
|
|
_, _ = w.do(call("wipe", `{"dir":"scratch"}`))
|
|
_, err := w.do(call("wipe", `{"dir":"scratch"}`))
|
|
if len(w.asked) != 1 || err == nil || !strings.HasPrefix(err.Error(), "Not run: still waiting for the operator's answer") || w.runs != 0 {
|
|
t.Fatalf("asked %d, %v, runs %d", len(w.asked), err, w.runs)
|
|
}
|
|
}},
|
|
{"§17: an ask the router holds back as the fourth open one still waits", func(t *testing.T, w *world) {
|
|
_, _ = w.do(call("wipe", `{}`))
|
|
a := w.asked[0]
|
|
rec := w.records["shell."+a.ID]
|
|
rec.State, rec.HeldSince = "held-back", operatorchannelclient.FormatTime(w.now)
|
|
w.records["shell."+a.ID] = rec
|
|
if _, err := w.do(call("wipe", `{}`)); err == nil || !strings.Contains(err.Error(), "still waiting") || len(w.asked) != 1 {
|
|
t.Fatalf("%v, asked %d", err, len(w.asked))
|
|
}
|
|
}},
|
|
{"§14, §15: approved, the same call runs once; made again it is already run and asks anew", func(t *testing.T, w *world) {
|
|
_, _ = w.do(call("wipe", `{"dir":"scratch"}`))
|
|
w.decide(gate.OptionApprove, w.now.Add(time.Minute))
|
|
w.now = w.now.Add(2 * time.Minute)
|
|
if got, err := w.do(call("wipe", `{"dir":"scratch"}`)); err != nil || got.(gate.Answered).Answer != "done" || w.runs != 1 ||
|
|
!strings.HasPrefix(got.(gate.Answered).Approval, "run on the operator's approval via telegram at 2026-11-12T10:01:00Z, ask ") {
|
|
t.Fatalf("%v %v, runs %d", got, err, w.runs)
|
|
}
|
|
_, err := w.do(call("wipe", `{"dir":"scratch"}`))
|
|
if err == nil || !strings.Contains(err.Error(), "was already run; asked again") || w.runs != 1 || len(w.asked) != 2 {
|
|
t.Fatalf("%v, runs %d, asked %d", err, w.runs, len(w.asked))
|
|
}
|
|
}},
|
|
{"§15: two calls at the same moment run it once", func(t *testing.T, w *world) {
|
|
_, _ = w.do(call("wipe", `{}`))
|
|
w.decide(gate.OptionApprove, w.now)
|
|
var wg sync.WaitGroup
|
|
var mu sync.Mutex
|
|
runs := 0
|
|
for i := 0; i < 2; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
_, _ = w.g.Run(call("wipe", `{}`), func() (any, error) { mu.Lock(); runs++; mu.Unlock(); return "done", nil })
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
if runs != 1 {
|
|
t.Fatalf("ran %d times", runs)
|
|
}
|
|
}},
|
|
{"§14: approved for other arguments runs nothing", func(t *testing.T, w *world) {
|
|
_, _ = w.do(call("wipe", `{"dir":"scratch"}`))
|
|
a := w.decide(gate.OptionApprove, w.now)
|
|
rec := w.records["shell."+a.ID]
|
|
other, _ := gate.ActOf(call("wipe", `{"dir":"home"}`), "n1")
|
|
d, _ := operatorchannelclient.ActDigest(other)
|
|
a.Options[0].Binds = d // the ask the person was shown bound another act
|
|
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)
|
|
k.Ask = a
|
|
raw, _ := json.Marshal(k)
|
|
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)
|
|
}
|
|
}},
|
|
{"§14: approved 31 minutes ago asks anew", func(t *testing.T, w *world) {
|
|
_, _ = w.do(call("wipe", `{}`))
|
|
w.decide(gate.OptionApprove, w.now)
|
|
w.now = w.now.Add(31 * time.Minute)
|
|
if _, err := w.do(call("wipe", `{}`)); err == nil || !strings.Contains(err.Error(), "approved too long ago") || w.runs != 0 || len(w.asked) != 2 {
|
|
t.Fatalf("%v, runs %d, asked %d", err, w.runs, len(w.asked))
|
|
}
|
|
}},
|
|
{"§14, §17: declined, the same call is not asked again for 10 minutes", func(t *testing.T, w *world) {
|
|
_, _ = w.do(call("wipe", `{}`))
|
|
w.decide(gate.OptionDecline, w.now)
|
|
w.now = w.now.Add(9 * time.Minute)
|
|
if _, err := w.do(call("wipe", `{}`)); err == nil || !strings.Contains(err.Error(), "declined") || len(w.asked) != 1 || w.runs != 0 {
|
|
t.Fatalf("%v, asked %d", err, len(w.asked))
|
|
}
|
|
w.now = w.now.Add(2 * time.Minute)
|
|
if _, err := w.do(call("wipe", `{}`)); err == nil || !strings.Contains(err.Error(), "asked again") || len(w.asked) != 2 {
|
|
t.Fatalf("%v, asked %d", err, len(w.asked))
|
|
}
|
|
}},
|
|
{"§14, §17: expired asks anew", func(t *testing.T, w *world) {
|
|
_, _ = w.do(call("wipe", `{}`))
|
|
a := w.asked[0]
|
|
rec := w.records["shell."+a.ID]
|
|
rec.State = "expired"
|
|
w.records["shell."+a.ID] = rec
|
|
if _, err := w.do(call("wipe", `{}`)); err == nil || !strings.Contains(err.Error(), "ended expired; asked again") || len(w.asked) != 2 {
|
|
t.Fatalf("%v, asked %d", err, len(w.asked))
|
|
}
|
|
}},
|
|
{"§14: another asker's warrant runs nothing", func(t *testing.T, w *world) {
|
|
_, _ = w.do(call("wipe", `{}`))
|
|
a := w.decide(gate.OptionApprove, w.now)
|
|
rec := w.records["shell."+a.ID]
|
|
rec.Warrant.Asker = "mail"
|
|
w.records["shell."+a.ID] = rec
|
|
if _, err := w.do(call("wipe", `{}`)); err == nil || w.runs != 0 || !strings.Contains(err.Error(), "its warrant does not hold") {
|
|
t.Fatalf("%v, runs %d", err, w.runs)
|
|
}
|
|
}},
|
|
{"§17: the router's refusal is said with its words", func(t *testing.T, w *world) {
|
|
_, _ = w.do(call("wipe", `{}`))
|
|
a := w.asked[0]
|
|
rec := w.records["shell."+a.ID]
|
|
rec.State, rec.Answer = "refused", "the asker has too many open asks"
|
|
w.records["shell."+a.ID] = rec
|
|
if _, err := w.do(call("wipe", `{}`)); err == nil || !strings.Contains(err.Error(), "the asker has too many open asks") {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
}},
|
|
{"§20: a runner's own tool marked is refused unasked, never run", func(t *testing.T, w *world) {
|
|
c := call("wipe", `{}`)
|
|
c.Own = true
|
|
if _, err := w.do(c); err == nil || w.runs != 0 || len(w.asked) != 0 {
|
|
t.Fatalf("%v, runs %d, asked %d", err, w.runs, len(w.asked))
|
|
}
|
|
}},
|
|
{"§13: with no binding, an unmarked call runs and a marked one is refused", func(t *testing.T, w *world) {
|
|
w.deps.State = nil
|
|
w.rebuild()
|
|
if got, err := w.do(call("tidy", `{}`)); err != nil || got != "done" {
|
|
t.Fatalf("unmarked: %v %v", got, err)
|
|
}
|
|
if _, err := w.do(call("wipe", `{}`)); err == nil || !strings.Contains(err.Error(), "binds no state approvals") || w.runs != 1 {
|
|
t.Fatalf("marked: %v, runs %d", err, w.runs)
|
|
}
|
|
}},
|
|
{"§13: the fixture state refuses a key the bus refuses, so every key the gate writes here is one the bus holds (A-209)", func(t *testing.T, w *world) {
|
|
if _, err := w.state.Put("n1.act.sha256:00", "x"); err == nil {
|
|
t.Fatal("the fixture state held a key with a colon, which the bus refuses")
|
|
}
|
|
if _, err := w.state.Create("n1..ask", "x"); err == nil {
|
|
t.Fatal("the fixture state held a key with an empty name")
|
|
}
|
|
_, _ = w.do(call("wipe", `{"dir":"scratch"}`))
|
|
if len(w.asked) != 1 || len(w.state.Vals) == 0 {
|
|
t.Fatalf("the gate kept nothing through the fixture state: asked %d, keys %d", len(w.asked), len(w.state.Vals))
|
|
}
|
|
}},
|
|
{"§13: an ask over 256 KiB is refused naming the cause", func(t *testing.T, w *world) {
|
|
c := call("wipe", `{}`)
|
|
c.Description = strings.Repeat("w", 300*1024)
|
|
if _, err := w.do(c); err == nil || !strings.Contains(err.Error(), "more than the 262144 a value of the state approvals holds") || len(w.asked) != 0 {
|
|
t.Fatalf("%v, asked %d", err, len(w.asked))
|
|
}
|
|
}},
|
|
{"§3, §2: a seat verb its seat marks is asked for its holder", func(t *testing.T, w *world) {
|
|
c := gate.Call{Address: "node-login-shell.execute", Module: "shell", Seat: "node-login-shell", Name: "execute", Fingerprint: "src2:1", Args: json.RawMessage(`{"command":"ls"}`)}
|
|
if _, err := w.do(c); err == nil || !strings.HasPrefix(err.Error(), "Not run") || len(w.asked) != 1 || w.asked[0].Headline != "Run node-login-shell.execute on n1?" {
|
|
t.Fatalf("%v, asked %d", err, len(w.asked))
|
|
}
|
|
}},
|
|
{"§16: the run is recorded once; unanswered within five minutes it is a finding, cleared when warranted answers", func(t *testing.T, w *world) {
|
|
_, _ = w.do(call("wipe", `{}`))
|
|
a := w.decide(gate.OptionApprove, w.now)
|
|
stop, _ := w.g.WatchBounds()
|
|
defer stop()
|
|
w.warranted = errors.New("no responders")
|
|
if _, err := w.do(call("wipe", `{}`)); err != nil || w.runs != 1 || len(w.recorded) != 0 {
|
|
t.Fatalf("%v, runs %d, recorded %v", err, w.runs, w.recorded)
|
|
}
|
|
if w.bounds.keys["n1.unanswered."+a.ID] != gate.UnansweredAfter || w.bounds.keys["n1.unrecorded."+a.ID] != gate.KeptFor-gate.SaidBefore {
|
|
t.Fatalf("bounds %v", w.bounds.keys)
|
|
}
|
|
w.bounds.expire("n1.unanswered." + a.ID)
|
|
if w.findings.raised[keys.EscapeName("n1."+a.ID)] != gate.KindCallUnanswered {
|
|
t.Fatalf("findings %v", w.findings.raised)
|
|
}
|
|
w.warranted = nil
|
|
// At a start each acted, unrecorded ask is pending in the runner's findings-stated until warranted is made again
|
|
// (design 47 G §384).
|
|
pendingKey := views.FindingKey("machine", keys.EscapeName("n1."+a.ID), gate.KindCallUnanswered)
|
|
warranted := w.deps.Warranted
|
|
var pendingAtCall bool
|
|
w.deps.Warranted = func(asker, ask string, open func() (bool, error)) error {
|
|
_, pendingAtCall = w.findings.pends[pendingKey]
|
|
return warranted(asker, ask, open)
|
|
}
|
|
w.rebuild() // a start
|
|
w.g.Recover() // a start, or the hand-act seat's holder heard again
|
|
w.g.Recover()
|
|
if !pendingAtCall || len(w.findings.pended) != 1 || len(w.findings.pends) != 0 || w.findings.pended[0] != pendingKey {
|
|
t.Fatalf("pending at the call %v, pended %v, pending after %v", pendingAtCall, w.findings.pended, w.findings.pends)
|
|
}
|
|
if len(w.recorded) != 1 || w.recorded[0] != "shell."+a.ID || len(w.findings.raised) != 0 || len(w.bounds.keys) != 0 {
|
|
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)
|
|
if !k.Recorded || !k.Acted {
|
|
t.Fatalf("kept %+v", k)
|
|
}
|
|
}},
|
|
{"§19: with no holder of operator-channel in the view, a marked call is refused asking nothing", func(t *testing.T, w *world) {
|
|
w.holders["operator-channel"] = []byte(`{"seat":"operator-channel","scope":"mesh","holders":[]}`)
|
|
if _, err := w.do(call("wipe", `{}`)); err == nil || !strings.Contains(err.Error(), "no module holds operator-channel") || len(w.asked) != 0 {
|
|
t.Fatalf("%v, asked %d", err, len(w.asked))
|
|
}
|
|
w.holdersErr = errors.New("the first view has not been answered yet")
|
|
if _, err := w.do(call("wipe", `{}`)); err == nil || len(w.asked) != 0 {
|
|
t.Fatalf("an unread view: %v, asked %d", err, len(w.asked))
|
|
}
|
|
}},
|
|
{"§19: a publish that fails and a record that cannot be read refuse, running nothing", func(t *testing.T, w *world) {
|
|
pub := w.publish
|
|
w.publish = func(string, string, any, string) error { return errors.New("the bus refused it") }
|
|
if _, err := w.do(call("wipe", `{}`)); err == nil || !strings.Contains(err.Error(), "the ask could not be raised") {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
w.publish = pub
|
|
w.state.Vals = map[string]json.RawMessage{}
|
|
_, _ = w.do(call("wipe", `{}`))
|
|
w.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 {
|
|
t.Fatalf("%v, runs %d", err, w.runs)
|
|
}
|
|
}},
|
|
{"§5, §19: a served entry that cannot be read refuses every marked call on that runtime", func(t *testing.T, w *world) {
|
|
c := call("wipe", `{}`)
|
|
c.Fingerprint = "src2:unknown"
|
|
if _, err := w.do(c); err == nil || !strings.Contains(err.Error(), "cannot be read") || len(w.asked) != 0 {
|
|
t.Fatalf("%v, asked %d", err, len(w.asked))
|
|
}
|
|
}},
|
|
{"§19: the marks unread, a call waits within 5 seconds and is refused naming mesh_first_views", func(t *testing.T, w *world) {
|
|
w.cat.ready = errors.New("the catalog half has no first view of main yet")
|
|
waited := time.Duration(0)
|
|
w.deps.Wait = func(d time.Duration) error { waited = d; return w.cat.ready }
|
|
w.rebuild()
|
|
if _, err := w.do(call("tidy", `{}`)); err == nil || !strings.Contains(err.Error(), "mesh_first_views") || waited != gate.RecordWithin || w.runs != 0 {
|
|
t.Fatalf("%v, waited %s", err, waited)
|
|
}
|
|
w.deps.Wait = func(time.Duration) error { w.cat.ready = nil; return nil }
|
|
w.rebuild()
|
|
if _, err := w.do(call("tidy", `{}`)); err != nil || w.runs != 1 {
|
|
t.Fatalf("read within the wait: %v, runs %d", err, w.runs)
|
|
}
|
|
}},
|
|
{"§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")
|
|
if _, err := w.do(call("wipe", `{}`)); err == nil || !strings.Contains(err.Error(), "cannot be read") {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
if w.bounds.keys["n1.unreadable.approvals"] != gate.UnreadableAfter || len(w.findings.raised) != 0 {
|
|
t.Fatalf("bounds %v, findings %v", w.bounds.keys, w.findings.raised)
|
|
}
|
|
w.bounds.expire("n1.unreadable.approvals")
|
|
if w.findings.raised[keys.EscapeName("n1.approvals")] != gate.KindApprovalsUnreadable {
|
|
t.Fatalf("findings %v", w.findings.raised)
|
|
}
|
|
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)
|
|
}
|
|
}},
|
|
{"§18: the asks of the last day, one read whole, the marks applied with their sources, a record again and a cancel", func(t *testing.T, w *world) {
|
|
_, _ = w.do(call("wipe", `{"dir":"scratch"}`))
|
|
list, err := w.g.List()
|
|
if err != nil || len(list) != 1 || list[0].State != "asked" || list[0].Address != "shell.wipe" || list[0].Fingerprint == "" {
|
|
t.Fatalf("%+v %v", list, err)
|
|
}
|
|
whole, err := w.g.Read(list[0].Ask)
|
|
if err != nil || string(whole.Arguments) != `{"dir":"scratch"}` || !strings.HasPrefix(whole.Digest, "sha256:") {
|
|
t.Fatalf("%+v %v", whole, err)
|
|
}
|
|
applied := w.g.Applied()
|
|
if len(applied) != 1 || applied[0].Tools["wipe"] != "entry shell src2:1" || applied[0].Verbs["node-login-shell.execute"] != "seat node-login-shell" || applied[0].Revision != "7" {
|
|
t.Fatalf("applied %+v", applied)
|
|
}
|
|
if ok, src := w.g.Describe("shell", "", "wipe"); !ok || src != "entry shell src2:1" {
|
|
t.Errorf("describe %v %q", ok, src)
|
|
}
|
|
if ok, src := w.g.Describe("", "node-login-shell", "execute"); !ok || src != "seat node-login-shell" {
|
|
t.Errorf("describe a seat verb %v %q", ok, src)
|
|
}
|
|
if err := w.g.Again(list[0].Ask); err == nil {
|
|
t.Error("an ask not run was asked its hand-act again")
|
|
}
|
|
cancelled := ""
|
|
w.publish = func(module, subject string, body any, id string) error { cancelled = subject + " " + id; return nil }
|
|
if err := w.g.Cancel(list[0].Ask); err != nil || cancelled != "mesh.seat.operator-channel.accept.cancel.shell cancel."+list[0].Ask {
|
|
t.Fatalf("cancel %v %q", err, cancelled)
|
|
}
|
|
}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) { tc.run(t, newWorld()) })
|
|
}
|
|
// Design 46 7-approval-mark §5, §3, §4 and design 34 every-event §33: the marks a module's calls are gated by, derived from
|
|
// fixture catalog halves and served entries.
|
|
t.Run("TheMarks", func(t *testing.T) {
|
|
login := map[string]seatdef.Definition{"node-login-shell": {Seat: "node-login-shell",
|
|
Verbs: []seatdef.Verb{{Name: "execute", NeedsApproval: true}, {Name: "sessions"}}}}
|
|
v1 := entry("src2:1", []string{"node-login-shell.execute", "node-login-shell.sessions"},
|
|
map[string]any{"needs-approval": []any{"wipe"}})
|
|
v2 := entry("src2:2", []string{"node-login-shell.execute", "node-login-shell.sessions"},
|
|
map[string]any{"needs-approval": []any{"erase", "node-login-shell.sessions"}})
|
|
mail := catalogseat.CatalogEntry{Module: "mail", Fp: "src2:m", Manifest: map[string]any{
|
|
"needs-approval": []any{"mail.received"}, "emits": []any{"mail.received"},
|
|
"streams": []any{map[string]any{"name": "MAIL", "events": []any{"mail.**"}, "grant": map[string]any{"config": "readers", "events": "mail.{key}.**"}}}}}
|
|
for _, tc := range []struct {
|
|
name string
|
|
cat *fakeCatalog
|
|
module string
|
|
fp string
|
|
tools []string
|
|
verbs map[string]string
|
|
subjects []string
|
|
unread bool
|
|
}{
|
|
{name: "a seat's mark stands for every holder, and the holder's own adds up", cat: &fakeCatalog{revision: "7",
|
|
byFp: map[string]catalogseat.CatalogEntry{"src2:1": v1}, newest: map[string]catalogseat.CatalogEntry{"shell": v1}, defs: login},
|
|
module: "shell", fp: "src2:1", tools: []string{"wipe"}, verbs: map[string]string{"node-login-shell.execute": "seat node-login-shell"}},
|
|
{name: "a holder's needs-approval cannot unmark its seat's verb, and marks another", cat: &fakeCatalog{revision: "8",
|
|
byFp: map[string]catalogseat.CatalogEntry{"src2:2": v2}, newest: map[string]catalogseat.CatalogEntry{"shell": v2}, defs: login},
|
|
module: "shell", fp: "src2:2", tools: []string{"erase"}, verbs: map[string]string{"node-login-shell.execute": "seat node-login-shell",
|
|
"node-login-shell.sessions": "entry shell src2:2"}},
|
|
{name: "across a version change the union stands: a dropped mark is asked while the old bundle runs", cat: &fakeCatalog{revision: "9",
|
|
byFp: map[string]catalogseat.CatalogEntry{"src2:1": v1, "src2:2": v2}, newest: map[string]catalogseat.CatalogEntry{"shell": v2}, defs: login},
|
|
module: "shell", fp: "src2:1", tools: []string{"erase", "wipe"}, verbs: map[string]string{"node-login-shell.execute": "seat node-login-shell",
|
|
"node-login-shell.sessions": "entry shell src2:2"}},
|
|
{name: "a served entry that cannot be read leaves the newest marks, every marked call refused", cat: &fakeCatalog{revision: "9",
|
|
newest: map[string]catalogseat.CatalogEntry{"shell": v2}, defs: login},
|
|
module: "shell", fp: "src2:1", tools: []string{"erase"}, verbs: map[string]string{"node-login-shell.execute": "seat node-login-shell",
|
|
"node-login-shell.sessions": "entry shell src2:2"}, unread: true},
|
|
{name: "a bundle with no fingerprint has no served entry", cat: &fakeCatalog{revision: "9", defs: login},
|
|
module: "shell", unread: true},
|
|
{name: "a marked event and a stream with a grant give their subjects and each .>", cat: &fakeCatalog{revision: "3",
|
|
byFp: map[string]catalogseat.CatalogEntry{"src2:m": mail}, newest: map[string]catalogseat.CatalogEntry{"mail": mail}},
|
|
module: "mail", fp: "src2:m", subjects: []string{"mesh.mod.mail.event.mail.received", "mesh.mod.mail.event.mail.received.>",
|
|
"mesh.stream.mail.event.mail.>", "mesh.stream.mail.event.mail.received", "mesh.stream.mail.event.mail.received.>"}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
m := gate.Derive(tc.cat, tc.module, tc.fp)
|
|
if got := keysOf(m.Tools); !reflect.DeepEqual(got, tc.tools) {
|
|
t.Errorf("tools %v, want %v", got, tc.tools)
|
|
}
|
|
if tc.verbs == nil {
|
|
tc.verbs = map[string]string{}
|
|
}
|
|
if !reflect.DeepEqual(m.Verbs, tc.verbs) {
|
|
t.Errorf("verbs %v, want %v", m.Verbs, tc.verbs)
|
|
}
|
|
if got := keysOf(m.Subjects); !reflect.DeepEqual(got, tc.subjects) {
|
|
t.Errorf("subjects %v, want %v", got, tc.subjects)
|
|
}
|
|
if (m.Unread != "") != tc.unread || m.Revision != tc.cat.revision {
|
|
t.Errorf("unread %q, revision %q", m.Unread, m.Revision)
|
|
}
|
|
})
|
|
}
|
|
// §4: a bundle whose marks name a tool it does not register is not loaded, its finding mark-unserved raised; a bundle that
|
|
// serves it clears it, and a catalog without the mark too.
|
|
t.Run("mark-unserved raised and cleared", func(t *testing.T) {
|
|
cat := &fakeCatalog{revision: "7", byFp: map[string]catalogseat.CatalogEntry{"src2:1": v1}, newest: map[string]catalogseat.CatalogEntry{"shell": v1}, defs: login}
|
|
w := &findings{}
|
|
g := gate.New(gate.Deps{Node: "n1", Runner: "node-tools", Catalog: cat, Findings: Findings(w, "n1", "node-tools", nil)})
|
|
if got := g.Unserved("shell", "src2:1", []string{"other"}); !reflect.DeepEqual(got, []string{"wipe"}) || w.raised[keys.EscapeName("n1.shell")] != gate.KindMarkUnserved {
|
|
t.Fatalf("unserved %v, findings %v", got, w.raised)
|
|
}
|
|
if got := g.Unserved("shell", "src2:1", []string{"wipe"}); got != nil || w.raised[keys.EscapeName("n1.shell")] != "" {
|
|
t.Fatalf("a bundle serving the mark: unserved %v, findings %v", got, w.raised)
|
|
}
|
|
_ = g.Unserved("shell", "src2:1", nil)
|
|
cat.byFp["src2:1"], cat.newest["shell"] = entry("src2:1", nil, nil), entry("src2:1", nil, nil)
|
|
if got := g.Unserved("shell", "src2:1", nil); got != nil || w.raised[keys.EscapeName("n1.shell")] != "" {
|
|
t.Fatalf("a catalog without the mark: unserved %v, findings %v", got, w.raised)
|
|
}
|
|
})
|
|
})
|
|
}
|