package agent import ( "context" "encoding/json" "testing" "ollie/pkg/backend" ) // TestTextToolCall_ParsedAndExecuted verifies that when a model emits a tool // call as plain text (no API-level tool calls) the loop parses it and executes // it as a normal tool call. func TestTextToolCall_ParsedAndExecuted(t *testing.T) { be := defaultBE() be.respond = func(_ context.Context, msgs []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) { // First call: model emits text tool call for _, m := range msgs { if m.Role == "tool" { // Second call after tool result: respond normally return textStream("done"), nil } } return textStream(`call_tool: {"calls":[{"tool":"file_read","args":["/tmp/test.go"]}]}`), nil } var execCalled bool c := newCore(t, be, nil) c.runtime.Tools = []backend.Tool{ {Name: "call_tool", Description: "run tools"}, {Name: "execute_code", Description: "run code"}, } c.runtime.Exec = func(_ context.Context, name string, args json.RawMessage) (string, error) { execCalled = true if name != "call_tool" { t.Errorf("expected tool name 'call_tool', got %q", name) } return "file contents here", nil } evs := collectEvents(context.Background(), c, "read a file") if !execCalled { t.Fatal("expected Exec to be called for text-based tool call") } errEvs := byRole(evs, "error") if len(errEvs) != 0 { t.Errorf("unexpected error events: %v", errEvs) } } // TestTextToolCall_RelaxedJSON verifies parsing of relaxed JSON (unquoted keys). func TestTextToolCall_RelaxedJSON(t *testing.T) { be := defaultBE() be.respond = func(_ context.Context, msgs []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) { for _, m := range msgs { if m.Role == "tool" { return textStream("done"), nil } } return textStream(`execute_code: steps=[{code: "date"}]`), nil } var execCalled bool c := newCore(t, be, nil) c.runtime.Tools = []backend.Tool{ {Name: "execute_code", Description: "run code"}, {Name: "call_tool", Description: "run tools"}, } c.runtime.Exec = func(_ context.Context, name string, args json.RawMessage) (string, error) { execCalled = true if name != "execute_code" { t.Errorf("expected tool name 'execute_code', got %q", name) } return "Sat May 23 11:00:00 UTC 2026", nil } collectEvents(context.Background(), c, "what time is it") if !execCalled { t.Fatal("expected Exec to be called for relaxed-JSON text tool call") } } // TestTextToolCall_NoFalsePositive verifies that a normal text response that // does not contain a tool-call pattern is not mistakenly parsed. func TestTextToolCall_NoFalsePositive(t *testing.T) { be := defaultBE() be.respond = func(_ context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) { return textStream("Here is how execute_code works in general."), nil } c := newCore(t, be, nil) c.runtime.Tools = []backend.Tool{{Name: "execute_code", Description: "run code"}} evs := collectEvents(context.Background(), c, "explain tools") errEvs := byRole(evs, "error") if len(errEvs) != 0 { t.Errorf("unexpected error events: %v", errEvs) } assistEvs := byRole(evs, "assistant") if len(assistEvs) == 0 { t.Error("expected assistant response; got none") } } // TestTextToolCall_NoToolsConfigured verifies that when no tools are configured // the detection does not fire (nothing to detect against). func TestTextToolCall_NoToolsConfigured(t *testing.T) { be := defaultBE() be.respond = func(_ context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) { return textStream(`execute_code: steps=[{code: "print('hi')"}]`), nil } c := newCore(t, be, nil) // No tools configured. evs := collectEvents(context.Background(), c, "run something") errEvs := byRole(evs, "error") if len(errEvs) != 0 { t.Errorf("unexpected error with no tools configured: %v", errEvs) } }