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
144 lines
7.2 KiB
Go
144 lines
7.2 KiB
Go
package approval
|
|
|
|
import (
|
|
"context"
|
|
"reflect"
|
|
"testing"
|
|
|
|
condseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
|
"git.novox.be/novox/mesh-sdk/go/keys"
|
|
|
|
catalogseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
|
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
|
)
|
|
|
|
// fakeCatalog is a catalog half as a fixture: entries by fingerprint, the newest by module, the seat definitions.
|
|
type fakeCatalog struct {
|
|
ready error
|
|
revision string
|
|
byFp map[string]catalogseat.CatalogEntry
|
|
newest map[string]catalogseat.CatalogEntry
|
|
defs map[string]seatdef.Definition
|
|
}
|
|
|
|
func (c *fakeCatalog) Ready() error { return c.ready }
|
|
func (c *fakeCatalog) Revision() string { return c.revision }
|
|
func (c *fakeCatalog) Served(fp string) (catalogseat.CatalogEntry, bool, string) {
|
|
e, ok := c.byFp[fp]
|
|
return e, ok, "not-found"
|
|
}
|
|
func (c *fakeCatalog) Newest(m string) (catalogseat.CatalogEntry, bool) {
|
|
e, ok := c.newest[m]
|
|
return e, ok
|
|
}
|
|
func (c *fakeCatalog) Definitions() map[string]seatdef.Definition { return c.defs }
|
|
func (c *fakeCatalog) Entries() []catalogseat.CatalogEntry {
|
|
var out []catalogseat.CatalogEntry
|
|
for _, e := range c.newest {
|
|
out = append(out, e)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func entry(fp string, serves []string, manifest map[string]any) catalogseat.CatalogEntry {
|
|
return catalogseat.CatalogEntry{Module: "shell", Fp: fp, Serves: serves, Manifest: manifest}
|
|
}
|
|
|
|
// 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.
|
|
func TestTheMarks(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 := 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 := New(Deps{Node: "n1", Runner: "node-tools", Catalog: cat, Findings: w})
|
|
if got := g.Unserved("shell", "src2:1", []string{"other"}); !reflect.DeepEqual(got, []string{"wipe"}) || w.raised[keys.EscapeName("n1.shell")] != 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)
|
|
}
|
|
})
|
|
}
|
|
|
|
// findings is the runner's held writer as a fixture: each finding raised now, by id.
|
|
type findings struct{ raised map[string]string }
|
|
|
|
func (f *findings) Write(_ context.Context, x condseat.Finding) error {
|
|
if f.raised == nil {
|
|
f.raised = map[string]string{}
|
|
}
|
|
f.raised[x.ID] = x.Kind
|
|
return nil
|
|
}
|
|
func (f *findings) Clear(_ context.Context, _, id, _, _ string) error {
|
|
delete(f.raised, id)
|
|
return nil
|
|
}
|