mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
407 lines
13 KiB
Go
407 lines
13 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestSpawnStatusTool_Name(t *testing.T) {
|
|
provider := &MockLLMProvider{}
|
|
workspace := t.TempDir()
|
|
manager := NewSubagentManager(provider, "test-model", workspace)
|
|
tool := NewSpawnStatusTool(manager)
|
|
|
|
if tool.Name() != "spawn_status" {
|
|
t.Errorf("Expected name 'spawn_status', got '%s'", tool.Name())
|
|
}
|
|
}
|
|
|
|
func TestSpawnStatusTool_Description(t *testing.T) {
|
|
provider := &MockLLMProvider{}
|
|
workspace := t.TempDir()
|
|
manager := NewSubagentManager(provider, "test-model", workspace)
|
|
tool := NewSpawnStatusTool(manager)
|
|
|
|
desc := tool.Description()
|
|
if desc == "" {
|
|
t.Error("Description should not be empty")
|
|
}
|
|
if !strings.Contains(strings.ToLower(desc), "subagent") {
|
|
t.Errorf("Description should mention 'subagent', got: %s", desc)
|
|
}
|
|
}
|
|
|
|
func TestSpawnStatusTool_Parameters(t *testing.T) {
|
|
provider := &MockLLMProvider{}
|
|
workspace := t.TempDir()
|
|
manager := NewSubagentManager(provider, "test-model", workspace)
|
|
tool := NewSpawnStatusTool(manager)
|
|
|
|
params := tool.Parameters()
|
|
if params["type"] != "object" {
|
|
t.Errorf("Expected type 'object', got: %v", params["type"])
|
|
}
|
|
props, ok := params["properties"].(map[string]any)
|
|
if !ok {
|
|
t.Fatal("Expected 'properties' to be a map")
|
|
}
|
|
if _, hasTaskID := props["task_id"]; !hasTaskID {
|
|
t.Error("Expected 'task_id' parameter in properties")
|
|
}
|
|
}
|
|
|
|
func TestSpawnStatusTool_NilManager(t *testing.T) {
|
|
tool := &SpawnStatusTool{manager: nil}
|
|
result := tool.Execute(context.Background(), map[string]any{})
|
|
if !result.IsError {
|
|
t.Error("Expected error result when manager is nil")
|
|
}
|
|
}
|
|
|
|
func TestSpawnStatusTool_Empty(t *testing.T) {
|
|
provider := &MockLLMProvider{}
|
|
workspace := t.TempDir()
|
|
manager := NewSubagentManager(provider, "test-model", workspace)
|
|
tool := NewSpawnStatusTool(manager)
|
|
|
|
result := tool.Execute(context.Background(), map[string]any{})
|
|
if result.IsError {
|
|
t.Fatalf("Expected success, got error: %s", result.ForLLM)
|
|
}
|
|
if !strings.Contains(result.ForLLM, "No subagents") {
|
|
t.Errorf("Expected 'No subagents' message, got: %s", result.ForLLM)
|
|
}
|
|
}
|
|
|
|
func TestSpawnStatusTool_ListAll(t *testing.T) {
|
|
provider := &MockLLMProvider{}
|
|
workspace := t.TempDir()
|
|
manager := NewSubagentManager(provider, "test-model", workspace)
|
|
|
|
now := time.Now().UnixMilli()
|
|
manager.mu.Lock()
|
|
manager.tasks["subagent-1"] = &SubagentTask{
|
|
ID: "subagent-1",
|
|
Task: "Do task A",
|
|
Label: "task-a",
|
|
Status: "running",
|
|
Created: now,
|
|
}
|
|
manager.tasks["subagent-2"] = &SubagentTask{
|
|
ID: "subagent-2",
|
|
Task: "Do task B",
|
|
Label: "task-b",
|
|
Status: "completed",
|
|
Result: "Done successfully",
|
|
Created: now,
|
|
}
|
|
manager.tasks["subagent-3"] = &SubagentTask{
|
|
ID: "subagent-3",
|
|
Task: "Do task C",
|
|
Status: "failed",
|
|
Result: "Error: something went wrong",
|
|
}
|
|
manager.mu.Unlock()
|
|
|
|
tool := NewSpawnStatusTool(manager)
|
|
result := tool.Execute(context.Background(), map[string]any{})
|
|
|
|
if result.IsError {
|
|
t.Fatalf("Expected success, got error: %s", result.ForLLM)
|
|
}
|
|
|
|
// Summary header
|
|
if !strings.Contains(result.ForLLM, "3 total") {
|
|
t.Errorf("Expected total count in header, got: %s", result.ForLLM)
|
|
}
|
|
|
|
// Individual task IDs
|
|
for _, id := range []string{"subagent-1", "subagent-2", "subagent-3"} {
|
|
if !strings.Contains(result.ForLLM, id) {
|
|
t.Errorf("Expected task %s in output, got:\n%s", id, result.ForLLM)
|
|
}
|
|
}
|
|
|
|
// Status values
|
|
for _, status := range []string{"running", "completed", "failed"} {
|
|
if !strings.Contains(result.ForLLM, status) {
|
|
t.Errorf("Expected status '%s' in output, got:\n%s", status, result.ForLLM)
|
|
}
|
|
}
|
|
|
|
// Result content
|
|
if !strings.Contains(result.ForLLM, "Done successfully") {
|
|
t.Errorf("Expected result text in output, got:\n%s", result.ForLLM)
|
|
}
|
|
}
|
|
|
|
func TestSpawnStatusTool_GetByID(t *testing.T) {
|
|
provider := &MockLLMProvider{}
|
|
manager := NewSubagentManager(provider, "test-model", "/tmp/test")
|
|
|
|
manager.mu.Lock()
|
|
manager.tasks["subagent-42"] = &SubagentTask{
|
|
ID: "subagent-42",
|
|
Task: "Specific task",
|
|
Label: "my-task",
|
|
Status: "failed",
|
|
Result: "Something went wrong",
|
|
Created: time.Now().UnixMilli(),
|
|
}
|
|
manager.mu.Unlock()
|
|
|
|
tool := NewSpawnStatusTool(manager)
|
|
result := tool.Execute(context.Background(), map[string]any{"task_id": "subagent-42"})
|
|
|
|
if result.IsError {
|
|
t.Fatalf("Expected success, got error: %s", result.ForLLM)
|
|
}
|
|
if !strings.Contains(result.ForLLM, "subagent-42") {
|
|
t.Errorf("Expected task ID in output, got: %s", result.ForLLM)
|
|
}
|
|
if !strings.Contains(result.ForLLM, "failed") {
|
|
t.Errorf("Expected status 'failed' in output, got: %s", result.ForLLM)
|
|
}
|
|
if !strings.Contains(result.ForLLM, "Something went wrong") {
|
|
t.Errorf("Expected result text in output, got: %s", result.ForLLM)
|
|
}
|
|
if !strings.Contains(result.ForLLM, "my-task") {
|
|
t.Errorf("Expected label in output, got: %s", result.ForLLM)
|
|
}
|
|
}
|
|
|
|
func TestSpawnStatusTool_GetByID_NotFound(t *testing.T) {
|
|
provider := &MockLLMProvider{}
|
|
manager := NewSubagentManager(provider, "test-model", "/tmp/test")
|
|
tool := NewSpawnStatusTool(manager)
|
|
|
|
result := tool.Execute(context.Background(), map[string]any{"task_id": "nonexistent-999"})
|
|
if !result.IsError {
|
|
t.Errorf("Expected error for nonexistent task, got: %s", result.ForLLM)
|
|
}
|
|
if !strings.Contains(result.ForLLM, "nonexistent-999") {
|
|
t.Errorf("Expected task ID in error message, got: %s", result.ForLLM)
|
|
}
|
|
}
|
|
|
|
func TestSpawnStatusTool_TaskID_NonString(t *testing.T) {
|
|
provider := &MockLLMProvider{}
|
|
manager := NewSubagentManager(provider, "test-model", "/tmp/test")
|
|
tool := NewSpawnStatusTool(manager)
|
|
|
|
for _, badVal := range []any{42, 3.14, true, map[string]any{"x": 1}, []string{"a"}} {
|
|
result := tool.Execute(context.Background(), map[string]any{"task_id": badVal})
|
|
if !result.IsError {
|
|
t.Errorf("Expected error for task_id=%T(%v), got success: %s", badVal, badVal, result.ForLLM)
|
|
}
|
|
if !strings.Contains(result.ForLLM, "task_id must be a string") {
|
|
t.Errorf("Expected type-error message, got: %s", result.ForLLM)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSpawnStatusTool_ResultTruncation(t *testing.T) {
|
|
provider := &MockLLMProvider{}
|
|
manager := NewSubagentManager(provider, "test-model", "/tmp/test")
|
|
|
|
longResult := strings.Repeat("X", 500)
|
|
manager.mu.Lock()
|
|
manager.tasks["subagent-1"] = &SubagentTask{
|
|
ID: "subagent-1",
|
|
Task: "Long task",
|
|
Status: "completed",
|
|
Result: longResult,
|
|
}
|
|
manager.mu.Unlock()
|
|
|
|
tool := NewSpawnStatusTool(manager)
|
|
result := tool.Execute(context.Background(), map[string]any{"task_id": "subagent-1"})
|
|
|
|
if result.IsError {
|
|
t.Fatalf("Unexpected error: %s", result.ForLLM)
|
|
}
|
|
// Output should be shorter than the raw result due to truncation
|
|
if len(result.ForLLM) >= len(longResult) {
|
|
t.Errorf("Expected result to be truncated, but ForLLM is %d chars", len(result.ForLLM))
|
|
}
|
|
if !strings.Contains(result.ForLLM, "…") {
|
|
t.Errorf("Expected truncation indicator '…' in output, got: %s", result.ForLLM)
|
|
}
|
|
}
|
|
|
|
func TestSpawnStatusTool_ResultTruncation_Unicode(t *testing.T) {
|
|
provider := &MockLLMProvider{}
|
|
manager := NewSubagentManager(provider, "test-model", "/tmp/test")
|
|
|
|
// Each CJK rune is 3 bytes; 400 runes = 1200 bytes — well over the 300-rune limit.
|
|
cjkChar := string(rune(0x5b57))
|
|
longResult := strings.Repeat(cjkChar, 400)
|
|
manager.mu.Lock()
|
|
manager.tasks["subagent-1"] = &SubagentTask{
|
|
ID: "subagent-1",
|
|
Task: "Unicode task",
|
|
Status: "completed",
|
|
Result: longResult,
|
|
}
|
|
manager.mu.Unlock()
|
|
|
|
tool := NewSpawnStatusTool(manager)
|
|
result := tool.Execute(context.Background(), map[string]any{"task_id": "subagent-1"})
|
|
|
|
if result.IsError {
|
|
t.Fatalf("Unexpected error: %s", result.ForLLM)
|
|
}
|
|
if !strings.Contains(result.ForLLM, "…") {
|
|
t.Errorf("Expected truncation indicator in output")
|
|
}
|
|
// The truncated result must be valid UTF-8 (no split rune boundaries).
|
|
if !strings.Contains(result.ForLLM, cjkChar) {
|
|
t.Errorf("Expected CJK runes to appear intact in output")
|
|
}
|
|
}
|
|
|
|
func TestSpawnStatusTool_StatusCounts(t *testing.T) {
|
|
provider := &MockLLMProvider{}
|
|
manager := NewSubagentManager(provider, "test-model", "/tmp/test")
|
|
|
|
manager.mu.Lock()
|
|
for i, status := range []string{"running", "running", "completed", "failed", "canceled"} {
|
|
id := fmt.Sprintf("subagent-%d", i+1)
|
|
manager.tasks[id] = &SubagentTask{ID: id, Task: "t", Status: status}
|
|
}
|
|
manager.mu.Unlock()
|
|
|
|
tool := NewSpawnStatusTool(manager)
|
|
result := tool.Execute(context.Background(), map[string]any{})
|
|
|
|
if result.IsError {
|
|
t.Fatalf("Unexpected error: %s", result.ForLLM)
|
|
}
|
|
// The summary line should mention all statuses that have counts
|
|
for _, want := range []string{"Running:", "Completed:", "Failed:", "Canceled:"} {
|
|
if !strings.Contains(result.ForLLM, want) {
|
|
t.Errorf("Expected %q in summary, got:\n%s", want, result.ForLLM)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSpawnStatusTool_SortByCreatedTimestamp(t *testing.T) {
|
|
provider := &MockLLMProvider{}
|
|
manager := NewSubagentManager(provider, "test-model", "/tmp/test")
|
|
|
|
now := time.Now().UnixMilli()
|
|
manager.mu.Lock()
|
|
// Intentionally insert with out-of-order IDs and timestamps that reflect
|
|
// true spawn order: subagent-2 was spawned first, subagent-10 second.
|
|
manager.tasks["subagent-10"] = &SubagentTask{
|
|
ID: "subagent-10", Task: "second", Status: "running",
|
|
Created: now + 1,
|
|
}
|
|
manager.tasks["subagent-2"] = &SubagentTask{
|
|
ID: "subagent-2", Task: "first", Status: "running",
|
|
Created: now,
|
|
}
|
|
manager.mu.Unlock()
|
|
|
|
tool := NewSpawnStatusTool(manager)
|
|
result := tool.Execute(context.Background(), map[string]any{})
|
|
|
|
if result.IsError {
|
|
t.Fatalf("Unexpected error: %s", result.ForLLM)
|
|
}
|
|
|
|
pos2 := strings.Index(result.ForLLM, "subagent-2")
|
|
pos10 := strings.Index(result.ForLLM, "subagent-10")
|
|
if pos2 < 0 || pos10 < 0 {
|
|
t.Fatalf("Both task IDs should appear in output:\n%s", result.ForLLM)
|
|
}
|
|
if pos2 > pos10 {
|
|
t.Errorf("Expected subagent-2 (created first) to appear before subagent-10, but got:\n%s", result.ForLLM)
|
|
}
|
|
}
|
|
|
|
func TestSpawnStatusTool_ChannelFiltering_ListAll(t *testing.T) {
|
|
provider := &MockLLMProvider{}
|
|
manager := NewSubagentManager(provider, "test-model", "/tmp/test")
|
|
|
|
manager.mu.Lock()
|
|
manager.tasks["subagent-1"] = &SubagentTask{
|
|
ID: "subagent-1", Task: "mine", Status: "running",
|
|
OriginChannel: "telegram", OriginChatID: "chat-A",
|
|
}
|
|
manager.tasks["subagent-2"] = &SubagentTask{
|
|
ID: "subagent-2", Task: "other user", Status: "running",
|
|
OriginChannel: "telegram", OriginChatID: "chat-B",
|
|
}
|
|
manager.mu.Unlock()
|
|
|
|
tool := NewSpawnStatusTool(manager)
|
|
|
|
// Caller is chat-A — should only see subagent-1.
|
|
ctx := WithToolContext(context.Background(), "telegram", "chat-A")
|
|
result := tool.Execute(ctx, map[string]any{})
|
|
|
|
if result.IsError {
|
|
t.Fatalf("Unexpected error: %s", result.ForLLM)
|
|
}
|
|
if !strings.Contains(result.ForLLM, "subagent-1") {
|
|
t.Errorf("Expected own task in output, got:\n%s", result.ForLLM)
|
|
}
|
|
if strings.Contains(result.ForLLM, "subagent-2") {
|
|
t.Errorf("Should NOT see other chat's task, got:\n%s", result.ForLLM)
|
|
}
|
|
}
|
|
|
|
func TestSpawnStatusTool_ChannelFiltering_GetByID(t *testing.T) {
|
|
provider := &MockLLMProvider{}
|
|
manager := NewSubagentManager(provider, "test-model", "/tmp/test")
|
|
|
|
manager.mu.Lock()
|
|
manager.tasks["subagent-99"] = &SubagentTask{
|
|
ID: "subagent-99", Task: "secret", Status: "completed", Result: "private data",
|
|
OriginChannel: "slack", OriginChatID: "room-Z",
|
|
}
|
|
manager.mu.Unlock()
|
|
|
|
tool := NewSpawnStatusTool(manager)
|
|
|
|
// Different chat trying to look up subagent-99 by ID.
|
|
ctx := WithToolContext(context.Background(), "slack", "room-OTHER")
|
|
result := tool.Execute(ctx, map[string]any{"task_id": "subagent-99"})
|
|
|
|
if !result.IsError {
|
|
t.Errorf("Expected error (cross-chat lookup blocked), got: %s", result.ForLLM)
|
|
}
|
|
}
|
|
|
|
func TestSpawnStatusTool_ChannelFiltering_NoContext(t *testing.T) {
|
|
provider := &MockLLMProvider{}
|
|
manager := NewSubagentManager(provider, "test-model", "/tmp/test")
|
|
|
|
manager.mu.Lock()
|
|
manager.tasks["subagent-1"] = &SubagentTask{
|
|
ID: "subagent-1", Task: "t", Status: "completed",
|
|
OriginChannel: "telegram", OriginChatID: "chat-A",
|
|
}
|
|
manager.mu.Unlock()
|
|
|
|
tool := NewSpawnStatusTool(manager)
|
|
|
|
// No ToolContext injected (e.g. a direct programmatic call that bypasses
|
|
// WithToolContext entirely) — callerChannel and callerChatID are both "".
|
|
// Note: the normal CLI path uses ProcessDirectWithChannel("cli", "direct"),
|
|
// which *does* inject a non-empty context; this test covers the case where
|
|
// no context injection happens at all.
|
|
// The filter conditions require a non-empty caller value, so all tasks pass through.
|
|
result := tool.Execute(context.Background(), map[string]any{})
|
|
if result.IsError {
|
|
t.Fatalf("Unexpected error: %s", result.ForLLM)
|
|
}
|
|
if !strings.Contains(result.ForLLM, "subagent-1") {
|
|
t.Errorf("Expected task visible from no-context caller, got:\n%s", result.ForLLM)
|
|
}
|
|
}
|