132 lines
4.0 KiB
Go
132 lines
4.0 KiB
Go
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, []backend.ContentBlock, error) {
|
|
execCalled = true
|
|
if name != "call_tool" {
|
|
t.Errorf("expected tool name 'call_tool', got %q", name)
|
|
}
|
|
return "file contents here", nil, 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, []backend.ContentBlock, 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, 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)
|
|
}
|
|
}
|