A-9 tests: a bundle not serving its marked tool is refused at the load; a withheld marked call is said so
The runtime's load test gains rows (run with the Python fixture, before the bundles' skip): a bundle whose marks name a tool it does not register serves no tool of its module, and one that registers it serves. mesh_call's failure words move into Surface.callFailed, whose rows hold that only a marked tool reaching no responder names withheld and mesh-grants.grants_user_list. Builds: design 46 7-approval-mark §4, §6, §22 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -927,6 +927,21 @@ func schemaWithoutNode(raw json.RawMessage) map[string]any {
|
||||
}
|
||||
|
||||
// answerText is a discovery verb's answer as MCP content: JSON, indented.
|
||||
// callFailed is why a call failed, as mesh_call says it: a marked tool or verb that reaches no responder names `withheld`
|
||||
// and the grants part's read (design 46 7-approval-mark §6), and a target known only from an earlier round says so.
|
||||
func (s *Surface) callFailed(t target, err error) string {
|
||||
why := whyItFailed(t.Address, err)
|
||||
if ok, _ := s.markedTool(t); ok && errors.Is(err, bus.ErrNoResponders) {
|
||||
// A marked subject withheld from a machine whose tool runner does not honour the mark (§6).
|
||||
why += "; it needs the operator's approval, so it may be withheld where the machine's tool runner does not state " +
|
||||
"approval-mark/1: mesh-grants.grants_user_list names it under withheld"
|
||||
}
|
||||
if t.Stale != nil {
|
||||
why += " (" + t.Stale.say() + ")"
|
||||
}
|
||||
return why
|
||||
}
|
||||
|
||||
func answerText(v any) map[string]any {
|
||||
var b strings.Builder
|
||||
enc := json.NewEncoder(&b)
|
||||
@@ -1137,16 +1152,7 @@ func (s *Surface) discover(ctx context.Context, name string, args map[string]any
|
||||
got, err = callTool(s.conn, t.Key, callArgs, nil, x.Listing)
|
||||
}
|
||||
if err != nil {
|
||||
why := whyItFailed(t.Address, err)
|
||||
if ok, _ := s.markedTool(t); ok && errors.Is(err, bus.ErrNoResponders) {
|
||||
// A marked subject withheld from a machine whose tool runner does not honour the mark (§6).
|
||||
why += "; it needs the operator's approval, so it may be withheld where the machine's tool runner does not state " +
|
||||
"approval-mark/1: mesh-grants.grants_user_list names it under withheld"
|
||||
}
|
||||
if t.Stale != nil {
|
||||
why += " (" + t.Stale.say() + ")"
|
||||
}
|
||||
return failure(why)
|
||||
return failure(s.callFailed(t, err))
|
||||
}
|
||||
result, unread := resolveSent(s.conn, got.Result)
|
||||
content := []map[string]any{{"type": "text", "text": pretty(result)}}
|
||||
|
||||
@@ -11,7 +11,8 @@ import (
|
||||
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
||||
)
|
||||
|
||||
// A tool's own error that mentions 503 is the tool's answer, not a routing fault (issue 1529); a real no-responders still says
|
||||
// A marked tool or verb that reaches no responder names withheld and the grants part's read (design 46 7-approval-mark §6);
|
||||
// an unmarked one, or a marked one failing otherwise, does not. A tool's own error that mentions 503 is the tool's answer, not a routing fault (issue 1529); a real no-responders still says
|
||||
// nothing serves.
|
||||
func TestARealNoRespondersStillSaysNothingServes(t *testing.T) {
|
||||
got := whyItFailed("scanner.scan", errors.New("the scanner refused the scan job: 503 Service Unavailable"))
|
||||
@@ -23,4 +24,20 @@ func TestARealNoRespondersStillSaysNothingServes(t *testing.T) {
|
||||
t.Fatalf("%v: got %q", err, got)
|
||||
}
|
||||
}
|
||||
s := &Surface{Approvals: &fakeApprovals{marked: "shell.wipe"}}
|
||||
for _, c := range []struct {
|
||||
target target
|
||||
err error
|
||||
hinted bool
|
||||
}{
|
||||
{target{Address: "shell.wipe", Tool: Tool{Module: "shell", Name: "wipe"}}, bus.ErrNoResponders, true},
|
||||
{target{Address: "node-login-shell.execute", Seat: true, Tool: Tool{Module: "node-login-shell", Name: "execute"}}, bus.ErrNoResponders, false},
|
||||
{target{Address: "shell.tidy", Tool: Tool{Module: "shell", Name: "tidy"}}, bus.ErrNoResponders, false},
|
||||
{target{Address: "shell.wipe", Tool: Tool{Module: "shell", Name: "wipe"}}, errors.New("its own failure"), false},
|
||||
} {
|
||||
got := s.callFailed(c.target, c.err)
|
||||
if hinted := strings.Contains(got, "approval-mark/1: mesh-grants.grants_user_list names it under withheld"); hinted != c.hinted {
|
||||
t.Errorf("%s (%v): %q", c.target.Address, c.err, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,17 +554,23 @@ func TestMeshWaitIsAnnouncedAmongSeven(t *testing.T) {
|
||||
func fmtAnswer(out map[string]any) string { raw, _ := json.Marshal(out); return string(raw) }
|
||||
|
||||
// fakeApprovals is a gate as mesh_approvals reads it.
|
||||
type fakeApprovals struct{ cancelled string }
|
||||
type fakeApprovals struct {
|
||||
cancelled string
|
||||
// marked is the one `<module>.<tool>` its Describe says is marked.
|
||||
marked string
|
||||
}
|
||||
|
||||
func (f *fakeApprovals) List() ([]approval.Listed, error) {
|
||||
return []approval.Listed{{Ask: "a1", State: "asked"}}, nil
|
||||
}
|
||||
func (f *fakeApprovals) Read(id string) (approval.Whole, error) { return approval.Whole{}, nil }
|
||||
func (f *fakeApprovals) Applied() []approval.Marks { return nil }
|
||||
func (f *fakeApprovals) Describe(_, _, _ string) (bool, string) { return false, "" }
|
||||
func (f *fakeApprovals) Again(string) error { return nil }
|
||||
func (f *fakeApprovals) Cancel(id string) error { f.cancelled = id; return nil }
|
||||
func (f *fakeApprovals) Fill() (uint64, int64, error) { return 10, 64 << 20, nil }
|
||||
func (f *fakeApprovals) Describe(module, _, name string) (bool, string) {
|
||||
return f.marked != "" && f.marked == module+"."+name, "entry " + module
|
||||
}
|
||||
func (f *fakeApprovals) Again(string) error { return nil }
|
||||
func (f *fakeApprovals) Cancel(id string) error { f.cancelled = id; return nil }
|
||||
func (f *fakeApprovals) Fill() (uint64, int64, error) { return 10, 64 << 20, nil }
|
||||
|
||||
// A client that hangs up or cancels ends its wait at once: the subscription is closed and nothing is left
|
||||
// waiting for the timeout (the review of issue 745). Over the console's own HTTP, as an agent host calls it.
|
||||
|
||||
@@ -4,10 +4,12 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-tools/node-tools/internal/approval"
|
||||
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
||||
"github.com/novox/mesh-tools/node-tools/internal/launch"
|
||||
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
|
||||
@@ -192,10 +194,49 @@ func TestEachBundleIsGivenItsOwnEnvironment(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// An entrypoint that is not executable is refused by name, and the others serve (ADR 0193).
|
||||
// unservingGate is an approval gate whose marks of each module name the tools given, so a bundle not registering them is
|
||||
// refused at the load (design 46 7-approval-mark §4).
|
||||
type unservingGate map[string][]string
|
||||
|
||||
func (g unservingGate) Unserved(module, _ string, registered []string) []string {
|
||||
var missing []string
|
||||
for _, tool := range g[module] {
|
||||
if !slices.Contains(registered, tool) {
|
||||
missing = append(missing, tool)
|
||||
}
|
||||
}
|
||||
return missing
|
||||
}
|
||||
func (unservingGate) Run(_ approval.Call, run func() (any, error)) (any, error) { return run() }
|
||||
|
||||
// An entrypoint that is not executable is refused by name, and the others serve (ADR 0193). A bundle whose marks name a
|
||||
// tool it does not register is refused at the load, serving no tool of its module, and one that registers it serves
|
||||
// (design 46 7-approval-mark §4).
|
||||
func TestAnEntrypointThatIsNotExecutableIsRefused(t *testing.T) {
|
||||
mt.NeedBundles(t)
|
||||
mesh := mt.New(t)
|
||||
{
|
||||
mesh.Issue(t, mt.MembershipOf("marked", "anchor", true, nil))
|
||||
mesh.Issue(t, mt.MembershipOf("served", "anchor", true, nil))
|
||||
nodeTools := connect(t, "node-tools", "anchor")
|
||||
asker := connect(t, "console", "workstation")
|
||||
logs := &mt.Logs{}
|
||||
stop, err := Run(nodeTools, []Served{{"marked", []string{mt.Fixture("caller-echo.py")}}, {"served", []string{mt.Fixture("caller-echo.py")}}},
|
||||
nil, logs.Logf, WithGate(unservingGate{"marked": {"wipe"}, "served": {"who"}}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !logs.Has("marked's bundle", "failed to load: its marks name wipe, which it does not register (design 46 7-approval-mark §4)") {
|
||||
t.Errorf("a bundle not serving its marked tool was not refused at the load:\n%s", logs.All())
|
||||
}
|
||||
if _, err := call(t, asker, "served.who@anchor", map[string]any{}); err != nil {
|
||||
t.Errorf("a bundle serving its marked tool: %v", err)
|
||||
}
|
||||
if _, err := asker.Ask("marked.who@anchor", map[string]any{}, ""); !errors.Is(err, bus.ErrNoResponders) {
|
||||
t.Errorf("a refused bundle's tool answered: %v", err)
|
||||
}
|
||||
stop()
|
||||
}
|
||||
mt.NeedBundles(t)
|
||||
mesh.Issue(t, mt.MembershipOf("alpha", "anchor", false, nil))
|
||||
mesh.Issue(t, mt.MembershipOf("plain", "anchor", false, nil))
|
||||
nodeTools := connect(t, "node-tools", "anchor")
|
||||
|
||||
Reference in New Issue
Block a user