mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
272536a11a
Implement per-agent workspace/model/session isolation with 7-level priority routing cascade (peer > parent_peer > guild > team > account > channel > default). Backward compatible - empty agents.list creates implicit "main" agent from defaults. Core components: - routing/agent_id.go: ID normalization with pre-compiled regex - routing/session_key.go: 4 DM scope modes with identity links - routing/route.go: RouteResolver with priority-based binding matcher - agent/instance.go: Per-agent state (workspace, sessions, tools, model) - agent/registry.go: Agent lifecycle, route resolution, subagent ACL Integration: - config.go: AgentModelConfig (flexible JSON), bindings, session config - loop.go: Complete rewrite for multi-agent dispatch - Channel adapters: peer_kind/peer_id metadata (telegram, discord, slack) - spawn.go: Subagent allowlist enforcement per agent Validated end-to-end with Discord channel-based bindings, default fallback routing, and per-agent session persistence.
163 lines
4.3 KiB
Go
163 lines
4.3 KiB
Go
package routing
|
|
|
|
import "testing"
|
|
|
|
func TestBuildAgentMainSessionKey(t *testing.T) {
|
|
got := BuildAgentMainSessionKey("sales")
|
|
want := "agent:sales:main"
|
|
if got != want {
|
|
t.Errorf("BuildAgentMainSessionKey('sales') = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildAgentMainSessionKey_Normalizes(t *testing.T) {
|
|
got := BuildAgentMainSessionKey("Sales Bot")
|
|
want := "agent:sales-bot:main"
|
|
if got != want {
|
|
t.Errorf("BuildAgentMainSessionKey('Sales Bot') = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildAgentPeerSessionKey_DMScopeMain(t *testing.T) {
|
|
got := BuildAgentPeerSessionKey(SessionKeyParams{
|
|
AgentID: "main",
|
|
Channel: "telegram",
|
|
Peer: &RoutePeer{Kind: "direct", ID: "user123"},
|
|
DMScope: DMScopeMain,
|
|
})
|
|
want := "agent:main:main"
|
|
if got != want {
|
|
t.Errorf("DMScopeMain = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildAgentPeerSessionKey_DMScopePerPeer(t *testing.T) {
|
|
got := BuildAgentPeerSessionKey(SessionKeyParams{
|
|
AgentID: "main",
|
|
Channel: "telegram",
|
|
Peer: &RoutePeer{Kind: "direct", ID: "user123"},
|
|
DMScope: DMScopePerPeer,
|
|
})
|
|
want := "agent:main:direct:user123"
|
|
if got != want {
|
|
t.Errorf("DMScopePerPeer = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildAgentPeerSessionKey_DMScopePerChannelPeer(t *testing.T) {
|
|
got := BuildAgentPeerSessionKey(SessionKeyParams{
|
|
AgentID: "main",
|
|
Channel: "telegram",
|
|
Peer: &RoutePeer{Kind: "direct", ID: "user123"},
|
|
DMScope: DMScopePerChannelPeer,
|
|
})
|
|
want := "agent:main:telegram:direct:user123"
|
|
if got != want {
|
|
t.Errorf("DMScopePerChannelPeer = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildAgentPeerSessionKey_DMScopePerAccountChannelPeer(t *testing.T) {
|
|
got := BuildAgentPeerSessionKey(SessionKeyParams{
|
|
AgentID: "main",
|
|
Channel: "telegram",
|
|
AccountID: "bot1",
|
|
Peer: &RoutePeer{Kind: "direct", ID: "User123"},
|
|
DMScope: DMScopePerAccountChannelPeer,
|
|
})
|
|
want := "agent:main:telegram:bot1:direct:user123"
|
|
if got != want {
|
|
t.Errorf("DMScopePerAccountChannelPeer = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildAgentPeerSessionKey_GroupPeer(t *testing.T) {
|
|
got := BuildAgentPeerSessionKey(SessionKeyParams{
|
|
AgentID: "main",
|
|
Channel: "telegram",
|
|
Peer: &RoutePeer{Kind: "group", ID: "chat456"},
|
|
DMScope: DMScopePerPeer,
|
|
})
|
|
want := "agent:main:telegram:group:chat456"
|
|
if got != want {
|
|
t.Errorf("GroupPeer = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildAgentPeerSessionKey_NilPeer(t *testing.T) {
|
|
got := BuildAgentPeerSessionKey(SessionKeyParams{
|
|
AgentID: "main",
|
|
Channel: "telegram",
|
|
Peer: nil,
|
|
DMScope: DMScopePerPeer,
|
|
})
|
|
// nil peer defaults to direct with empty ID, falls to main
|
|
want := "agent:main:main"
|
|
if got != want {
|
|
t.Errorf("NilPeer = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildAgentPeerSessionKey_IdentityLink(t *testing.T) {
|
|
links := map[string][]string{
|
|
"john": {"telegram:user123", "discord:john#1234"},
|
|
}
|
|
got := BuildAgentPeerSessionKey(SessionKeyParams{
|
|
AgentID: "main",
|
|
Channel: "telegram",
|
|
Peer: &RoutePeer{Kind: "direct", ID: "user123"},
|
|
DMScope: DMScopePerPeer,
|
|
IdentityLinks: links,
|
|
})
|
|
want := "agent:main:direct:john"
|
|
if got != want {
|
|
t.Errorf("IdentityLink = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestParseAgentSessionKey_Valid(t *testing.T) {
|
|
parsed := ParseAgentSessionKey("agent:sales:telegram:direct:user123")
|
|
if parsed == nil {
|
|
t.Fatal("expected non-nil result")
|
|
}
|
|
if parsed.AgentID != "sales" {
|
|
t.Errorf("AgentID = %q, want 'sales'", parsed.AgentID)
|
|
}
|
|
if parsed.Rest != "telegram:direct:user123" {
|
|
t.Errorf("Rest = %q, want 'telegram:direct:user123'", parsed.Rest)
|
|
}
|
|
}
|
|
|
|
func TestParseAgentSessionKey_Invalid(t *testing.T) {
|
|
tests := []string{
|
|
"",
|
|
"foo:bar",
|
|
"notprefix:sales:main",
|
|
"agent::main",
|
|
"agent:sales:",
|
|
}
|
|
for _, input := range tests {
|
|
if got := ParseAgentSessionKey(input); got != nil {
|
|
t.Errorf("ParseAgentSessionKey(%q) = %+v, want nil", input, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIsSubagentSessionKey(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
want bool
|
|
}{
|
|
{"subagent:task-1", true},
|
|
{"agent:main:subagent:task-1", true},
|
|
{"agent:main:main", false},
|
|
{"agent:main:telegram:direct:user123", false},
|
|
{"", false},
|
|
}
|
|
for _, tt := range tests {
|
|
if got := IsSubagentSessionKey(tt.input); got != tt.want {
|
|
t.Errorf("IsSubagentSessionKey(%q) = %v, want %v", tt.input, got, tt.want)
|
|
}
|
|
}
|
|
}
|