refactor(routing): remove legacy bindings config

This commit is contained in:
Hoshina 2026-04-01 21:34:39 +08:00
parent 3a9d1fc6fd
commit 19a01d4264
7 changed files with 44 additions and 477 deletions

View file

@ -755,12 +755,12 @@ func TestAppendEventContextFields_IncludesInboundRouteAndScope(t *testing.T) {
SenderID: "U123", SenderID: "U123",
Mentioned: true, Mentioned: true,
}, },
Route: &routing.ResolvedRoute{ Route: &routing.ResolvedRoute{
AgentID: "support", AgentID: "support",
Channel: "slack", Channel: "slack",
AccountID: "workspace-a", AccountID: "workspace-a",
MatchedBy: "binding.team", MatchedBy: "default",
SessionPolicy: routing.SessionPolicy{ SessionPolicy: routing.SessionPolicy{
Dimensions: []string{"chat", "sender"}, Dimensions: []string{"chat", "sender"},
IdentityLinks: map[string][]string{ IdentityLinks: map[string][]string{
"canonical-user": {"slack:U123"}, "canonical-user": {"slack:U123"},
@ -786,8 +786,8 @@ func TestAppendEventContextFields_IncludesInboundRouteAndScope(t *testing.T) {
if fields["inbound_topic_id"] != "thread-42" { if fields["inbound_topic_id"] != "thread-42" {
t.Fatalf("inbound_topic_id = %v, want thread-42", fields["inbound_topic_id"]) t.Fatalf("inbound_topic_id = %v, want thread-42", fields["inbound_topic_id"])
} }
if fields["route_matched_by"] != "binding.team" { if fields["route_matched_by"] != "default" {
t.Fatalf("route_matched_by = %v, want binding.team", fields["route_matched_by"]) t.Fatalf("route_matched_by = %v, want default", fields["route_matched_by"])
} }
if fields["route_dimensions"] != "chat,sender" { if fields["route_dimensions"] != "chat,sender" {
t.Fatalf("route_dimensions = %v, want chat,sender", fields["route_dimensions"]) 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() tmpDir := t.TempDir()
cfg := &config.Config{ cfg := &config.Config{
Agents: config.AgentsConfig{ Agents: config.AgentsConfig{
@ -819,16 +819,6 @@ func TestResolveMessageRoute_UsesInboundContextAccountAndSpace(t *testing.T) {
{ID: "work"}, {ID: "work"},
}, },
}, },
Bindings: []config.AgentBinding{
{
AgentID: "work",
Match: config.BindingMatch{
Channel: "slack",
AccountID: "*",
TeamID: "T001",
},
},
},
Session: config.SessionConfig{ Session: config.SessionConfig{
Dimensions: []string{"sender"}, Dimensions: []string{"sender"},
}, },
@ -852,11 +842,14 @@ func TestResolveMessageRoute_UsesInboundContextAccountAndSpace(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("resolveMessageRoute() error = %v", err) t.Fatalf("resolveMessageRoute() error = %v", err)
} }
if route.AgentID != "work" { if route.AgentID != "main" {
t.Fatalf("AgentID = %q, want work", route.AgentID) t.Fatalf("AgentID = %q, want main", route.AgentID)
} }
if route.MatchedBy != "binding.team" { if route.MatchedBy != "default" {
t.Fatalf("MatchedBy = %q, want binding.team", route.MatchedBy) t.Fatalf("MatchedBy = %q, want default", route.MatchedBy)
}
if route.AccountID != "workspace-a" {
t.Fatalf("AccountID = %q, want workspace-a", route.AccountID)
} }
} }

View file

@ -27,7 +27,6 @@ const CurrentVersion = 2
type Config struct { type Config struct {
Version int `json:"version" yaml:"-"` // Config schema version for migration Version int `json:"version" yaml:"-"` // Config schema version for migration
Agents AgentsConfig `json:"agents" yaml:"-"` Agents AgentsConfig `json:"agents" yaml:"-"`
Bindings []AgentBinding `json:"bindings,omitempty" yaml:"-"`
Session SessionConfig `json:"session,omitempty" yaml:"-"` Session SessionConfig `json:"session,omitempty" yaml:"-"`
Channels ChannelsConfig `json:"channels" yaml:"channels"` Channels ChannelsConfig `json:"channels" yaml:"channels"`
ModelList SecureModelList `json:"model_list" yaml:"model_list"` // New model-centric provider configuration 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"` 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 { type SessionConfig struct {
Dimensions []string `json:"dimensions,omitempty"` Dimensions []string `json:"dimensions,omitempty"`
IdentityLinks map[string][]string `json:"identity_links,omitempty"` IdentityLinks map[string][]string `json:"identity_links,omitempty"`

View file

@ -47,7 +47,6 @@ type agentsConfigV0 struct {
// It is unexported since it's only used internally for migration. // It is unexported since it's only used internally for migration.
type configV0 struct { type configV0 struct {
Agents agentsConfigV0 `json:"agents"` Agents agentsConfigV0 `json:"agents"`
Bindings []AgentBinding `json:"bindings,omitempty"`
Session SessionConfig `json:"session,omitempty"` Session SessionConfig `json:"session,omitempty"`
Channels channelsConfigV0 `json:"channels"` Channels channelsConfigV0 `json:"channels"`
Providers providersConfigV0 `json:"providers,omitempty"` Providers providersConfigV0 `json:"providers,omitempty"`
@ -701,7 +700,6 @@ func (c *configV0) Migrate() (*Config, error) {
cfg.Agents.Defaults.Routing = c.Agents.Defaults.Routing cfg.Agents.Defaults.Routing = c.Agents.Defaults.Routing
// Copy other top-level fields // Copy other top-level fields
cfg.Bindings = c.Bindings
cfg.Session = c.Session cfg.Session = c.Session
cfg.Channels = c.Channels.ToChannelsConfig() cfg.Channels = c.Channels.ToChannelsConfig()
cfg.Gateway = c.Gateway cfg.Gateway = c.Gateway

View file

@ -175,20 +175,9 @@ func TestAgentConfig_FullParse(t *testing.T) {
t.Errorf("support.Subagents = %+v", support.Subagents) t.Errorf("support.Subagents = %+v", support.Subagents)
} }
if len(cfg.Bindings) != 1 { if len(cfg.Session.Dimensions) != 1 || cfg.Session.Dimensions[0] != "sender" {
t.Fatalf("bindings len = %d, want 1", len(cfg.Bindings)) t.Errorf("Session.Dimensions = %v", cfg.Session.Dimensions)
} }
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.IdentityLinks) != 1 { if len(cfg.Session.IdentityLinks) != 1 {
t.Errorf("Session.IdentityLinks = %v", cfg.Session.IdentityLinks) 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 { if len(cfg.Agents.List) != 0 {
t.Errorf("agents.list should be empty for backward compat, got %d", len(cfg.Agents.List)) 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 // TestDefaultConfig_HeartbeatEnabled verifies heartbeat is enabled by default

View file

@ -35,7 +35,6 @@ func DefaultConfig() *Config {
SplitOnMarker: false, SplitOnMarker: false,
}, },
}, },
Bindings: []AgentBinding{},
Session: SessionConfig{ Session: SessionConfig{
Dimensions: []string{"chat"}, Dimensions: []string{"chat"},
}, },

View file

@ -13,21 +13,16 @@ type SessionPolicy struct {
IdentityLinks map[string][]string IdentityLinks map[string][]string
} }
type RoutePeer struct {
Kind string
ID string
}
// ResolvedRoute is the result of agent routing. // ResolvedRoute is the result of agent routing.
type ResolvedRoute struct { type ResolvedRoute struct {
AgentID string AgentID string
Channel string Channel string
AccountID string AccountID string
SessionPolicy SessionPolicy 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 { type RouteResolver struct {
cfg *config.Config cfg *config.Config
} }
@ -40,167 +35,17 @@ func NewRouteResolver(cfg *config.Config) *RouteResolver {
// ResolveRoute determines which agent handles the message from a normalized // ResolveRoute determines which agent handles the message from a normalized
// inbound context and returns the session policy that should be used to // inbound context and returns the session policy that should be used to
// allocate session state. // 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 { func (r *RouteResolver) ResolveRoute(inbound bus.InboundContext) ResolvedRoute {
channel := strings.ToLower(strings.TrimSpace(inbound.Channel)) channel := strings.ToLower(strings.TrimSpace(inbound.Channel))
accountID := NormalizeAccountID(inbound.Account) accountID := NormalizeAccountID(inbound.Account)
peer := routePeerFromContext(inbound)
sessionPolicy := r.sessionPolicy() return ResolvedRoute{
AgentID: r.pickAgentID(r.resolveDefaultAgentID()),
bindings := r.filterBindings(channel, accountID) Channel: channel,
AccountID: accountID,
choose := func(agentID string, matchedBy string) ResolvedRoute { SessionPolicy: r.sessionPolicy(),
resolvedAgentID := r.pickAgentID(agentID) MatchedBy: "default",
return ResolvedRoute{
AgentID: resolvedAgentID,
Channel: channel,
AccountID: accountID,
SessionPolicy: sessionPolicy,
MatchedBy: matchedBy,
}
} }
// 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 { func (r *RouteResolver) pickAgentID(agentID string) string {
@ -273,46 +118,6 @@ func normalizeSessionDimensions(dimensions []string) []string {
return normalized 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 { func cloneIdentityLinks(src map[string][]string) map[string][]string {
if len(src) == 0 { if len(src) == 0 {
return nil return nil
@ -325,7 +130,3 @@ func cloneIdentityLinks(src map[string][]string) map[string][]string {
} }
return cloned return cloned
} }
func normalizeChannel(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}

View file

@ -7,7 +7,7 @@ import (
"github.com/sipeed/picoclaw/pkg/config" "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{ return &config.Config{
Agents: config.AgentsConfig{ Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{ Defaults: config.AgentDefaults{
@ -16,7 +16,6 @@ func testConfig(agents []config.AgentConfig, bindings []config.AgentBinding) *co
}, },
List: agents, List: agents,
}, },
Bindings: bindings,
Session: config.SessionConfig{ Session: config.SessionConfig{
Dimensions: []string{"sender"}, Dimensions: []string{"sender"},
}, },
@ -24,7 +23,7 @@ func testConfig(agents []config.AgentConfig, bindings []config.AgentBinding) *co
} }
func TestResolveRoute_DefaultAgent_NoBindings(t *testing.T) { func TestResolveRoute_DefaultAgent_NoBindings(t *testing.T) {
cfg := testConfig(nil, nil) cfg := testConfig(nil)
r := NewRouteResolver(cfg) r := NewRouteResolver(cfg)
route := r.ResolveRoute(bus.InboundContext{ route := r.ResolveRoute(bus.InboundContext{
@ -47,209 +46,28 @@ func TestResolveRoute_DefaultAgent_NoBindings(t *testing.T) {
} }
} }
func TestResolveRoute_PeerBinding(t *testing.T) { func TestResolveRoute_UsesNormalizedInboundContextFields(t *testing.T) {
agents := []config.AgentConfig{ cfg := testConfig([]config.AgentConfig{{ID: "sales", Default: true}})
{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)
r := NewRouteResolver(cfg) r := NewRouteResolver(cfg)
route := r.ResolveRoute(bus.InboundContext{ route := r.ResolveRoute(bus.InboundContext{
Channel: "telegram", Channel: "Telegram",
Account: "Bot2",
ChatType: "direct", ChatType: "direct",
SenderID: "user123", SenderID: "user123",
}) })
if route.AgentID != "support" { if route.AgentID != "sales" {
t.Errorf("AgentID = %q, want 'support'", route.AgentID) t.Errorf("AgentID = %q, want 'sales'", route.AgentID)
} }
if route.MatchedBy != "binding.peer" { if route.Channel != "telegram" {
t.Errorf("MatchedBy = %q, want 'binding.peer'", route.MatchedBy) t.Errorf("Channel = %q, want 'telegram'", route.Channel)
} }
} if route.AccountID != "bot2" {
t.Errorf("AccountID = %q, want 'bot2'", route.AccountID)
func TestResolveRoute_GuildBinding(t *testing.T) {
agents := []config.AgentConfig{
{ID: "general", Default: true},
{ID: "gaming"},
} }
bindings := []config.AgentBinding{ if route.MatchedBy != "default" {
{ t.Errorf("MatchedBy = %q, want 'default'", route.MatchedBy)
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)
} }
} }
@ -257,16 +75,7 @@ func TestResolveRoute_InvalidAgentFallsToDefault(t *testing.T) {
agents := []config.AgentConfig{ agents := []config.AgentConfig{
{ID: "main", Default: true}, {ID: "main", Default: true},
} }
bindings := []config.AgentBinding{ cfg := testConfig(agents)
{
AgentID: "nonexistent",
Match: config.BindingMatch{
Channel: "telegram",
AccountID: "*",
},
},
}
cfg := testConfig(agents, bindings)
r := NewRouteResolver(cfg) r := NewRouteResolver(cfg)
route := r.ResolveRoute(bus.InboundContext{Channel: "telegram"}) route := r.ResolveRoute(bus.InboundContext{Channel: "telegram"})
@ -282,7 +91,7 @@ func TestResolveRoute_DefaultAgentSelection(t *testing.T) {
{ID: "beta", Default: true}, {ID: "beta", Default: true},
{ID: "gamma"}, {ID: "gamma"},
} }
cfg := testConfig(agents, nil) cfg := testConfig(agents)
r := NewRouteResolver(cfg) r := NewRouteResolver(cfg)
route := r.ResolveRoute(bus.InboundContext{Channel: "cli"}) route := r.ResolveRoute(bus.InboundContext{Channel: "cli"})
@ -297,7 +106,7 @@ func TestResolveRoute_NoDefaultUsesFirst(t *testing.T) {
{ID: "alpha"}, {ID: "alpha"},
{ID: "beta"}, {ID: "beta"},
} }
cfg := testConfig(agents, nil) cfg := testConfig(agents)
r := NewRouteResolver(cfg) r := NewRouteResolver(cfg)
route := r.ResolveRoute(bus.InboundContext{Channel: "cli"}) route := r.ResolveRoute(bus.InboundContext{Channel: "cli"})