66 "io"
77 "net/http"
88 "net/http/httptest"
9+ "net/url"
910 "os"
1011 "path/filepath"
1112 "strings"
@@ -155,6 +156,10 @@ func TestConfigDefaultsUsesWSMethod(t *testing.T) {
155156func TestToolsInvokeArgsReadsFile (t * testing.T ) {
156157 defer resetTestFlag (toolsInvokeCmd , "args" , "" )
157158 defer resetTestFlag (toolsInvokeCmd , "param" , "" )
159+ defer resetTestFlag (toolsInvokeCmd , "agent" , "" )
160+ defer resetTestFlag (toolsInvokeCmd , "action" , "" )
161+ defer resetTestFlag (toolsInvokeCmd , "session" , "" )
162+ defer resetTestFlag (toolsInvokeCmd , "dry-run" , "false" )
158163 var body map [string ]any
159164 srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
160165 if r .URL .Path != "/v1/tools/invoke" {
@@ -172,15 +177,195 @@ func TestToolsInvokeArgsReadsFile(t *testing.T) {
172177 if err := os .WriteFile (path , []byte (`{"city":"Saigon"}` ), 0o600 ); err != nil {
173178 t .Fatalf ("write args: %v" , err )
174179 }
175- if err := runCmd (t , "tools" , "invoke" , "weather" , "--args=@" + path , "--param=unit=c" ); err != nil {
180+ if err := runCmd (t , "tools" , "invoke" , "weather" , "--args=@" + path , "--param=unit=c" ,
181+ "--agent=goclaw" , "--action=forecast" , "--session=sess-1" , "--dry-run" ); err != nil {
176182 t .Fatalf ("tools invoke: %v" , err )
177183 }
178- params := body ["parameters" ].(map [string ]any )
184+ if body ["tool" ] != "weather" {
185+ t .Fatalf ("tool = %#v" , body ["tool" ])
186+ }
187+ if body ["agentId" ] != "goclaw" || body ["action" ] != "forecast" ||
188+ body ["sessionKey" ] != "sess-1" || body ["dryRun" ] != true {
189+ t .Fatalf ("context fields = %#v" , body )
190+ }
191+ params := body ["args" ].(map [string ]any )
179192 if params ["city" ] != "Saigon" || params ["unit" ] != "c" {
180193 t .Fatalf ("params = %#v" , params )
181194 }
182195}
183196
197+ func TestToolsCustomUnsupportedDoesNotCallServer (t * testing.T ) {
198+ requests := 0
199+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
200+ requests ++
201+ w .WriteHeader (http .StatusInternalServerError )
202+ }))
203+ defer srv .Close ()
204+ t .Setenv ("GOCLAW_SERVER" , srv .URL )
205+ t .Setenv ("GOCLAW_TOKEN" , "test-token" )
206+
207+ err := runCmd (t , "tools" , "custom" , "list" )
208+ if err == nil {
209+ t .Fatal ("expected unsupported custom tools error" )
210+ }
211+ var detail * output.ErrorDetail
212+ if ! errors .As (err , & detail ) || detail .Code != "INVALID_REQUEST" {
213+ t .Fatalf ("error = %#v, want INVALID_REQUEST detail" , err )
214+ }
215+ if requests != 0 {
216+ t .Fatalf ("requests = %d, want 0" , requests )
217+ }
218+ }
219+
220+ func TestUsageTimeseriesMapsFlagsToServerContract (t * testing.T ) {
221+ defer resetTestFlag (usageTimeseriesCmd , "start" , "" )
222+ defer resetTestFlag (usageTimeseriesCmd , "end" , "" )
223+ defer resetTestFlag (usageTimeseriesCmd , "granularity" , "day" )
224+ defer resetTestFlag (usageTimeseriesCmd , "agent" , "" )
225+ var query url.Values
226+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
227+ if r .URL .Path != "/v1/usage/timeseries" {
228+ w .WriteHeader (http .StatusNotFound )
229+ return
230+ }
231+ query = r .URL .Query ()
232+ okJSON (t , w , map [string ]any {"series" : []map [string ]any {}})
233+ }))
234+ defer srv .Close ()
235+ t .Setenv ("GOCLAW_SERVER" , srv .URL )
236+ t .Setenv ("GOCLAW_TOKEN" , "test-token" )
237+
238+ if err := runCmd (t , "usage" , "timeseries" , "--start=2026-05-20" , "--end=2026-05-21" , "--granularity=day" , "--agent=agent-1" ); err != nil {
239+ t .Fatalf ("usage timeseries: %v" , err )
240+ }
241+ if query .Get ("from" ) != "2026-05-20T00:00:00Z" || query .Get ("to" ) != "2026-05-21T00:00:00Z" ||
242+ query .Get ("group_by" ) != "day" || query .Get ("agent_id" ) != "agent-1" {
243+ t .Fatalf ("query = %#v" , query )
244+ }
245+ if query .Has ("start" ) || query .Has ("end" ) || query .Has ("granularity" ) || query .Has ("agent" ) {
246+ t .Fatalf ("query contains legacy keys: %#v" , query )
247+ }
248+ }
249+
250+ func TestUsageBreakdownMapsFlagsToServerContract (t * testing.T ) {
251+ defer resetTestFlag (usageBreakdownCmd , "start" , "" )
252+ defer resetTestFlag (usageBreakdownCmd , "end" , "" )
253+ defer resetTestFlag (usageBreakdownCmd , "by" , "agent" )
254+ var query url.Values
255+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
256+ if r .URL .Path != "/v1/usage/breakdown" {
257+ w .WriteHeader (http .StatusNotFound )
258+ return
259+ }
260+ query = r .URL .Query ()
261+ okJSON (t , w , map [string ]any {"groups" : []map [string ]any {}})
262+ }))
263+ defer srv .Close ()
264+ t .Setenv ("GOCLAW_SERVER" , srv .URL )
265+ t .Setenv ("GOCLAW_TOKEN" , "test-token" )
266+
267+ if err := runCmd (t , "usage" , "breakdown" , "--start=2026-05-20" , "--end=2026-05-21" , "--by=provider" ); err != nil {
268+ t .Fatalf ("usage breakdown: %v" , err )
269+ }
270+ if query .Get ("from" ) != "2026-05-20T00:00:00Z" || query .Get ("to" ) != "2026-05-21T00:00:00Z" ||
271+ query .Get ("group_by" ) != "provider" {
272+ t .Fatalf ("query = %#v" , query )
273+ }
274+ if query .Has ("start" ) || query .Has ("end" ) || query .Has ("by" ) {
275+ t .Fatalf ("query contains legacy keys: %#v" , query )
276+ }
277+ }
278+
279+ func TestMemoryCommandsUseAgentScopedHTTPRoutes (t * testing.T ) {
280+ defer resetTestFlag (memoryListCmd , "user" , "" )
281+ defer resetTestFlag (memorySearchCmd , "query" , "" )
282+ defer resetTestFlag (memorySearchCmd , "user" , "" )
283+ var paths []string
284+ var searchBody map [string ]any
285+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
286+ paths = append (paths , r .URL .String ())
287+ switch r .URL .Path {
288+ case "/v1/agents/goclaw/memory/documents" :
289+ okJSON (t , w , []map [string ]any {{"path" : "notes.md" }})
290+ case "/v1/agents/goclaw/memory/search" :
291+ _ = json .NewDecoder (r .Body ).Decode (& searchBody )
292+ okJSON (t , w , map [string ]any {"results" : []map [string ]any {}, "count" : 0 })
293+ default :
294+ w .WriteHeader (http .StatusNotFound )
295+ }
296+ }))
297+ defer srv .Close ()
298+ t .Setenv ("GOCLAW_SERVER" , srv .URL )
299+ t .Setenv ("GOCLAW_TOKEN" , "test-token" )
300+
301+ if err := runCmd (t , "memory" , "list" , "goclaw" , "--user=system" ); err != nil {
302+ t .Fatalf ("memory list: %v" , err )
303+ }
304+ if err := runCmd (t , "memory" , "search" , "goclaw" , "--query=test" ); err != nil {
305+ t .Fatalf ("memory search: %v" , err )
306+ }
307+ if len (paths ) != 2 || paths [0 ] != "/v1/agents/goclaw/memory/documents?user_id=system" ||
308+ paths [1 ] != "/v1/agents/goclaw/memory/search" {
309+ t .Fatalf ("paths = %#v" , paths )
310+ }
311+ if searchBody ["query" ] != "test" {
312+ t .Fatalf ("search body = %#v" , searchBody )
313+ }
314+ }
315+
316+ func TestListCommandsAcceptObjectWrappedLists (t * testing.T ) {
317+ var paths []string
318+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
319+ paths = append (paths , r .URL .Path )
320+ switch r .URL .Path {
321+ case "/v1/agents" :
322+ okJSON (t , w , map [string ]any {"agents" : []map [string ]any {{"id" : "agent-1" }}})
323+ case "/v1/sessions" :
324+ okJSON (t , w , map [string ]any {"sessions" : []map [string ]any {{"session_key" : "sess-1" }}})
325+ case "/v1/tools/builtin" :
326+ okJSON (t , w , map [string ]any {"tools" : []map [string ]any {{"name" : "sessions_list" }}})
327+ default :
328+ w .WriteHeader (http .StatusNotFound )
329+ }
330+ }))
331+ defer srv .Close ()
332+ t .Setenv ("GOCLAW_SERVER" , srv .URL )
333+ t .Setenv ("GOCLAW_TOKEN" , "test-token" )
334+ t .Setenv ("GOCLAW_OUTPUT" , "json" )
335+
336+ agentsOut , err := captureStdout (t , func () error {
337+ return runCmd (t , "agents" , "list" )
338+ })
339+ if err != nil {
340+ t .Fatalf ("agents list: %v" , err )
341+ }
342+ sessionsOut , err := captureStdout (t , func () error {
343+ return runCmd (t , "sessions" , "list" )
344+ })
345+ if err != nil {
346+ t .Fatalf ("sessions list: %v" , err )
347+ }
348+ toolsOut , err := captureStdout (t , func () error {
349+ return runCmd (t , "tools" , "builtin" , "list" )
350+ })
351+ if err != nil {
352+ t .Fatalf ("tools builtin list: %v" , err )
353+ }
354+ if strings .Contains (agentsOut , "null" ) || ! strings .Contains (agentsOut , "agent-1" ) {
355+ t .Fatalf ("agents stdout = %q" , agentsOut )
356+ }
357+ if strings .Contains (sessionsOut , "null" ) || ! strings .Contains (sessionsOut , "sess-1" ) {
358+ t .Fatalf ("sessions stdout = %q" , sessionsOut )
359+ }
360+ if strings .Contains (toolsOut , "null" ) || ! strings .Contains (toolsOut , "sessions_list" ) {
361+ t .Fatalf ("tools stdout = %q" , toolsOut )
362+ }
363+ if len (paths ) != 3 || paths [0 ] != "/v1/agents" || paths [1 ] != "/v1/sessions" ||
364+ paths [2 ] != "/v1/tools/builtin" {
365+ t .Fatalf ("paths = %#v" , paths )
366+ }
367+ }
368+
184369func TestChatReplayAndResumeUseExistingWSContracts (t * testing.T ) {
185370 defer resetTestFlag (chatReplayCmd , "session" , "" )
186371 defer resetTestFlag (chatReplayCmd , "before" , "" )
0 commit comments