/ internal / tools / cloud_delegate_test.go
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  }