From 19a01d426453a7bbad1b2e07b24a91959cb3c26f Mon Sep 17 00:00:00 2001 From: Hoshina Date: Wed, 1 Apr 2026 21:34:39 +0800 Subject: [PATCH] refactor(routing): remove legacy bindings config --- pkg/agent/loop_test.go | 39 +++---- pkg/config/config.go | 19 ---- pkg/config/config_old.go | 2 - pkg/config/config_test.go | 20 +--- pkg/config/defaults.go | 1 - pkg/routing/route.go | 215 ++---------------------------------- pkg/routing/route_test.go | 225 +++----------------------------------- 7 files changed, 44 insertions(+), 477 deletions(-) diff --git a/pkg/agent/loop_test.go b/pkg/agent/loop_test.go index 4aa356f88..f288f1f2b 100644 --- a/pkg/agent/loop_test.go +++ b/pkg/agent/loop_test.go @@ -755,12 +755,12 @@ func TestAppendEventContextFields_IncludesInboundRouteAndScope(t *testing.T) { SenderID: "U123", Mentioned: true, }, - Route: &routing.ResolvedRoute{ - AgentID: "support", - Channel: "slack", - AccountID: "workspace-a", - MatchedBy: "binding.team", - SessionPolicy: routing.SessionPolicy{ + Route: &routing.ResolvedRoute{ + AgentID: "support", + Channel: "slack", + AccountID: "workspace-a", + MatchedBy: "default", + SessionPolicy: routing.SessionPolicy{ Dimensions: []string{"chat", "sender"}, IdentityLinks: map[string][]string{ "canonical-user": {"slack:U123"}, @@ -786,8 +786,8 @@ func TestAppendEventContextFields_IncludesInboundRouteAndScope(t *testing.T) { if fields["inbound_topic_id"] != "thread-42" { t.Fatalf("inbound_topic_id = %v, want thread-42", fields["inbound_topic_id"]) } - if fields["route_matched_by"] != "binding.team" { - t.Fatalf("route_matched_by = %v, want binding.team", fields["route_matched_by"]) + if fields["route_matched_by"] != "default" { + t.Fatalf("route_matched_by = %v, want default", fields["route_matched_by"]) } if fields["route_dimensions"] != "chat,sender" { t.Fatalf("route_dimensions = %v, want chat,sender", fields["route_dimensions"]) @@ -806,7 +806,7 @@ func TestAppendEventContextFields_IncludesInboundRouteAndScope(t *testing.T) { } } -func TestResolveMessageRoute_UsesInboundContextAccountAndSpace(t *testing.T) { +func TestResolveMessageRoute_UsesInboundContextAccount(t *testing.T) { tmpDir := t.TempDir() cfg := &config.Config{ Agents: config.AgentsConfig{ @@ -819,16 +819,6 @@ func TestResolveMessageRoute_UsesInboundContextAccountAndSpace(t *testing.T) { {ID: "work"}, }, }, - Bindings: []config.AgentBinding{ - { - AgentID: "work", - Match: config.BindingMatch{ - Channel: "slack", - AccountID: "*", - TeamID: "T001", - }, - }, - }, Session: config.SessionConfig{ Dimensions: []string{"sender"}, }, @@ -852,11 +842,14 @@ func TestResolveMessageRoute_UsesInboundContextAccountAndSpace(t *testing.T) { if err != nil { t.Fatalf("resolveMessageRoute() error = %v", err) } - if route.AgentID != "work" { - t.Fatalf("AgentID = %q, want work", route.AgentID) + if route.AgentID != "main" { + t.Fatalf("AgentID = %q, want main", route.AgentID) } - if route.MatchedBy != "binding.team" { - t.Fatalf("MatchedBy = %q, want binding.team", route.MatchedBy) + if route.MatchedBy != "default" { + t.Fatalf("MatchedBy = %q, want default", route.MatchedBy) + } + if route.AccountID != "workspace-a" { + t.Fatalf("AccountID = %q, want workspace-a", route.AccountID) } } diff --git a/pkg/config/config.go b/pkg/config/config.go index 014c90045..739980912 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -27,7 +27,6 @@ const CurrentVersion = 2 type Config struct { Version int `json:"version" yaml:"-"` // Config schema version for migration Agents AgentsConfig `json:"agents" yaml:"-"` - Bindings []AgentBinding `json:"bindings,omitempty" yaml:"-"` Session SessionConfig `json:"session,omitempty" yaml:"-"` Channels ChannelsConfig `json:"channels" yaml:"channels"` ModelList SecureModelList `json:"model_list" yaml:"model_list"` // New model-centric provider configuration @@ -176,24 +175,6 @@ type SubagentsConfig struct { Model *AgentModelConfig `json:"model,omitempty"` } -type PeerMatch struct { - Kind string `json:"kind"` - ID string `json:"id"` -} - -type BindingMatch struct { - Channel string `json:"channel"` - AccountID string `json:"account_id,omitempty"` - Peer *PeerMatch `json:"peer,omitempty"` - GuildID string `json:"guild_id,omitempty"` - TeamID string `json:"team_id,omitempty"` -} - -type AgentBinding struct { - AgentID string `json:"agent_id"` - Match BindingMatch `json:"match"` -} - type SessionConfig struct { Dimensions []string `json:"dimensions,omitempty"` IdentityLinks map[string][]string `json:"identity_links,omitempty"` diff --git a/pkg/config/config_old.go b/pkg/config/config_old.go index 150275aac..0b10fbf0b 100644 --- a/pkg/config/config_old.go +++ b/pkg/config/config_old.go @@ -47,7 +47,6 @@ type agentsConfigV0 struct { // It is unexported since it's only used internally for migration. type configV0 struct { Agents agentsConfigV0 `json:"agents"` - Bindings []AgentBinding `json:"bindings,omitempty"` Session SessionConfig `json:"session,omitempty"` Channels channelsConfigV0 `json:"channels"` Providers providersConfigV0 `json:"providers,omitempty"` @@ -701,7 +700,6 @@ func (c *configV0) Migrate() (*Config, error) { cfg.Agents.Defaults.Routing = c.Agents.Defaults.Routing // Copy other top-level fields - cfg.Bindings = c.Bindings cfg.Session = c.Session cfg.Channels = c.Channels.ToChannelsConfig() cfg.Gateway = c.Gateway diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index e8ebf1cfe..58c1461f5 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -175,20 +175,9 @@ func TestAgentConfig_FullParse(t *testing.T) { t.Errorf("support.Subagents = %+v", support.Subagents) } - if len(cfg.Bindings) != 1 { - t.Fatalf("bindings len = %d, want 1", len(cfg.Bindings)) - } - binding := cfg.Bindings[0] - if binding.AgentID != "support" || binding.Match.Channel != "telegram" { - t.Errorf("binding = %+v", binding) - } - if binding.Match.Peer == nil || binding.Match.Peer.Kind != "direct" || binding.Match.Peer.ID != "user123" { - t.Errorf("binding.Match.Peer = %+v", binding.Match.Peer) - } - - if len(cfg.Session.Dimensions) != 1 || cfg.Session.Dimensions[0] != "sender" { - t.Errorf("Session.Dimensions = %v", cfg.Session.Dimensions) - } + if len(cfg.Session.Dimensions) != 1 || cfg.Session.Dimensions[0] != "sender" { + t.Errorf("Session.Dimensions = %v", cfg.Session.Dimensions) + } if len(cfg.Session.IdentityLinks) != 1 { t.Errorf("Session.IdentityLinks = %v", cfg.Session.IdentityLinks) } @@ -218,9 +207,6 @@ func TestConfig_BackwardCompat_NoAgentsList(t *testing.T) { if len(cfg.Agents.List) != 0 { t.Errorf("agents.list should be empty for backward compat, got %d", len(cfg.Agents.List)) } - if len(cfg.Bindings) != 0 { - t.Errorf("bindings should be empty, got %d", len(cfg.Bindings)) - } } // TestDefaultConfig_HeartbeatEnabled verifies heartbeat is enabled by default diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index 58cd05088..9165045d4 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -35,7 +35,6 @@ func DefaultConfig() *Config { SplitOnMarker: false, }, }, - Bindings: []AgentBinding{}, Session: SessionConfig{ Dimensions: []string{"chat"}, }, diff --git a/pkg/routing/route.go b/pkg/routing/route.go index 88a0006da..6300460f8 100644 --- a/pkg/routing/route.go +++ b/pkg/routing/route.go @@ -13,21 +13,16 @@ type SessionPolicy struct { IdentityLinks map[string][]string } -type RoutePeer struct { - Kind string - ID string -} - // ResolvedRoute is the result of agent routing. type ResolvedRoute struct { AgentID string Channel string AccountID string SessionPolicy SessionPolicy - MatchedBy string // "binding.peer", "binding.peer.parent", "binding.guild", "binding.team", "binding.account", "binding.channel", "default" + MatchedBy string // currently always "default" until the new binding system lands } -// RouteResolver determines which agent handles a message based on config bindings. +// RouteResolver determines which agent handles a message. type RouteResolver struct { cfg *config.Config } @@ -40,167 +35,17 @@ func NewRouteResolver(cfg *config.Config) *RouteResolver { // ResolveRoute determines which agent handles the message from a normalized // inbound context and returns the session policy that should be used to // allocate session state. -// Implements the 7-level priority cascade: -// peer > parent_peer > guild > team > account > channel_wildcard > default func (r *RouteResolver) ResolveRoute(inbound bus.InboundContext) ResolvedRoute { channel := strings.ToLower(strings.TrimSpace(inbound.Channel)) accountID := NormalizeAccountID(inbound.Account) - peer := routePeerFromContext(inbound) - sessionPolicy := r.sessionPolicy() - - bindings := r.filterBindings(channel, accountID) - - choose := func(agentID string, matchedBy string) ResolvedRoute { - resolvedAgentID := r.pickAgentID(agentID) - return ResolvedRoute{ - AgentID: resolvedAgentID, - Channel: channel, - AccountID: accountID, - SessionPolicy: sessionPolicy, - MatchedBy: matchedBy, - } + return ResolvedRoute{ + AgentID: r.pickAgentID(r.resolveDefaultAgentID()), + Channel: channel, + AccountID: accountID, + SessionPolicy: r.sessionPolicy(), + MatchedBy: "default", } - - // Priority 1: Peer binding - if peer != nil && strings.TrimSpace(peer.ID) != "" { - if match := r.findPeerMatch(bindings, peer); match != nil { - return choose(match.AgentID, "binding.peer") - } - } - - // Priority 2: Parent peer binding - parentPeer := parentPeerFromContext(inbound) - if parentPeer != nil && strings.TrimSpace(parentPeer.ID) != "" { - if match := r.findPeerMatch(bindings, parentPeer); match != nil { - return choose(match.AgentID, "binding.peer.parent") - } - } - - // Priority 3: Guild binding - guildID := routeGuildIDFromContext(inbound) - if guildID != "" { - if match := r.findGuildMatch(bindings, guildID); match != nil { - return choose(match.AgentID, "binding.guild") - } - } - - // Priority 4: Team binding - teamID := routeTeamIDFromContext(inbound) - if teamID != "" { - if match := r.findTeamMatch(bindings, teamID); match != nil { - return choose(match.AgentID, "binding.team") - } - } - - // Priority 5: Account binding - if match := r.findAccountMatch(bindings); match != nil { - return choose(match.AgentID, "binding.account") - } - - // Priority 6: Channel wildcard binding - if match := r.findChannelWildcardMatch(bindings); match != nil { - return choose(match.AgentID, "binding.channel") - } - - // Priority 7: Default agent - return choose(r.resolveDefaultAgentID(), "default") -} - -func (r *RouteResolver) filterBindings(channel, accountID string) []config.AgentBinding { - var filtered []config.AgentBinding - for _, b := range r.cfg.Bindings { - matchChannel := strings.ToLower(strings.TrimSpace(b.Match.Channel)) - if matchChannel == "" || matchChannel != channel { - continue - } - if !matchesAccountID(b.Match.AccountID, accountID) { - continue - } - filtered = append(filtered, b) - } - return filtered -} - -func matchesAccountID(matchAccountID, actual string) bool { - trimmed := strings.TrimSpace(matchAccountID) - if trimmed == "" { - return actual == DefaultAccountID - } - if trimmed == "*" { - return true - } - return strings.ToLower(trimmed) == strings.ToLower(actual) -} - -func (r *RouteResolver) findPeerMatch(bindings []config.AgentBinding, peer *RoutePeer) *config.AgentBinding { - for i := range bindings { - b := &bindings[i] - if b.Match.Peer == nil { - continue - } - peerKind := strings.ToLower(strings.TrimSpace(b.Match.Peer.Kind)) - peerID := strings.TrimSpace(b.Match.Peer.ID) - if peerKind == "" || peerID == "" { - continue - } - if peerKind == strings.ToLower(peer.Kind) && peerID == peer.ID { - return b - } - } - return nil -} - -func (r *RouteResolver) findGuildMatch(bindings []config.AgentBinding, guildID string) *config.AgentBinding { - for i := range bindings { - b := &bindings[i] - matchGuild := strings.TrimSpace(b.Match.GuildID) - if matchGuild != "" && matchGuild == guildID { - return &bindings[i] - } - } - return nil -} - -func (r *RouteResolver) findTeamMatch(bindings []config.AgentBinding, teamID string) *config.AgentBinding { - for i := range bindings { - b := &bindings[i] - matchTeam := strings.TrimSpace(b.Match.TeamID) - if matchTeam != "" && matchTeam == teamID { - return &bindings[i] - } - } - return nil -} - -func (r *RouteResolver) findAccountMatch(bindings []config.AgentBinding) *config.AgentBinding { - for i := range bindings { - b := &bindings[i] - accountID := strings.TrimSpace(b.Match.AccountID) - if accountID == "*" { - continue - } - if b.Match.Peer != nil || b.Match.GuildID != "" || b.Match.TeamID != "" { - continue - } - return &bindings[i] - } - return nil -} - -func (r *RouteResolver) findChannelWildcardMatch(bindings []config.AgentBinding) *config.AgentBinding { - for i := range bindings { - b := &bindings[i] - accountID := strings.TrimSpace(b.Match.AccountID) - if accountID != "*" { - continue - } - if b.Match.Peer != nil || b.Match.GuildID != "" || b.Match.TeamID != "" { - continue - } - return &bindings[i] - } - return nil } func (r *RouteResolver) pickAgentID(agentID string) string { @@ -273,46 +118,6 @@ func normalizeSessionDimensions(dimensions []string) []string { return normalized } -func routePeerFromContext(ctx bus.InboundContext) *RoutePeer { - peerKind := normalizeChannel(strings.TrimSpace(ctx.ChatType)) - if peerKind == "" || peerKind == "unknown" { - return nil - } - - peerID := strings.TrimSpace(ctx.ChatID) - if peerKind == "direct" && peerID == "" { - peerID = strings.TrimSpace(ctx.SenderID) - } - if peerID == "" { - return nil - } - - return &RoutePeer{Kind: peerKind, ID: peerID} -} - -func parentPeerFromContext(ctx bus.InboundContext) *RoutePeer { - if topicID := strings.TrimSpace(ctx.TopicID); topicID != "" { - return &RoutePeer{Kind: "topic", ID: topicID} - } - return nil -} - -func routeGuildIDFromContext(ctx bus.InboundContext) string { - if strings.EqualFold(strings.TrimSpace(ctx.SpaceType), "guild") { - return strings.TrimSpace(ctx.SpaceID) - } - return "" -} - -func routeTeamIDFromContext(ctx bus.InboundContext) string { - switch strings.ToLower(strings.TrimSpace(ctx.SpaceType)) { - case "team", "workspace": - return strings.TrimSpace(ctx.SpaceID) - default: - return "" - } -} - func cloneIdentityLinks(src map[string][]string) map[string][]string { if len(src) == 0 { return nil @@ -325,7 +130,3 @@ func cloneIdentityLinks(src map[string][]string) map[string][]string { } return cloned } - -func normalizeChannel(value string) string { - return strings.ToLower(strings.TrimSpace(value)) -} diff --git a/pkg/routing/route_test.go b/pkg/routing/route_test.go index 46a0f9f13..b4e3d6406 100644 --- a/pkg/routing/route_test.go +++ b/pkg/routing/route_test.go @@ -7,7 +7,7 @@ import ( "github.com/sipeed/picoclaw/pkg/config" ) -func testConfig(agents []config.AgentConfig, bindings []config.AgentBinding) *config.Config { +func testConfig(agents []config.AgentConfig) *config.Config { return &config.Config{ Agents: config.AgentsConfig{ Defaults: config.AgentDefaults{ @@ -16,7 +16,6 @@ func testConfig(agents []config.AgentConfig, bindings []config.AgentBinding) *co }, List: agents, }, - Bindings: bindings, Session: config.SessionConfig{ Dimensions: []string{"sender"}, }, @@ -24,7 +23,7 @@ func testConfig(agents []config.AgentConfig, bindings []config.AgentBinding) *co } func TestResolveRoute_DefaultAgent_NoBindings(t *testing.T) { - cfg := testConfig(nil, nil) + cfg := testConfig(nil) r := NewRouteResolver(cfg) route := r.ResolveRoute(bus.InboundContext{ @@ -47,209 +46,28 @@ func TestResolveRoute_DefaultAgent_NoBindings(t *testing.T) { } } -func TestResolveRoute_PeerBinding(t *testing.T) { - agents := []config.AgentConfig{ - {ID: "sales", Default: true}, - {ID: "support"}, - } - bindings := []config.AgentBinding{ - { - AgentID: "support", - Match: config.BindingMatch{ - Channel: "telegram", - AccountID: "*", - Peer: &config.PeerMatch{Kind: "direct", ID: "user123"}, - }, - }, - } - cfg := testConfig(agents, bindings) +func TestResolveRoute_UsesNormalizedInboundContextFields(t *testing.T) { + cfg := testConfig([]config.AgentConfig{{ID: "sales", Default: true}}) r := NewRouteResolver(cfg) route := r.ResolveRoute(bus.InboundContext{ - Channel: "telegram", + Channel: "Telegram", + Account: "Bot2", ChatType: "direct", SenderID: "user123", }) - if route.AgentID != "support" { - t.Errorf("AgentID = %q, want 'support'", route.AgentID) + if route.AgentID != "sales" { + t.Errorf("AgentID = %q, want 'sales'", route.AgentID) } - if route.MatchedBy != "binding.peer" { - t.Errorf("MatchedBy = %q, want 'binding.peer'", route.MatchedBy) + if route.Channel != "telegram" { + t.Errorf("Channel = %q, want 'telegram'", route.Channel) } -} - -func TestResolveRoute_GuildBinding(t *testing.T) { - agents := []config.AgentConfig{ - {ID: "general", Default: true}, - {ID: "gaming"}, + if route.AccountID != "bot2" { + t.Errorf("AccountID = %q, want 'bot2'", route.AccountID) } - bindings := []config.AgentBinding{ - { - AgentID: "gaming", - Match: config.BindingMatch{ - Channel: "discord", - AccountID: "*", - GuildID: "guild-abc", - }, - }, - } - cfg := testConfig(agents, bindings) - r := NewRouteResolver(cfg) - - route := r.ResolveRoute(bus.InboundContext{ - Channel: "discord", - ChatID: "ch1", - ChatType: "channel", - SpaceID: "guild-abc", - SpaceType: "guild", - }) - - if route.AgentID != "gaming" { - t.Errorf("AgentID = %q, want 'gaming'", route.AgentID) - } - if route.MatchedBy != "binding.guild" { - t.Errorf("MatchedBy = %q, want 'binding.guild'", route.MatchedBy) - } -} - -func TestResolveRoute_TeamBinding(t *testing.T) { - agents := []config.AgentConfig{ - {ID: "general", Default: true}, - {ID: "work"}, - } - bindings := []config.AgentBinding{ - { - AgentID: "work", - Match: config.BindingMatch{ - Channel: "slack", - AccountID: "*", - TeamID: "T12345", - }, - }, - } - cfg := testConfig(agents, bindings) - r := NewRouteResolver(cfg) - - route := r.ResolveRoute(bus.InboundContext{ - Channel: "slack", - ChatID: "C001", - ChatType: "channel", - SpaceID: "T12345", - SpaceType: "team", - }) - - if route.AgentID != "work" { - t.Errorf("AgentID = %q, want 'work'", route.AgentID) - } - if route.MatchedBy != "binding.team" { - t.Errorf("MatchedBy = %q, want 'binding.team'", route.MatchedBy) - } -} - -func TestResolveRoute_AccountBinding(t *testing.T) { - agents := []config.AgentConfig{ - {ID: "default-agent", Default: true}, - {ID: "premium"}, - } - bindings := []config.AgentBinding{ - { - AgentID: "premium", - Match: config.BindingMatch{ - Channel: "telegram", - AccountID: "bot2", - }, - }, - } - cfg := testConfig(agents, bindings) - r := NewRouteResolver(cfg) - - route := r.ResolveRoute(bus.InboundContext{ - Channel: "telegram", - Account: "bot2", - ChatType: "direct", - SenderID: "user1", - }) - - if route.AgentID != "premium" { - t.Errorf("AgentID = %q, want 'premium'", route.AgentID) - } - if route.MatchedBy != "binding.account" { - t.Errorf("MatchedBy = %q, want 'binding.account'", route.MatchedBy) - } -} - -func TestResolveRoute_ChannelWildcard(t *testing.T) { - agents := []config.AgentConfig{ - {ID: "main", Default: true}, - {ID: "telegram-bot"}, - } - bindings := []config.AgentBinding{ - { - AgentID: "telegram-bot", - Match: config.BindingMatch{ - Channel: "telegram", - AccountID: "*", - }, - }, - } - cfg := testConfig(agents, bindings) - r := NewRouteResolver(cfg) - - route := r.ResolveRoute(bus.InboundContext{ - Channel: "telegram", - ChatType: "direct", - SenderID: "user1", - }) - - if route.AgentID != "telegram-bot" { - t.Errorf("AgentID = %q, want 'telegram-bot'", route.AgentID) - } - if route.MatchedBy != "binding.channel" { - t.Errorf("MatchedBy = %q, want 'binding.channel'", route.MatchedBy) - } -} - -func TestResolveRoute_PriorityOrder_PeerBeatsGuild(t *testing.T) { - agents := []config.AgentConfig{ - {ID: "general", Default: true}, - {ID: "vip"}, - {ID: "gaming"}, - } - bindings := []config.AgentBinding{ - { - AgentID: "vip", - Match: config.BindingMatch{ - Channel: "discord", - AccountID: "*", - Peer: &config.PeerMatch{Kind: "direct", ID: "user-vip"}, - }, - }, - { - AgentID: "gaming", - Match: config.BindingMatch{ - Channel: "discord", - AccountID: "*", - GuildID: "guild-1", - }, - }, - } - cfg := testConfig(agents, bindings) - r := NewRouteResolver(cfg) - - route := r.ResolveRoute(bus.InboundContext{ - Channel: "discord", - ChatType: "direct", - SenderID: "user-vip", - SpaceID: "guild-1", - SpaceType: "guild", - }) - - if route.AgentID != "vip" { - t.Errorf("AgentID = %q, want 'vip' (peer should beat guild)", route.AgentID) - } - if route.MatchedBy != "binding.peer" { - t.Errorf("MatchedBy = %q, want 'binding.peer'", route.MatchedBy) + if route.MatchedBy != "default" { + t.Errorf("MatchedBy = %q, want 'default'", route.MatchedBy) } } @@ -257,16 +75,7 @@ func TestResolveRoute_InvalidAgentFallsToDefault(t *testing.T) { agents := []config.AgentConfig{ {ID: "main", Default: true}, } - bindings := []config.AgentBinding{ - { - AgentID: "nonexistent", - Match: config.BindingMatch{ - Channel: "telegram", - AccountID: "*", - }, - }, - } - cfg := testConfig(agents, bindings) + cfg := testConfig(agents) r := NewRouteResolver(cfg) route := r.ResolveRoute(bus.InboundContext{Channel: "telegram"}) @@ -282,7 +91,7 @@ func TestResolveRoute_DefaultAgentSelection(t *testing.T) { {ID: "beta", Default: true}, {ID: "gamma"}, } - cfg := testConfig(agents, nil) + cfg := testConfig(agents) r := NewRouteResolver(cfg) route := r.ResolveRoute(bus.InboundContext{Channel: "cli"}) @@ -297,7 +106,7 @@ func TestResolveRoute_NoDefaultUsesFirst(t *testing.T) { {ID: "alpha"}, {ID: "beta"}, } - cfg := testConfig(agents, nil) + cfg := testConfig(agents) r := NewRouteResolver(cfg) route := r.ResolveRoute(bus.InboundContext{Channel: "cli"})