diff --git a/merge-check.sh b/merge-check.sh index 37449c2..e4912d2 100644 --- a/merge-check.sh +++ b/merge-check.sh @@ -26,9 +26,12 @@ fi CGO_ENABLED=0 go vet ./... packages=$(go list ./...) +bundleless="" if ! command -v node >/dev/null 2>&1 || [ ! -d node_modules/@novox/mesh-sdk ]; then echo "NOT TESTED HERE: internal/runtime and internal/console launch bundles that need node and @novox/mesh-sdk" packages=$(printf '%s\n' "$packages" | grep -v -e '/internal/runtime$' -e '/internal/console$') + # What of the console launches no bundle runs anyway: how an address resolves (novox/hq issue 284). + bundleless='^TestASeatAndAModuleOfOneNameAreEachReached$' fi if command -v gcc >/dev/null 2>&1; then CGO_ENABLED=1 go test -race -count=1 $packages @@ -36,4 +39,7 @@ else echo "NOT RACE-CHECKED: the toolchain holds no C compiler; the suite runs without the race detector" CGO_ENABLED=0 go test -count=1 $packages fi +if [ -n "$bundleless" ]; then + CGO_ENABLED=0 go test -count=1 -run "$bundleless" ./internal/console +fi echo "NOT TESTED HERE: node-tools/src (TypeScript) needs @novox/mesh-sdk from the package registry" diff --git a/node-tools/internal/console/address.go b/node-tools/internal/console/address.go index 3754936..d585500 100644 --- a/node-tools/internal/console/address.go +++ b/node-tools/internal/console/address.go @@ -243,100 +243,17 @@ func indexOn(conn *bus.Conn) (*index, error) { return nil, err } - l := &Listing{Tools: []Tool{}, NotAnswering: []string{}} - x := &index{Modules: map[string]*moduleInfo{}, Listing: l, Discovery: d, Recorded: map[string][]string{}} + var endpoints []announce.Endpoint + for _, h := range d.Heard { + endpoints = append(endpoints, announce.Endpoints(h.Info)...) + } + x, announced, machines := indexOfEndpoints(endpoints) + l := x.Listing + x.Discovery = d for _, s := range d.Silent { x.Unheard = append(x.Unheard, unheard{Runtime: s.Name, Machine: s.Machine, Why: fmt.Sprintf("it answered PING; what it serves did not arrive within %s", announce.Patience)}) } - announced := map[string]map[string]bool{} // module → node → announced something - seats := map[string]*seatInfo{} - toolAt := map[string]int{} // . or . → index in l.Tools - machines := map[string]bool{} - for _, h := range d.Heard { - for _, e := range announce.Endpoints(h.Info) { - if e.Node != "" { - machines[e.Node] = true - } - if announced[e.Module] == nil { - announced[e.Module] = map[string]bool{} - } - announced[e.Module][e.Node] = true - switch e.Kind { - case announce.KindSeat: - st := seats[e.Seat] - if st == nil { - st = &seatInfo{Seat: e.Seat, Scope: e.Scope} - seats[e.Seat] = st - } - if st.Scope != "node" && e.Scope == "node" { - st.Scope = "node" - } - h := holder{Module: e.Module, Node: e.Node} - if !containsHolder(st.Holders, h) { - st.Holders = append(st.Holders, h) - } - key := e.Seat + "." + e.Tool - if _, have := toolAt[key]; !have { - toolAt[key] = len(l.Tools) - t := Tool{Module: e.Seat, Name: e.Tool, Description: e.Description, Input: e.Schema, Seat: true, Scope: st.Scope} - l.Tools = append(l.Tools, t) - st.Verbs = append(st.Verbs, t) - } - case announce.KindTool: - m := x.Modules[e.Module] - if m == nil { - m = &moduleInfo{Module: e.Module} - x.Modules[e.Module] = m - } - if e.Node != "" && !contains(m.On, e.Node) { - m.On = append(m.On, e.Node) - } - m.Interchangeable = m.Interchangeable || e.Interchangeable - key := e.Module + "." + e.Tool - i, have := toolAt[key] - if !have { - i = len(l.Tools) - toolAt[key] = i - l.Tools = append(l.Tools, Tool{Module: e.Module, Name: e.Tool, Description: e.Description, Input: e.Schema}) - } - if !contains(l.Tools[i].Subjects, e.Subject) { - l.Tools[i].Subjects = append(l.Tools[i].Subjects, e.Subject) - } - } - } - } - // A tool's subjects as a call looks them up: the plain one any instance answers first, then each - // machine's. - for i := range l.Tools { - t := &l.Tools[i] - if t.Seat { - continue - } - plain := "mesh.mod." + t.Module + ".tool." + t.Name - sort.SliceStable(t.Subjects, func(a, b int) bool { - if (t.Subjects[a] == plain) != (t.Subjects[b] == plain) { - return t.Subjects[a] == plain - } - return t.Subjects[a] < t.Subjects[b] - }) - } - for _, t := range l.Tools { - if !t.Seat { - x.Modules[t.Module].Tools = append(x.Modules[t.Module].Tools, t) - } - } - for _, m := range x.Modules { - sort.Strings(m.On) - } - for _, st := range seats { - sort.Slice(st.Holders, func(a, b int) bool { return st.Holders[a].Node < st.Holders[b].Node }) - x.Seats = append(x.Seats, *st) - } - sort.Slice(x.Seats, func(i, j int) bool { return x.Seats[i].Seat < x.Seats[j].Seat }) - sort.SliceStable(l.Tools, func(i, j int) bool { - return l.Tools[i].Module+"."+l.Tools[i].Name < l.Tools[j].Module+"."+l.Tools[j].Name - }) // What should have answered: every assignment of a module that declares tools. Silence is named; // a module with no tools is never a name here. @@ -410,6 +327,104 @@ func (s *Surface) indexAsked(fresh bool) (*index, error) { return x, nil } +// indexOfEndpoints is what the announced endpoints say: every seat with its verbs and holders, every +// module with its tools and machines, the flat listing, and which module announced on which machine. +func indexOfEndpoints(endpoints []announce.Endpoint) (*index, map[string]map[string]bool, map[string]bool) { + l := &Listing{Tools: []Tool{}, NotAnswering: []string{}} + x := &index{Modules: map[string]*moduleInfo{}, Listing: l, Recorded: map[string][]string{}} + announced := map[string]map[string]bool{} // module → node → announced something + seats := map[string]*seatInfo{} + // ., or seat:., → index in l.Tools. **Apart**, because a seat and a module may + // share a name — mesh-delivery is the delivery's seat and the module holding it (novox/hq ADR 0239), and + // the module answers the seat's verbs with tools of the same names: in one namespace the module's tool + // took the key first, and the seat was listed with no verb at all (novox/hq issue 284). + toolAt := map[string]int{} + machines := map[string]bool{} + for _, e := range endpoints { + if e.Node != "" { + machines[e.Node] = true + } + if announced[e.Module] == nil { + announced[e.Module] = map[string]bool{} + } + announced[e.Module][e.Node] = true + switch e.Kind { + case announce.KindSeat: + st := seats[e.Seat] + if st == nil { + st = &seatInfo{Seat: e.Seat, Scope: e.Scope} + seats[e.Seat] = st + } + if st.Scope != "node" && e.Scope == "node" { + st.Scope = "node" + } + h := holder{Module: e.Module, Node: e.Node} + if !containsHolder(st.Holders, h) { + st.Holders = append(st.Holders, h) + } + key := "seat:" + e.Seat + "." + e.Tool + if _, have := toolAt[key]; !have { + toolAt[key] = len(l.Tools) + t := Tool{Module: e.Seat, Name: e.Tool, Description: e.Description, Input: e.Schema, Seat: true, Scope: st.Scope} + l.Tools = append(l.Tools, t) + st.Verbs = append(st.Verbs, t) + } + case announce.KindTool: + m := x.Modules[e.Module] + if m == nil { + m = &moduleInfo{Module: e.Module} + x.Modules[e.Module] = m + } + if e.Node != "" && !contains(m.On, e.Node) { + m.On = append(m.On, e.Node) + } + m.Interchangeable = m.Interchangeable || e.Interchangeable + key := e.Module + "." + e.Tool + i, have := toolAt[key] + if !have { + i = len(l.Tools) + toolAt[key] = i + l.Tools = append(l.Tools, Tool{Module: e.Module, Name: e.Tool, Description: e.Description, Input: e.Schema}) + } + if !contains(l.Tools[i].Subjects, e.Subject) { + l.Tools[i].Subjects = append(l.Tools[i].Subjects, e.Subject) + } + } + } + // A tool's subjects as a call looks them up: the plain one any instance answers first, then each + // machine's. + for i := range l.Tools { + t := &l.Tools[i] + if t.Seat { + continue + } + plain := "mesh.mod." + t.Module + ".tool." + t.Name + sort.SliceStable(t.Subjects, func(a, b int) bool { + if (t.Subjects[a] == plain) != (t.Subjects[b] == plain) { + return t.Subjects[a] == plain + } + return t.Subjects[a] < t.Subjects[b] + }) + } + for _, t := range l.Tools { + if !t.Seat { + x.Modules[t.Module].Tools = append(x.Modules[t.Module].Tools, t) + } + } + for _, m := range x.Modules { + sort.Strings(m.On) + } + for _, st := range seats { + sort.Slice(st.Holders, func(a, b int) bool { return st.Holders[a].Node < st.Holders[b].Node }) + x.Seats = append(x.Seats, *st) + } + sort.Slice(x.Seats, func(i, j int) bool { return x.Seats[i].Seat < x.Seats[j].Seat }) + sort.SliceStable(l.Tools, func(i, j int) bool { + return l.Tools[i].Module+"."+l.Tools[i].Name < l.Tools[j].Module+"."+l.Tools[j].Name + }) + return x, announced, machines +} + // target is what an address resolves to. type target struct { Address string @@ -433,9 +448,21 @@ func resolve(x *index, address string) (target, error) { } prefix, name := rest[:dot], rest[dot+1:] - if s := x.seat(prefix); s != nil { + s := x.seat(prefix) + if s != nil && x.Modules[prefix] != nil && findTool(x.Modules[prefix].Tools, name) != nil { + // **A seat and a module of one name** (novox/hq issue 284): mesh-delivery is the delivery's seat and + // the module holding it. The module's tool is meant when the seat has no such verb, or when a machine + // is named for a seat held once for the mesh — `/.` is a module on one machine. + if findTool(s.Verbs, name) == nil || (node != "" && s.Scope != "node") { + s = nil + } + } + if s != nil { verb := findTool(s.Verbs, name) if verb == nil { + if len(s.Verbs) == 0 { + return target{}, fmt.Errorf("the seat %s has no verb %s: its holder announced none", prefix, name) + } return target{}, fmt.Errorf("the seat %s has no verb %s; it has %s", prefix, name, toolNames(s.Verbs)) } if s.Scope == "node" { diff --git a/node-tools/internal/console/one_name_test.go b/node-tools/internal/console/one_name_test.go new file mode 100644 index 0000000..14c7c98 --- /dev/null +++ b/node-tools/internal/console/one_name_test.go @@ -0,0 +1,57 @@ +package console + +import ( + "strings" + "testing" + + "github.com/novox/mesh-tools/node-tools/internal/announce" +) + +// **Issue 284**: mesh-delivery is the delivery's seat, held once for the mesh, and the module holding it, +// which answers the seat's verbs with tools of the same names and has tools of its own. Discovery put the +// module's tool and the seat's verb under one key, so the seat was listed with no verb; and every address +// with the name resolved to the seat — `mesh-delivery.deliveries` and `novox/mesh-delivery.delivery_status` +// alike were refused, the seat's verbs and the module's tools both out of reach. +func TestASeatAndAModuleOfOneNameAreEachReached(t *testing.T) { + var endpoints []announce.Endpoint + for _, tool := range []string{"deliveries", "delivery_status"} { + endpoints = append(endpoints, announce.Endpoint{Kind: announce.KindTool, Module: "mesh-delivery", Tool: tool, + Node: "anchor", Subject: "mesh.mod.mesh-delivery.tool." + tool + ".anchor"}) + } + for _, verb := range []string{"deliveries", "stalled"} { + endpoints = append(endpoints, announce.Endpoint{Kind: announce.KindSeat, Module: "mesh-delivery", Seat: "mesh-delivery", + Tool: verb, Scope: "mesh", Subject: "mesh.seat.mesh-delivery.tool." + verb}) + } + x, _, _ := indexOfEndpoints(endpoints) + + s := x.seat("mesh-delivery") + if s == nil || len(s.Verbs) != 2 { + t.Fatalf("the seat is listed with verbs %v, not its two", s) + } + if m := x.Modules["mesh-delivery"]; m == nil || len(m.Tools) != 2 { + t.Fatalf("the module is listed with tools %v, not its two", m) + } + + for address, want := range map[string]struct { + seat bool + key string + }{ + "mesh-delivery.deliveries": {true, "seat:mesh-delivery.deliveries"}, + "mesh-delivery.stalled": {true, "seat:mesh-delivery.stalled"}, + "anchor/mesh-delivery.delivery_status": {false, "mesh-delivery.delivery_status@anchor"}, + "anchor/mesh-delivery.deliveries": {false, "mesh-delivery.deliveries@anchor"}, + } { + got, err := resolve(x, address) + if err != nil { + t.Errorf("%s: %v", address, err) + continue + } + if got.Seat != want.seat || got.Key != want.key { + t.Errorf("%s resolves to %+v, not the %s", address, got, want.key) + } + } + // A module tool of the name without a machine: the module keeps state on its machine, so it says so. + if _, err := resolve(x, "mesh-delivery.delivery_status"); err == nil || !strings.Contains(err.Error(), "/") { + t.Errorf("the module's tool without a machine: %v", err) + } +}