Files
clawgo/pkg/tools/subagent_runtime_control_test.go

166 lines
4.2 KiB
Go

package tools
import (
"context"
"errors"
"strings"
"testing"
"time"
"clawgo/pkg/bus"
)
func TestSubagentSpawnEnforcesTaskQuota(t *testing.T) {
t.Parallel()
workspace := t.TempDir()
manager := NewSubagentManager(nil, workspace, nil, nil)
manager.SetRunFunc(func(ctx context.Context, task *SubagentTask) (string, error) {
return "ok", nil
})
store := manager.ProfileStore()
if store == nil {
t.Fatalf("expected profile store")
}
if _, err := store.Upsert(SubagentProfile{
AgentID: "coder",
MaxTaskChars: 8,
}); err != nil {
t.Fatalf("failed to create profile: %v", err)
}
_, err := manager.Spawn(context.Background(), SubagentSpawnOptions{
Task: "this task is too long",
AgentID: "coder",
OriginChannel: "cli",
OriginChatID: "direct",
})
if err == nil {
t.Fatalf("expected max_task_chars quota to reject spawn")
}
}
func TestSubagentRunWithRetryEventuallySucceeds(t *testing.T) {
workspace := t.TempDir()
manager := NewSubagentManager(nil, workspace, nil, nil)
attempts := 0
manager.SetRunFunc(func(ctx context.Context, task *SubagentTask) (string, error) {
attempts++
if attempts == 1 {
return "", errors.New("temporary failure")
}
return "retry success", nil
})
_, err := manager.Spawn(context.Background(), SubagentSpawnOptions{
Task: "retry task",
AgentID: "coder",
OriginChannel: "cli",
OriginChatID: "direct",
MaxRetries: 1,
RetryBackoff: 1,
})
if err != nil {
t.Fatalf("spawn failed: %v", err)
}
task := waitSubagentDone(t, manager, 4*time.Second)
if task.Status != "completed" {
t.Fatalf("expected completed task, got %s (%s)", task.Status, task.Result)
}
if task.RetryCount != 1 {
t.Fatalf("expected retry_count=1, got %d", task.RetryCount)
}
if attempts < 2 {
t.Fatalf("expected at least 2 attempts, got %d", attempts)
}
}
func TestSubagentRunWithTimeoutFails(t *testing.T) {
workspace := t.TempDir()
manager := NewSubagentManager(nil, workspace, nil, nil)
manager.SetRunFunc(func(ctx context.Context, task *SubagentTask) (string, error) {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(2 * time.Second):
return "unexpected", nil
}
})
_, err := manager.Spawn(context.Background(), SubagentSpawnOptions{
Task: "timeout task",
AgentID: "coder",
OriginChannel: "cli",
OriginChatID: "direct",
TimeoutSec: 1,
})
if err != nil {
t.Fatalf("spawn failed: %v", err)
}
task := waitSubagentDone(t, manager, 4*time.Second)
if task.Status != "failed" {
t.Fatalf("expected failed task on timeout, got %s", task.Status)
}
if task.RetryCount != 0 {
t.Fatalf("expected retry_count=0, got %d", task.RetryCount)
}
}
func TestSubagentBroadcastIncludesFailureStatus(t *testing.T) {
workspace := t.TempDir()
msgBus := bus.NewMessageBus()
defer msgBus.Close()
manager := NewSubagentManager(nil, workspace, msgBus, nil)
manager.SetRunFunc(func(ctx context.Context, task *SubagentTask) (string, error) {
return "", errors.New("boom")
})
_, err := manager.Spawn(context.Background(), SubagentSpawnOptions{
Task: "failing task",
AgentID: "coder",
OriginChannel: "cli",
OriginChatID: "direct",
})
if err != nil {
t.Fatalf("spawn failed: %v", err)
}
task := waitSubagentDone(t, manager, 4*time.Second)
if task.Status != "failed" {
t.Fatalf("expected failed task, got %s", task.Status)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
msg, ok := msgBus.ConsumeInbound(ctx)
if !ok {
t.Fatalf("expected subagent completion message")
}
if got := strings.TrimSpace(msg.Metadata["status"]); got != "failed" {
t.Fatalf("expected metadata status=failed, got %q", got)
}
if !strings.Contains(strings.ToLower(msg.Content), "failed") {
t.Fatalf("expected failure wording in content, got %q", msg.Content)
}
}
func waitSubagentDone(t *testing.T, manager *SubagentManager, timeout time.Duration) *SubagentTask {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
tasks := manager.ListTasks()
if len(tasks) > 0 {
task := tasks[0]
if task.Status != "running" {
return task
}
}
time.Sleep(30 * time.Millisecond)
}
t.Fatalf("timeout waiting for subagent completion")
return nil
}