mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
182 lines
4.9 KiB
Go
182 lines
4.9 KiB
Go
// PicoClaw - Ultra-lightweight personal AI agent
|
|
// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot
|
|
// License: MIT
|
|
//
|
|
// Copyright (c) 2026 PicoClaw contributors
|
|
|
|
package agent
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/config"
|
|
"github.com/sipeed/picoclaw/pkg/mcp"
|
|
)
|
|
|
|
func boolPtr(b bool) *bool { return &b }
|
|
|
|
func TestMCPRuntimeResetClearsState(t *testing.T) {
|
|
var rt mcpRuntime
|
|
manager := mcp.NewManager()
|
|
rt.setManager(manager)
|
|
rt.setInitErr(errors.New("stale init error"))
|
|
rt.initOnce.Do(func() {})
|
|
|
|
got := rt.reset()
|
|
if got != manager {
|
|
t.Fatalf("reset() manager = %p, want %p", got, manager)
|
|
}
|
|
if rt.hasManager() {
|
|
t.Fatal("expected manager to be cleared after reset")
|
|
}
|
|
if err := rt.getInitErr(); err != nil {
|
|
t.Fatalf("getInitErr() = %v, want nil", err)
|
|
}
|
|
|
|
reran := false
|
|
rt.initOnce.Do(func() { reran = true })
|
|
if !reran {
|
|
t.Fatal("expected initOnce to be reset")
|
|
}
|
|
}
|
|
|
|
func TestReloadProviderAndConfig_ResetsMCPRuntime(t *testing.T) {
|
|
al, cfg, _, _, cleanup := newTestAgentLoop(t)
|
|
defer cleanup()
|
|
defer al.Close()
|
|
|
|
manager := mcp.NewManager()
|
|
al.mcp.setManager(manager)
|
|
al.mcp.setInitErr(errors.New("stale init error"))
|
|
al.mcp.initOnce.Do(func() {})
|
|
|
|
if !al.mcp.hasManager() {
|
|
t.Fatal("expected MCP manager to exist before reload")
|
|
}
|
|
|
|
if err := al.ReloadProviderAndConfig(context.Background(), &mockProvider{}, cfg); err != nil {
|
|
t.Fatalf("ReloadProviderAndConfig() error = %v", err)
|
|
}
|
|
|
|
if al.mcp.hasManager() {
|
|
t.Fatal("expected MCP manager to be cleared when reloaded config has MCP disabled")
|
|
}
|
|
if err := al.mcp.getInitErr(); err != nil {
|
|
t.Fatalf("getInitErr() = %v, want nil", err)
|
|
}
|
|
|
|
reran := false
|
|
al.mcp.initOnce.Do(func() { reran = true })
|
|
if !reran {
|
|
t.Fatal("expected MCP initOnce to be reset after reload")
|
|
}
|
|
}
|
|
|
|
func TestServerIsDeferred(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
discoveryEnabled bool
|
|
serverDeferred *bool
|
|
want bool
|
|
}{
|
|
// --- global false always wins: per-server deferred is ignored ---
|
|
{
|
|
name: "global false: per-server deferred=true is ignored",
|
|
discoveryEnabled: false,
|
|
serverDeferred: boolPtr(true),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "global false: per-server deferred=false stays false",
|
|
discoveryEnabled: false,
|
|
serverDeferred: boolPtr(false),
|
|
want: false,
|
|
},
|
|
// --- global true: per-server override applies ---
|
|
{
|
|
name: "global true: per-server deferred=false opts out",
|
|
discoveryEnabled: true,
|
|
serverDeferred: boolPtr(false),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "global true: per-server deferred=true stays true",
|
|
discoveryEnabled: true,
|
|
serverDeferred: boolPtr(true),
|
|
want: true,
|
|
},
|
|
// --- no per-server override: fall back to global ---
|
|
{
|
|
name: "no per-server field, global discovery enabled",
|
|
discoveryEnabled: true,
|
|
serverDeferred: nil,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "no per-server field, global discovery disabled",
|
|
discoveryEnabled: false,
|
|
serverDeferred: nil,
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
serverCfg := config.MCPServerConfig{Deferred: tt.serverDeferred}
|
|
got := serverIsDeferred(tt.discoveryEnabled, serverCfg)
|
|
if got != tt.want {
|
|
t.Errorf("serverIsDeferred(discoveryEnabled=%v, deferred=%v) = %v, want %v",
|
|
tt.discoveryEnabled, tt.serverDeferred, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEnsureMCPInitialized_LoadFailureSetsInitErr(t *testing.T) {
|
|
al, cfg, _, _, cleanup := newTestAgentLoop(t)
|
|
defer cleanup()
|
|
defer al.Close()
|
|
|
|
cfg.Tools = config.ToolsConfig{
|
|
MCP: config.MCPConfig{
|
|
ToolConfig: config.ToolConfig{Enabled: true},
|
|
Servers: map[string]config.MCPServerConfig{
|
|
"broken": {
|
|
Enabled: true,
|
|
Command: "picoclaw-command-that-does-not-exist-for-mcp-tests",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := al.ensureMCPInitialized(context.Background())
|
|
if err == nil {
|
|
t.Fatal("ensureMCPInitialized() error = nil, want load failure")
|
|
}
|
|
if !strings.Contains(err.Error(), "failed to load MCP servers") {
|
|
t.Fatalf("ensureMCPInitialized() error = %q, want wrapped load failure", err.Error())
|
|
}
|
|
|
|
initErr := al.mcp.getInitErr()
|
|
if initErr == nil {
|
|
t.Fatal("getInitErr() = nil, want cached load failure")
|
|
}
|
|
if !strings.Contains(initErr.Error(), "failed to load MCP servers") {
|
|
t.Fatalf("getInitErr() = %q, want wrapped load failure", initErr.Error())
|
|
}
|
|
if al.mcp.getManager() != nil {
|
|
t.Fatal("expected MCP manager to remain nil after load failure")
|
|
}
|
|
|
|
err = al.ensureMCPInitialized(context.Background())
|
|
if err == nil {
|
|
t.Fatal("second ensureMCPInitialized() error = nil, want cached load failure")
|
|
}
|
|
if !strings.Contains(err.Error(), "failed to load MCP servers") {
|
|
t.Fatalf("second ensureMCPInitialized() error = %q, want wrapped load failure", err.Error())
|
|
}
|
|
}
|