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
529 lines
24 KiB
Go
529 lines
24 KiB
Go
package approval
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"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/contracts/go/seats/meshcatalog"
|
||
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 (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
|
||
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
|
||
}
|
||
|
||
// 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.g = New(Deps{Node: "n1", Runner: "node-tools", Catalog: w.cat, State: w.state, Findings: w.findings, Bounds: w.bounds,
|
||
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
|
||
},
|
||
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
|
||
},
|
||
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.g.Unserved("shell", "src2:1", []string{"wipe", "tidy"})
|
||
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 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) Call {
|
||
return Call{Address: "shell." + name, Module: "shell", Name: name, Fingerprint: "src2:1", Caller: "agent", Args: json.RawMessage(args),
|
||
Description: "Wipes a scratch directory.\nMore words."}
|
||
}
|
||
|
||
// theActAndItsAsk is §8's and §11's rows: the act's fields, and how an ask shows a call's arguments.
|
||
func theActAndItsAsk(t *testing.T) {
|
||
base := call("wipe", `{"dir":"scratch","n":1}`)
|
||
digest := func(c Call) string {
|
||
act, err := ActOf(c, "n1")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
d, _ := operatorchannelclient.ActDigest(act)
|
||
return d
|
||
}
|
||
act, _ := ActOf(base, "n1")
|
||
want := operatorchannelclient.Act{"address": "shell.wipe", "node": "n1", "module": "shell", "tool": "wipe", "build": "src2:1",
|
||
"arg.dir": "scratch", "arg.n": "1"}
|
||
if len(act) != len(want) {
|
||
t.Fatalf("act %v", act)
|
||
}
|
||
for k, v := range want {
|
||
if act[k] != v {
|
||
t.Errorf("act[%s] = %q, want %q", k, act[k], v)
|
||
}
|
||
}
|
||
seen := map[string]string{digest(base): "the call"}
|
||
for name, c := range map[string]Call{
|
||
"an omitted argument": call("wipe", `{"dir":"scratch"}`),
|
||
"another number": call("wipe", `{"dir":"scratch","n":2}`),
|
||
"an object": call("wipe", `{"dir":{"name":"scratch"},"n":1}`),
|
||
"a changed build": func() Call { c := base; c.Fingerprint = "src2:2"; return c }(),
|
||
"another node's seat": {Address: "node-login-shell.execute", Module: "shell", Seat: "node-login-shell", Name: "execute", Fingerprint: "src2:1"},
|
||
} {
|
||
d := digest(c)
|
||
if other, dup := seen[d]; dup {
|
||
t.Errorf("%s gives the digest of %s", name, other)
|
||
}
|
||
seen[d] = name
|
||
}
|
||
if got, _ := Canonical(json.RawMessage(`{"b":[1.0,2.5e-7,1e21],"a":"é<"}`)); got != `{"a":"é<","b":[1,2.5e-7,1e+21]}` {
|
||
t.Errorf("canonical form %s", got)
|
||
}
|
||
// §11: a path, an address and a long argument are shown in their shape and cut.
|
||
text, whole := shown(json.RawMessage(`{"dir":"/home/someone/scratch","host":"10.0.0.7","ok":"yes"}`))
|
||
if whole || text != "dir: ‹path›\nhost: ‹address›\nok: yes" {
|
||
t.Errorf("shown %q whole %v", text, whole)
|
||
}
|
||
long, _ := json.Marshal(map[string]string{"text": strings.Repeat("word ", 500)})
|
||
if text, whole := shown(long); whole || !strings.HasSuffix(text, "NOT SHOWN IN FULL") || len(text) > PartBytes+len("\nNOT SHOWN IN FULL") {
|
||
t.Errorf("a long argument shown whole %v, %d bytes", whole, len(text))
|
||
}
|
||
// §10: the headline, cut with … to 60 characters, ending Part shown.
|
||
for _, tc := range []struct{ name, node, want string }{
|
||
{"wipe", "n1", "Run wipe on n1?"},
|
||
{strings.Repeat("x", 50), "n1", "Run " + strings.Repeat("x", 50) + "?"},
|
||
{strings.Repeat("y", 80), "n1", "Run " + strings.Repeat("y", 54) + "…?"},
|
||
} {
|
||
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("Run", "wipe", "n1", false); got != "Run wipe on n1? Part shown" {
|
||
t.Errorf("headline %q", got)
|
||
}
|
||
}
|
||
|
||
// 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) {
|
||
t.Run("§8, §10, §11: the act's fields give each its digest, and the ask shows the arguments", theActAndItsAsk)
|
||
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.g.firstCall(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.g.firstCall(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.g.firstCall(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, _ := ActOf(call("wipe", `{"dir":"scratch"}`), "n1")
|
||
d, _ := operatorchannelclient.ActDigest(act)
|
||
if a.About != d || a.Who != "operator" || a.OnExpiry != 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."+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) {
|
||
w.takes = false
|
||
start := w.now
|
||
_, err := w.g.firstCall(call("wipe", `{}`))
|
||
if err == nil || !strings.HasPrefix(err.Error(), "Not run, asked; not yet taken by the router (ask ") || w.now.Sub(start) > 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(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.(Answered).Answer != "done" || w.runs != 1 ||
|
||
!strings.HasPrefix(got.(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(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(OptionApprove, w.now)
|
||
rec := w.records["shell."+a.ID]
|
||
other, _ := 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(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(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(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.g.d.State = nil
|
||
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: 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 := 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(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] != UnansweredAfter || w.bounds.keys["n1.unrecorded."+a.ID] != KeptFor-SaidBefore {
|
||
t.Fatalf("bounds %v", w.bounds.keys)
|
||
}
|
||
w.bounds.expire("n1.unanswered." + a.ID)
|
||
if w.findings.raised[keys.EscapeName("n1."+a.ID)] != KindCallUnanswered {
|
||
t.Fatalf("findings %v", w.findings.raised)
|
||
}
|
||
w.warranted = nil
|
||
w.g.Recover() // a start, or the hand-act seat's holder heard again
|
||
w.g.Recover()
|
||
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.g.d.Publish
|
||
w.g.d.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.g.d.Publish = pub
|
||
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 {
|
||
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.g.d.Wait = func(d time.Duration) error { waited = d; return w.cat.ready }
|
||
if _, err := w.do(call("tidy", `{}`)); err == nil || !strings.Contains(err.Error(), "mesh_first_views") || waited != RecordWithin || w.runs != 0 {
|
||
t.Fatalf("%v, waited %s", err, waited)
|
||
}
|
||
w.g.d.Wait = func(time.Duration) error { w.cat.ready = nil; return nil }
|
||
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"] != 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")] != 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.g.d.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()) })
|
||
}
|
||
}
|