Files
picoclaw/pkg/config/config_test.go
T
Leandro Barbosa 272536a11a feat: add multi-agent routing with declarative bindings
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.
2026-02-13 12:12:33 -03:00

187 lines
4.7 KiB
Go

package config
import (
"encoding/json"
"testing"
)
func TestAgentModelConfig_UnmarshalString(t *testing.T) {
var m AgentModelConfig
if err := json.Unmarshal([]byte(`"gpt-4"`), &m); err != nil {
t.Fatalf("unmarshal string: %v", err)
}
if m.Primary != "gpt-4" {
t.Errorf("Primary = %q, want 'gpt-4'", m.Primary)
}
if m.Fallbacks != nil {
t.Errorf("Fallbacks = %v, want nil", m.Fallbacks)
}
}
func TestAgentModelConfig_UnmarshalObject(t *testing.T) {
var m AgentModelConfig
data := `{"primary": "claude-opus", "fallbacks": ["gpt-4o-mini", "haiku"]}`
if err := json.Unmarshal([]byte(data), &m); err != nil {
t.Fatalf("unmarshal object: %v", err)
}
if m.Primary != "claude-opus" {
t.Errorf("Primary = %q, want 'claude-opus'", m.Primary)
}
if len(m.Fallbacks) != 2 {
t.Fatalf("Fallbacks len = %d, want 2", len(m.Fallbacks))
}
if m.Fallbacks[0] != "gpt-4o-mini" || m.Fallbacks[1] != "haiku" {
t.Errorf("Fallbacks = %v", m.Fallbacks)
}
}
func TestAgentModelConfig_MarshalString(t *testing.T) {
m := AgentModelConfig{Primary: "gpt-4"}
data, err := json.Marshal(m)
if err != nil {
t.Fatalf("marshal: %v", err)
}
if string(data) != `"gpt-4"` {
t.Errorf("marshal = %s, want '\"gpt-4\"'", string(data))
}
}
func TestAgentModelConfig_MarshalObject(t *testing.T) {
m := AgentModelConfig{Primary: "claude-opus", Fallbacks: []string{"haiku"}}
data, err := json.Marshal(m)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var result map[string]interface{}
json.Unmarshal(data, &result)
if result["primary"] != "claude-opus" {
t.Errorf("primary = %v", result["primary"])
}
}
func TestAgentConfig_FullParse(t *testing.T) {
jsonData := `{
"agents": {
"defaults": {
"workspace": "~/.picoclaw/workspace",
"model": "glm-4.7",
"max_tokens": 8192,
"max_tool_iterations": 20
},
"list": [
{
"id": "sales",
"default": true,
"name": "Sales Bot",
"model": "gpt-4"
},
{
"id": "support",
"name": "Support Bot",
"model": {
"primary": "claude-opus",
"fallbacks": ["haiku"]
},
"subagents": {
"allow_agents": ["sales"]
}
}
]
},
"bindings": [
{
"agent_id": "support",
"match": {
"channel": "telegram",
"account_id": "*",
"peer": {"kind": "direct", "id": "user123"}
}
}
],
"session": {
"dm_scope": "per-peer",
"identity_links": {
"john": ["telegram:123", "discord:john#1234"]
}
}
}`
cfg := DefaultConfig()
if err := json.Unmarshal([]byte(jsonData), cfg); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if len(cfg.Agents.List) != 2 {
t.Fatalf("agents.list len = %d, want 2", len(cfg.Agents.List))
}
sales := cfg.Agents.List[0]
if sales.ID != "sales" || !sales.Default || sales.Name != "Sales Bot" {
t.Errorf("sales = %+v", sales)
}
if sales.Model == nil || sales.Model.Primary != "gpt-4" {
t.Errorf("sales.Model = %+v", sales.Model)
}
support := cfg.Agents.List[1]
if support.ID != "support" || support.Name != "Support Bot" {
t.Errorf("support = %+v", support)
}
if support.Model == nil || support.Model.Primary != "claude-opus" {
t.Errorf("support.Model = %+v", support.Model)
}
if len(support.Model.Fallbacks) != 1 || support.Model.Fallbacks[0] != "haiku" {
t.Errorf("support.Model.Fallbacks = %v", support.Model.Fallbacks)
}
if support.Subagents == nil || len(support.Subagents.AllowAgents) != 1 {
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 cfg.Session.DMScope != "per-peer" {
t.Errorf("Session.DMScope = %q", cfg.Session.DMScope)
}
if len(cfg.Session.IdentityLinks) != 1 {
t.Errorf("Session.IdentityLinks = %v", cfg.Session.IdentityLinks)
}
links := cfg.Session.IdentityLinks["john"]
if len(links) != 2 {
t.Errorf("john links = %v", links)
}
}
func TestConfig_BackwardCompat_NoAgentsList(t *testing.T) {
jsonData := `{
"agents": {
"defaults": {
"workspace": "~/.picoclaw/workspace",
"model": "glm-4.7",
"max_tokens": 8192,
"max_tool_iterations": 20
}
}
}`
cfg := DefaultConfig()
if err := json.Unmarshal([]byte(jsonData), cfg); err != nil {
t.Fatalf("unmarshal: %v", err)
}
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))
}
}