Files
mesh-sdk/go/approval/approval_test.go
T
jschoubben d6ca425737 Let a tool runner's catalog-half reads through its gate while that half is unread (A-353)
A runner serving the indexer's tools refused every call until its own catalog half was read, and that read is a call of
the same seat verbs through the same gate: G could not start. The four seat verbs the catalog half is read by now run
unmarked while it is unread; every other call waits and is refused as before.

Builds: design 47 G lead answers A-353, A-359

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-13 17:28:58 +01:00

134 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package approval
import (
"encoding/json"
"errors"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/operatorask"
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
// call is a fixture call of the marked tool wipe.
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."}
}
// Design 46 7-approval-mark §8, §10, §11: the act's fields give each call its digest, and the ask shows a call's arguments
// in their shape and its headline cut (the gate's other cases run on the tool runner's fixture mesh, node-tools
// internal/approval).
func TestTheActAndItsAsk(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, _ := operatorask.ActDigest(act)
return d
}
act, _ := ActOf(base, "n1")
want := operatorask.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)
}
}
// unread is a catalog half whose first view is not read yet.
type unread struct{}
func (unread) Ready() error { return errors.New("the registrations are not answered") }
func (unread) Revision() string { return "" }
func (unread) Served(string) (Entry, bool, string) { return Entry{}, false, "not read" }
func (unread) Newest(string) (Entry, bool) { return Entry{}, false }
func (unread) Definitions() map[string]seatdef.Definition { return nil }
func (unread) Entries() []Entry { return nil }
// Design 47 G lead answers A-353, A-359: while its catalog half is unread, the gate lets through the four seat verbs that
// half is read by, told from the call's seat and verb, and holds every other call as before.
func TestAnUnreadCatalogHalfLetsItsOwnReadsThrough(t *testing.T) {
waited := 0
g := New(Deps{Node: "n1", Runner: "node-tools", Catalog: unread{}, Wait: func(time.Duration) error {
waited++
return errors.New("still unread")
}})
for _, tc := range []struct {
seat, name string
runs bool
}{
{"mesh-catalog", "registrations", true},
{"mesh-catalog", "revision", true},
{"mesh-catalog", "entry", true},
{"mesh-delivery", "source", true},
{"mesh-catalog", "register", false},
{"mesh-delivery", "registrations", false},
{"", "registrations", false}, // a module's tool of that name is no seat verb
{"", "revision", false},
} {
ran := false
address := tc.seat + "." + tc.name
if tc.seat == "" {
address = "mesh-delivery-indexer." + tc.name
}
_, err := g.Run(Call{Address: address, Module: "mesh-delivery-indexer", Seat: tc.seat, Name: tc.name, Fingerprint: "fp1",
Caller: "node-tools"}, func() (any, error) { ran = true; return "ok", nil })
if ran != tc.runs || (err == nil) != tc.runs {
t.Errorf("%s: ran %v (%v), want ran %v", address, ran, err, tc.runs)
}
if !tc.runs && (err == nil || !strings.Contains(err.Error(), "are not read yet")) {
t.Errorf("%s: refused with %v, want the marks not read yet", address, err)
}
}
if waited != 4 {
t.Errorf("the other calls waited for the catalog half %d times, want 4 (the four start reads wait for nothing)", waited)
}
}