mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
test(tools): add delegate tool unit tests
12 test cases covering: - success path with result attribution - agent_id validation (missing, empty, whitespace, wrong type) - task validation (missing, empty, whitespace) - permission denied / allowed via allowlist checker - self-delegation blocked - nil spawner, spawner error, nil result from spawner - open access when no allowlist checker is set Ref: #2148
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// delegateMockSpawner records the config and returns a canned result.
|
||||
type delegateMockSpawner struct {
|
||||
lastCfg SubTurnConfig
|
||||
result *ToolResult
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *delegateMockSpawner) SpawnSubTurn(_ context.Context, cfg SubTurnConfig) (*ToolResult, error) {
|
||||
m.lastCfg = cfg
|
||||
if m.err != nil {
|
||||
return nil, m.err
|
||||
}
|
||||
if m.result != nil {
|
||||
return m.result, nil
|
||||
}
|
||||
return &ToolResult{
|
||||
ForLLM: "completed: " + cfg.SystemPrompt,
|
||||
ForUser: "completed",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestDelegateTool_Name(t *testing.T) {
|
||||
tool := NewDelegateTool()
|
||||
if tool.Name() != "delegate" {
|
||||
t.Errorf("Name() = %q, want %q", tool.Name(), "delegate")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelegateTool_Parameters(t *testing.T) {
|
||||
tool := NewDelegateTool()
|
||||
params := tool.Parameters()
|
||||
|
||||
props, ok := params["properties"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("properties should be a map")
|
||||
}
|
||||
_, hasAgentID := props["agent_id"]
|
||||
if !hasAgentID {
|
||||
t.Error("agent_id parameter should exist")
|
||||
}
|
||||
_, hasTask := props["task"]
|
||||
if !hasTask {
|
||||
t.Error("task parameter should exist")
|
||||
}
|
||||
|
||||
required, ok := params["required"].([]string)
|
||||
if !ok {
|
||||
t.Fatal("required should be a string array")
|
||||
}
|
||||
if len(required) != 2 {
|
||||
t.Fatalf("required should have 2 entries, got %d", len(required))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelegateTool_Execute_Success(t *testing.T) {
|
||||
spawner := &delegateMockSpawner{}
|
||||
tool := NewDelegateTool()
|
||||
tool.SetSpawner(spawner)
|
||||
|
||||
result := tool.Execute(context.Background(), map[string]any{
|
||||
"agent_id": "researcher",
|
||||
"task": "summarize the logs",
|
||||
})
|
||||
|
||||
if result.IsError {
|
||||
t.Fatalf("expected success, got error: %s", result.ForLLM)
|
||||
}
|
||||
if !strings.Contains(result.ForLLM, `[Response from agent "researcher"]`) {
|
||||
t.Errorf("result should contain attribution, got: %s", result.ForLLM)
|
||||
}
|
||||
if !strings.Contains(result.ForLLM, "summarize the logs") {
|
||||
t.Errorf("result should contain task output, got: %s", result.ForLLM)
|
||||
}
|
||||
|
||||
// Verify spawner received correct config
|
||||
if spawner.lastCfg.TargetAgentID != "researcher" {
|
||||
t.Errorf("TargetAgentID = %q, want %q", spawner.lastCfg.TargetAgentID, "researcher")
|
||||
}
|
||||
if spawner.lastCfg.Async {
|
||||
t.Error("delegate should be synchronous (Async=false)")
|
||||
}
|
||||
if spawner.lastCfg.SystemPrompt != "summarize the logs" {
|
||||
t.Errorf("SystemPrompt = %q, want %q", spawner.lastCfg.SystemPrompt, "summarize the logs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelegateTool_Execute_EmptyAgentID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args map[string]any
|
||||
}{
|
||||
{"missing", map[string]any{"task": "test"}},
|
||||
{"empty string", map[string]any{"agent_id": "", "task": "test"}},
|
||||
{"whitespace only", map[string]any{"agent_id": " ", "task": "test"}},
|
||||
{"wrong type", map[string]any{"agent_id": 123, "task": "test"}},
|
||||
}
|
||||
|
||||
tool := NewDelegateTool()
|
||||
tool.SetSpawner(&delegateMockSpawner{})
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tool.Execute(context.Background(), tt.args)
|
||||
if !result.IsError {
|
||||
t.Error("expected error for invalid agent_id")
|
||||
}
|
||||
if !strings.Contains(result.ForLLM, "agent_id is required") {
|
||||
t.Errorf("error should mention agent_id, got: %s", result.ForLLM)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelegateTool_Execute_EmptyTask(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args map[string]any
|
||||
}{
|
||||
{"missing", map[string]any{"agent_id": "a"}},
|
||||
{"empty string", map[string]any{"agent_id": "a", "task": ""}},
|
||||
{"whitespace only", map[string]any{"agent_id": "a", "task": "\t\n"}},
|
||||
}
|
||||
|
||||
tool := NewDelegateTool()
|
||||
tool.SetSpawner(&delegateMockSpawner{})
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tool.Execute(context.Background(), tt.args)
|
||||
if !result.IsError {
|
||||
t.Error("expected error for invalid task")
|
||||
}
|
||||
if !strings.Contains(result.ForLLM, "task is required") {
|
||||
t.Errorf("error should mention task, got: %s", result.ForLLM)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelegateTool_Execute_PermissionDenied(t *testing.T) {
|
||||
tool := NewDelegateTool()
|
||||
tool.SetSpawner(&delegateMockSpawner{})
|
||||
tool.SetAllowlistChecker(func(targetAgentID string) bool {
|
||||
return targetAgentID == "allowed-agent"
|
||||
})
|
||||
|
||||
result := tool.Execute(context.Background(), map[string]any{
|
||||
"agent_id": "forbidden-agent",
|
||||
"task": "test",
|
||||
})
|
||||
|
||||
if !result.IsError {
|
||||
t.Error("expected error for denied agent")
|
||||
}
|
||||
if !strings.Contains(result.ForLLM, "not allowed to delegate") {
|
||||
t.Errorf("error should mention permission, got: %s", result.ForLLM)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelegateTool_Execute_PermissionAllowed(t *testing.T) {
|
||||
tool := NewDelegateTool()
|
||||
tool.SetSpawner(&delegateMockSpawner{})
|
||||
tool.SetAllowlistChecker(func(targetAgentID string) bool {
|
||||
return targetAgentID == "allowed-agent"
|
||||
})
|
||||
|
||||
result := tool.Execute(context.Background(), map[string]any{
|
||||
"agent_id": "allowed-agent",
|
||||
"task": "test",
|
||||
})
|
||||
|
||||
if result.IsError {
|
||||
t.Errorf("expected success for allowed agent, got error: %s", result.ForLLM)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelegateTool_Execute_NoSpawner(t *testing.T) {
|
||||
tool := NewDelegateTool()
|
||||
|
||||
result := tool.Execute(context.Background(), map[string]any{
|
||||
"agent_id": "a",
|
||||
"task": "test",
|
||||
})
|
||||
|
||||
if !result.IsError {
|
||||
t.Error("expected error when spawner is nil")
|
||||
}
|
||||
if !strings.Contains(result.ForLLM, "not configured") {
|
||||
t.Errorf("error should mention not configured, got: %s", result.ForLLM)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelegateTool_Execute_SpawnerError(t *testing.T) {
|
||||
spawner := &delegateMockSpawner{
|
||||
err: fmt.Errorf("context deadline exceeded"),
|
||||
}
|
||||
tool := NewDelegateTool()
|
||||
tool.SetSpawner(spawner)
|
||||
|
||||
result := tool.Execute(context.Background(), map[string]any{
|
||||
"agent_id": "researcher",
|
||||
"task": "test",
|
||||
})
|
||||
|
||||
if !result.IsError {
|
||||
t.Error("expected error when spawner fails")
|
||||
}
|
||||
if !strings.Contains(result.ForLLM, "delegation to agent") {
|
||||
t.Errorf("error should mention delegation failure, got: %s", result.ForLLM)
|
||||
}
|
||||
if !strings.Contains(result.ForLLM, "context deadline exceeded") {
|
||||
t.Errorf("error should propagate cause, got: %s", result.ForLLM)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelegateTool_Execute_NoAllowlistCheck(t *testing.T) {
|
||||
// When no allowlist checker is set, all agents are allowed
|
||||
tool := NewDelegateTool()
|
||||
tool.SetSpawner(&delegateMockSpawner{})
|
||||
|
||||
result := tool.Execute(context.Background(), map[string]any{
|
||||
"agent_id": "any-agent",
|
||||
"task": "test",
|
||||
})
|
||||
|
||||
if result.IsError {
|
||||
t.Errorf("expected success without allowlist, got error: %s", result.ForLLM)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelegateTool_Execute_NilResult(t *testing.T) {
|
||||
tool := NewDelegateTool()
|
||||
tool.SetSpawner(&nilResultSpawner{})
|
||||
|
||||
result := tool.Execute(context.Background(), map[string]any{
|
||||
"agent_id": "researcher",
|
||||
"task": "test",
|
||||
})
|
||||
|
||||
if !result.IsError {
|
||||
t.Error("expected error for nil result")
|
||||
}
|
||||
if !strings.Contains(result.ForLLM, "returned no result") {
|
||||
t.Errorf("error should mention no result, got: %s", result.ForLLM)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelegateTool_Execute_SelfDelegation(t *testing.T) {
|
||||
tool := NewDelegateTool()
|
||||
tool.SetSpawner(&delegateMockSpawner{})
|
||||
tool.SetSelfAgentID("alpha")
|
||||
|
||||
result := tool.Execute(context.Background(), map[string]any{
|
||||
"agent_id": "alpha",
|
||||
"task": "test",
|
||||
})
|
||||
|
||||
if !result.IsError {
|
||||
t.Error("expected error for self-delegation")
|
||||
}
|
||||
if !strings.Contains(result.ForLLM, "cannot delegate to self") {
|
||||
t.Errorf("error should mention self-delegation, got: %s", result.ForLLM)
|
||||
}
|
||||
}
|
||||
|
||||
// nilResultSpawner always returns (nil, nil).
|
||||
type nilResultSpawner struct{}
|
||||
|
||||
func (m *nilResultSpawner) SpawnSubTurn(_ context.Context, _ SubTurnConfig) (*ToolResult, error) {
|
||||
return nil, nil
|
||||
}
|
||||
Reference in New Issue
Block a user