mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
refactor(routing): remove legacy bindings config
This commit is contained in:
+16
-23
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -35,7 +35,6 @@ func DefaultConfig() *Config {
|
||||
SplitOnMarker: false,
|
||||
},
|
||||
},
|
||||
Bindings: []AgentBinding{},
|
||||
Session: SessionConfig{
|
||||
Dimensions: []string{"chat"},
|
||||
},
|
||||
|
||||
+8
-207
@@ -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))
|
||||
}
|
||||
|
||||
+17
-208
@@ -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"})
|
||||
|
||||
Reference in New Issue
Block a user