- 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.
214 lines
6.3 KiB
Go
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)
|
|
}
|