cloud_delegate_test.go
1 package tools 2 3 import ( 4 "context" 5 "encoding/json" 6 "testing" 7 "time" 8 9 "github.com/Kocoro-lab/ShanClaw/internal/agent" 10 ) 11 12 func TestCloudDelegateInfo(t *testing.T) { 13 tool := NewCloudDelegateTool(nil, "", 60*time.Second, nil, "", "") 14 info := tool.Info() 15 if info.Name != "cloud_delegate" { 16 t.Errorf("expected name cloud_delegate, got %s", info.Name) 17 } 18 if len(info.Required) != 1 || info.Required[0] != "task" { 19 t.Errorf("expected required=[task], got %v", info.Required) 20 } 21 // Schema must expose the terminal parameter 22 props, ok := info.Parameters["properties"].(map[string]any) 23 if !ok { 24 t.Fatal("expected properties in schema") 25 } 26 if _, ok := props["terminal"]; !ok { 27 t.Error("schema should expose 'terminal' parameter") 28 } 29 } 30 31 func TestCloudDelegateTerminalDefault(t *testing.T) { 32 tests := []struct { 33 name string 34 args string 35 wantCloud bool // expected CloudResult (ignoring fullResultConfirmed) 36 }{ 37 {"research defaults terminal", `{"task":"t","workflow_type":"research"}`, true}, 38 {"swarm defaults non-terminal", `{"task":"t","workflow_type":"swarm"}`, false}, 39 {"auto defaults non-terminal", `{"task":"t","workflow_type":"auto"}`, false}, 40 {"omitted defaults non-terminal", `{"task":"t"}`, false}, 41 {"explicit false overrides research", `{"task":"t","workflow_type":"research","terminal":false}`, false}, 42 {"explicit true overrides swarm", `{"task":"t","workflow_type":"swarm","terminal":true}`, true}, 43 } 44 45 for _, tt := range tests { 46 t.Run(tt.name, func(t *testing.T) { 47 // Will fail at gateway (nil), but we can check CloudResult on the error path 48 // Since gateway is nil, result is always an error — CloudResult won't be set. 49 // Instead, verify the arg parsing and terminal logic directly. 50 var args cloudDelegateArgs 51 if err := json.Unmarshal([]byte(tt.args), &args); err != nil { 52 t.Fatalf("failed to parse args: %v", err) 53 } 54 terminal := args.WorkflowType == "research" 55 if args.Terminal != nil { 56 terminal = *args.Terminal 57 } 58 if terminal != tt.wantCloud { 59 t.Errorf("terminal=%v, want %v", terminal, tt.wantCloud) 60 } 61 }) 62 } 63 } 64 65 func TestCloudDelegateRequiresApproval(t *testing.T) { 66 tool := NewCloudDelegateTool(nil, "", 60*time.Second, nil, "", "") 67 if !tool.RequiresApproval() { 68 t.Error("cloud_delegate should require approval") 69 } 70 if tool.IsSafeArgs(`{"task":"anything"}`) { 71 t.Error("IsSafeArgs should always return false") 72 } 73 } 74 75 func TestCloudDelegateEmptyTask(t *testing.T) { 76 tool := NewCloudDelegateTool(nil, "", 60*time.Second, nil, "", "") 77 result, err := tool.Run(context.Background(), `{"task":""}`) 78 if err != nil { 79 t.Fatal(err) 80 } 81 if !result.IsError { 82 t.Error("expected error for empty task") 83 } 84 } 85 86 func TestCloudDelegateInvalidJSON(t *testing.T) { 87 tool := NewCloudDelegateTool(nil, "", 60*time.Second, nil, "", "") 88 result, err := tool.Run(context.Background(), `not json`) 89 if err != nil { 90 t.Fatal(err) 91 } 92 if !result.IsError { 93 t.Error("expected error for invalid JSON") 94 } 95 } 96 97 func TestCloudDelegateNoGateway(t *testing.T) { 98 tool := NewCloudDelegateTool(nil, "", 60*time.Second, nil, "", "") 99 result, err := tool.Run(context.Background(), `{"task":"test task"}`) 100 if err != nil { 101 t.Fatal(err) 102 } 103 if !result.IsError { 104 t.Error("expected error when gateway is nil") 105 } 106 } 107 108 func TestCloudDelegateContextTruncation(t *testing.T) { 109 tool := NewCloudDelegateTool(nil, "", 60*time.Second, nil, "", "") 110 longCtx := make([]byte, 9000) 111 for i := range longCtx { 112 longCtx[i] = 'x' 113 } 114 // Will fail at submission (nil gateway), but should get past arg parsing + truncation 115 result, _ := tool.Run(context.Background(), `{"task":"test","context":"`+string(longCtx)+`"}`) 116 if !result.IsError { 117 t.Log("Expected error (nil gateway)") 118 } 119 } 120 121 func TestCloudDelegateAccumulateUsage_ParsesSplitCacheCreation(t *testing.T) { 122 tool := NewCloudDelegateTool(nil, "", 60*time.Second, nil, "", "") 123 var usage agent.TurnUsage 124 125 tool.accumulateUsage(`{ 126 "metadata": { 127 "input_tokens": 120, 128 "output_tokens": 30, 129 "tokens_used": 180, 130 "cost_usd": 0.42, 131 "cache_read_tokens": 50, 132 "cache_creation_5m_tokens": 100, 133 "cache_creation_1h_tokens": 200, 134 "model_used": "claude-cloud" 135 } 136 }`, &usage) 137 138 if usage.InputTokens != 120 || usage.OutputTokens != 30 { 139 t.Fatalf("expected input/output 120/30, got %d/%d", usage.InputTokens, usage.OutputTokens) 140 } 141 if usage.TotalTokens != 180 { 142 t.Fatalf("expected total tokens 180, got %d", usage.TotalTokens) 143 } 144 if usage.CacheCreationTokens != 300 { 145 t.Fatalf("expected legacy cache creation total 300, got %d", usage.CacheCreationTokens) 146 } 147 if usage.CacheCreation5mTokens != 100 || usage.CacheCreation1hTokens != 200 { 148 t.Fatalf("expected split cache creation 100/200, got %d/%d", usage.CacheCreation5mTokens, usage.CacheCreation1hTokens) 149 } 150 if usage.Model != "claude-cloud" { 151 t.Fatalf("expected model claude-cloud, got %q", usage.Model) 152 } 153 if usage.LLMCalls != 1 { 154 t.Fatalf("expected 1 LLM call, got %d", usage.LLMCalls) 155 } 156 }