yao/agent/caller/process_test.go
Max 342c2f11a3 Refactor agent call handling and enhance request structures
- Update `forceSkipForSubAgent` to respect caller's `skip.output` setting, allowing for silent internal worker agent execution.
- Refactor error handling in `callAgentWithContext` to utilize a new `NewResult` function for consistent result construction.
- Introduce `ProcessCallRequest` structure for improved agent call parameters, including a default timeout constant for process calls.
2026-02-14 11:30:20 +08:00

214 lines
6.3 KiB
Go

package caller_test
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yaoapp/yao/agent/caller"
agentContext "github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/openapi/oauth/types"
)
// --- NewHeadlessContext tests ---
func TestNewHeadlessContext_Basic(t *testing.T) {
authInfo := &types.AuthorizedInfo{
TeamID: "team-123",
UserID: "user-456",
}
req := &caller.ProcessCallRequest{
AssistantID: "yao.keeper.classify",
Messages: []map[string]interface{}{
{"role": "user", "content": "hello"},
},
Locale: "zh-CN",
}
ctx, opts := caller.NewHeadlessContext(context.Background(), authInfo, req)
defer ctx.Release()
assert.Equal(t, "yao.keeper.classify", ctx.AssistantID)
assert.Equal(t, agentContext.RefererProcess, ctx.Referer)
assert.Equal(t, "zh-CN", ctx.Locale)
assert.NotEmpty(t, ctx.ChatID) // auto-generated
require.NotNil(t, opts)
require.NotNil(t, opts.Skip)
assert.True(t, opts.Skip.Output, "skip.output must be forced true for headless context")
assert.True(t, opts.Skip.History, "skip.history must be forced true for headless context")
assert.Empty(t, opts.Connector)
}
func TestNewHeadlessContext_WithModel(t *testing.T) {
req := &caller.ProcessCallRequest{
AssistantID: "test.agent",
Messages: []map[string]interface{}{{"role": "user", "content": "hi"}},
Model: "deepseek.v3",
}
ctx, opts := caller.NewHeadlessContext(context.Background(), nil, req)
defer ctx.Release()
assert.Equal(t, "deepseek.v3", opts.Connector)
}
func TestNewHeadlessContext_WithChatID(t *testing.T) {
req := &caller.ProcessCallRequest{
AssistantID: "test.agent",
Messages: []map[string]interface{}{{"role": "user", "content": "hi"}},
ChatID: "custom-chat-id",
}
ctx, _ := caller.NewHeadlessContext(context.Background(), nil, req)
defer ctx.Release()
assert.Equal(t, "custom-chat-id", ctx.ChatID)
}
func TestNewHeadlessContext_ForceSkipOverridesUserSkip(t *testing.T) {
req := &caller.ProcessCallRequest{
AssistantID: "test.agent",
Messages: []map[string]interface{}{{"role": "user", "content": "hi"}},
Skip: &agentContext.Skip{Output: false, History: false, Trace: true},
}
_, opts := caller.NewHeadlessContext(context.Background(), nil, req)
// Output and History must be forced true regardless of user input
assert.True(t, opts.Skip.Output, "skip.output must be forced true")
assert.True(t, opts.Skip.History, "skip.history must be forced true")
// User-specified skip.trace should be preserved
assert.True(t, opts.Skip.Trace, "skip.trace should be preserved from user input")
}
func TestNewHeadlessContext_WithMetadata(t *testing.T) {
req := &caller.ProcessCallRequest{
AssistantID: "test.agent",
Messages: []map[string]interface{}{{"role": "user", "content": "hi"}},
Metadata: map[string]interface{}{"key": "value"},
Route: "/test",
}
ctx, _ := caller.NewHeadlessContext(context.Background(), nil, req)
defer ctx.Release()
assert.Equal(t, "value", ctx.Metadata["key"])
assert.Equal(t, "/test", ctx.Route)
}
func TestNewHeadlessContext_WithTimeout(t *testing.T) {
req := &caller.ProcessCallRequest{
AssistantID: "test.agent",
Messages: []map[string]interface{}{{"role": "user", "content": "hi"}},
Timeout: 30,
}
// Pass a context with timeout to verify it propagates
ctx, _ := caller.NewHeadlessContext(context.Background(), nil, req)
defer ctx.Release()
// Timeout field is consumed by processAgentCall, not NewHeadlessContext.
// Here we just verify the field is correctly set in the struct.
assert.Equal(t, 30, req.Timeout)
}
func TestProcessCallRequest_DefaultTimeout(t *testing.T) {
req := &caller.ProcessCallRequest{
AssistantID: "test.agent",
Messages: []map[string]interface{}{{"role": "user", "content": "hi"}},
}
// When Timeout is 0 (zero value), the default should be used
assert.Equal(t, 0, req.Timeout, "zero value means use default")
assert.Equal(t, 600, caller.DefaultProcessTimeout, "default timeout should be 600 seconds")
}
// --- ParseMessages tests ---
func TestParseMessages_Basic(t *testing.T) {
raw := []map[string]interface{}{
{"role": "user", "content": "hello"},
{"role": "assistant", "content": "hi there"},
}
messages := caller.ParseMessages(raw)
require.Len(t, messages, 2)
assert.Equal(t, agentContext.MessageRole("user"), messages[0].Role)
assert.Equal(t, "hello", messages[0].Content)
assert.Equal(t, agentContext.MessageRole("assistant"), messages[1].Role)
assert.Equal(t, "hi there", messages[1].Content)
}
func TestParseMessages_WithOptionalFields(t *testing.T) {
name := "test-name"
raw := []map[string]interface{}{
{
"role": "tool",
"content": "result",
"name": name,
"tool_call_id": "tc-1",
},
}
messages := caller.ParseMessages(raw)
require.Len(t, messages, 1)
msg := messages[0]
assert.Equal(t, agentContext.MessageRole("tool"), msg.Role)
require.NotNil(t, msg.Name)
assert.Equal(t, name, *msg.Name)
require.NotNil(t, msg.ToolCallID)
assert.Equal(t, "tc-1", *msg.ToolCallID)
}
func TestParseMessages_Empty(t *testing.T) {
messages := caller.ParseMessages(nil)
assert.Empty(t, messages)
}
// --- NewResult tests ---
func TestNewResult_Success(t *testing.T) {
resp := &agentContext.Response{
Completion: &agentContext.CompletionResponse{
Content: "answer text",
},
}
result := caller.NewResult("test.agent", resp, nil)
assert.Equal(t, "test.agent", result.AgentID)
assert.Equal(t, "answer text", result.Content)
assert.Empty(t, result.Error)
assert.NotNil(t, result.Response)
}
func TestNewResult_WithError(t *testing.T) {
result := caller.NewResult("test.agent", nil, errors.New("something failed"))
assert.Equal(t, "test.agent", result.AgentID)
assert.Equal(t, "something failed", result.Error)
assert.Empty(t, result.Content)
assert.Nil(t, result.Response)
}
func TestNewResult_NilResponse(t *testing.T) {
result := caller.NewResult("test.agent", nil, nil)
assert.Equal(t, "test.agent", result.AgentID)
assert.Empty(t, result.Content)
assert.Empty(t, result.Error)
assert.Nil(t, result.Response)
}
func TestNewResult_NilCompletion(t *testing.T) {
resp := &agentContext.Response{Completion: nil}
result := caller.NewResult("test.agent", resp, nil)
assert.Empty(t, result.Content, "content should be empty when completion is nil")
assert.NotNil(t, result.Response)
}