yao/agent/context/jsapi_agent_v8_test.go
Max 3a9f32af12 Enhance Agent Context with JSAPI Initialization and Message Handling
- Added initialization for the Agent JSAPI factory to support ctx.agent.* methods, improving agent interaction capabilities.
- Introduced a new agent object in the JSAPI context for calling other agents, enhancing modularity.
- Implemented an OnMessage callback in the context options to handle messages sent via ctx.Send(), allowing for more flexible message processing.
2026-01-25 19:42:56 +08:00

669 lines
18 KiB
Go

package context_test
import (
stdContext "context"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v8 "github.com/yaoapp/gou/runtime/v8"
"github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/testutils"
"github.com/yaoapp/yao/openapi/oauth/types"
// Import assistant package to register AgentAPIFactory
_ "github.com/yaoapp/yao/agent/assistant"
)
// TestAgent_Call_V8 tests basic ctx.agent.Call() functionality with real V8 execution
func TestAgent_Call_V8(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
testutils.Prepare(t)
defer testutils.Clean(t)
// Create authorized info for the context
authorized := &types.AuthorizedInfo{
Subject: "test-user",
UserID: "test-123",
TenantID: "test-tenant",
}
ctx := context.New(stdContext.Background(), authorized, "test-chat-v8-call")
ctx.AssistantID = "tests.agent-caller"
defer ctx.Release()
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
const result = ctx.agent.Call(
"tests.simple-greeting",
[{ role: "user", content: "Hello" }]
);
return {
success: true,
agent_id: result.agent_id,
has_content: result.content && result.content.length > 0,
has_response: result.response !== undefined,
error: result.error || ""
};
} catch (error) {
return { success: false, error: error.message };
}
}`, ctx)
require.NoError(t, err)
result, ok := res.(map[string]interface{})
require.True(t, ok, "Result should be a map")
success, _ := result["success"].(bool)
if !success {
t.Fatalf("Test failed: %v", result["error"])
}
assert.Equal(t, "tests.simple-greeting", result["agent_id"])
hasContent, _ := result["has_content"].(bool)
assert.True(t, hasContent, "Should have content in response")
hasResponse, _ := result["has_response"].(bool)
assert.True(t, hasResponse, "Should have response object")
errorStr, _ := result["error"].(string)
assert.Empty(t, errorStr, "Should not have error")
}
// TestAgent_Call_WithOptions_V8 tests ctx.agent.Call() with options
func TestAgent_Call_WithOptions_V8(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
testutils.Prepare(t)
defer testutils.Clean(t)
authorized := &types.AuthorizedInfo{
Subject: "test-user",
UserID: "test-123",
TenantID: "test-tenant",
}
ctx := context.New(stdContext.Background(), authorized, "test-chat-v8-options")
ctx.AssistantID = "tests.agent-caller"
defer ctx.Release()
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
const result = ctx.agent.Call(
"tests.simple-greeting",
[{ role: "user", content: "Hi there!" }],
{
skip: {
history: true,
trace: true
}
}
);
return {
success: true,
agent_id: result.agent_id,
content: result.content || "",
error: result.error || ""
};
} catch (error) {
return { success: false, error: error.message };
}
}`, ctx)
require.NoError(t, err)
result := res.(map[string]interface{})
if !result["success"].(bool) {
t.Fatalf("Test failed: %v", result["error"])
}
assert.Equal(t, "tests.simple-greeting", result["agent_id"])
assert.NotEmpty(t, result["content"], "Should have content")
}
// TestAgent_All_V8 tests ctx.agent.All() for parallel execution
func TestAgent_All_V8(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
testutils.Prepare(t)
defer testutils.Clean(t)
authorized := &types.AuthorizedInfo{
Subject: "test-user",
UserID: "test-123",
TenantID: "test-tenant",
}
ctx := context.New(stdContext.Background(), authorized, "test-chat-v8-all")
ctx.AssistantID = "tests.agent-caller"
defer ctx.Release()
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
const results = ctx.agent.All([
{
agent: "tests.simple-greeting",
messages: [{ role: "user", content: "Hello from request 1" }]
},
{
agent: "tests.simple-greeting",
messages: [{ role: "user", content: "Hello from request 2" }]
}
]);
return {
success: true,
count: results.length,
first_agent: results[0] ? results[0].agent_id : "",
second_agent: results[1] ? results[1].agent_id : "",
first_has_content: results[0] && results[0].content && results[0].content.length > 0,
second_has_content: results[1] && results[1].content && results[1].content.length > 0,
first_error: results[0] ? (results[0].error || "") : "no result",
second_error: results[1] ? (results[1].error || "") : "no result"
};
} catch (error) {
return { success: false, error: error.message };
}
}`, ctx)
require.NoError(t, err)
result := res.(map[string]interface{})
if !result["success"].(bool) {
t.Fatalf("Test failed: %v", result["error"])
}
assert.Equal(t, float64(2), result["count"])
assert.Equal(t, "tests.simple-greeting", result["first_agent"])
assert.Equal(t, "tests.simple-greeting", result["second_agent"])
assert.True(t, result["first_has_content"].(bool), "First result should have content")
assert.True(t, result["second_has_content"].(bool), "Second result should have content")
assert.Empty(t, result["first_error"], "First result should not have error")
assert.Empty(t, result["second_error"], "Second result should not have error")
}
// TestAgent_Any_V8 tests ctx.agent.Any() returns on first success
func TestAgent_Any_V8(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
testutils.Prepare(t)
defer testutils.Clean(t)
authorized := &types.AuthorizedInfo{
Subject: "test-user",
UserID: "test-123",
TenantID: "test-tenant",
}
ctx := context.New(stdContext.Background(), authorized, "test-chat-v8-any")
ctx.AssistantID = "tests.agent-caller"
defer ctx.Release()
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
const results = ctx.agent.Any([
{
agent: "tests.simple-greeting",
messages: [{ role: "user", content: "Hello" }]
},
{
agent: "tests.simple-greeting",
messages: [{ role: "user", content: "Hi" }]
}
]);
// At least one result should be successful
let hasSuccess = false;
for (const r of results) {
if (r && r.content && !r.error) {
hasSuccess = true;
break;
}
}
return {
success: true,
count: results.length,
has_successful_result: hasSuccess
};
} catch (error) {
return { success: false, error: error.message };
}
}`, ctx)
require.NoError(t, err)
result := res.(map[string]interface{})
if !result["success"].(bool) {
t.Fatalf("Test failed: %v", result["error"])
}
assert.Equal(t, float64(2), result["count"])
assert.True(t, result["has_successful_result"].(bool), "Should have at least one successful result")
}
// TestAgent_Race_V8 tests ctx.agent.Race() returns on first completion
func TestAgent_Race_V8(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
testutils.Prepare(t)
defer testutils.Clean(t)
authorized := &types.AuthorizedInfo{
Subject: "test-user",
UserID: "test-123",
TenantID: "test-tenant",
}
ctx := context.New(stdContext.Background(), authorized, "test-chat-v8-race")
ctx.AssistantID = "tests.agent-caller"
defer ctx.Release()
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
const results = ctx.agent.Race([
{
agent: "tests.simple-greeting",
messages: [{ role: "user", content: "Hello" }]
},
{
agent: "tests.simple-greeting",
messages: [{ role: "user", content: "Hi" }]
}
]);
// At least one result should exist (first to complete)
let hasResult = false;
for (const r of results) {
if (r && (r.content || r.error)) {
hasResult = true;
break;
}
}
return {
success: true,
count: results.length,
has_result: hasResult
};
} catch (error) {
return { success: false, error: error.message };
}
}`, ctx)
require.NoError(t, err)
result := res.(map[string]interface{})
if !result["success"].(bool) {
t.Fatalf("Test failed: %v", result["error"])
}
assert.Equal(t, float64(2), result["count"])
assert.True(t, result["has_result"].(bool), "Should have at least one result")
}
// TestAgent_ErrorHandling_V8 tests error handling when calling non-existent agent
func TestAgent_ErrorHandling_V8(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
testutils.Prepare(t)
defer testutils.Clean(t)
authorized := &types.AuthorizedInfo{
Subject: "test-user",
UserID: "test-123",
TenantID: "test-tenant",
}
ctx := context.New(stdContext.Background(), authorized, "test-chat-v8-error")
ctx.AssistantID = "tests.agent-caller"
defer ctx.Release()
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
const result = ctx.agent.Call(
"non-existent-agent",
[{ role: "user", content: "Hello" }]
);
return {
success: true,
has_error: result.error && result.error.length > 0,
error_message: result.error || ""
};
} catch (error) {
return { success: false, error: error.message };
}
}`, ctx)
require.NoError(t, err)
result := res.(map[string]interface{})
// The call should succeed (no JS exception), but result should contain error
assert.True(t, result["success"].(bool), "JS execution should succeed")
assert.True(t, result["has_error"].(bool), "Result should have error for non-existent agent")
assert.True(t, strings.Contains(result["error_message"].(string), "failed to get agent"), "Error should mention failed to get agent")
}
// TestAgent_EmptyRequests_V8 tests handling of empty requests array
func TestAgent_EmptyRequests_V8(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
testutils.Prepare(t)
defer testutils.Clean(t)
authorized := &types.AuthorizedInfo{
Subject: "test-user",
UserID: "test-123",
TenantID: "test-tenant",
}
ctx := context.New(stdContext.Background(), authorized, "test-chat-v8-empty")
ctx.AssistantID = "tests.agent-caller"
defer ctx.Release()
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
const results = ctx.agent.All([]);
return {
success: true,
count: results.length
};
} catch (error) {
return { success: false, error: error.message };
}
}`, ctx)
require.NoError(t, err)
result := res.(map[string]interface{})
assert.True(t, result["success"].(bool))
assert.Equal(t, float64(0), result["count"])
}
// TestAgent_InvalidArguments_V8 tests error handling for invalid arguments
func TestAgent_InvalidArguments_V8(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
testutils.Prepare(t)
defer testutils.Clean(t)
authorized := &types.AuthorizedInfo{
Subject: "test-user",
UserID: "test-123",
TenantID: "test-tenant",
}
ctx := context.New(stdContext.Background(), authorized, "test-chat-v8-invalid")
ctx.AssistantID = "tests.agent-caller"
defer ctx.Release()
// Test missing arguments
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
// Call with no arguments should throw
ctx.agent.Call();
return { success: false, error: "Should have thrown" };
} catch (error) {
return { success: true, error: error.message };
}
}`, ctx)
require.NoError(t, err)
result := res.(map[string]interface{})
assert.True(t, result["success"].(bool), "Should catch the error")
assert.Contains(t, result["error"].(string), "requires")
}
// ============================================================================
// Callback Tests
// ============================================================================
// TestAgent_Call_WithCallback_V8 tests ctx.agent.Call() with onChunk callback
func TestAgent_Call_WithCallback_V8(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
testutils.Prepare(t)
defer testutils.Clean(t)
authorized := &types.AuthorizedInfo{
Subject: "test-user",
UserID: "test-123",
TenantID: "test-tenant",
}
ctx := context.New(stdContext.Background(), authorized, "test-chat-v8-callback")
ctx.AssistantID = "tests.agent-caller"
defer ctx.Release()
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
const messages = [];
let messageCount = 0;
const result = ctx.agent.Call(
"tests.simple-greeting",
[{ role: "user", content: "Hello" }],
{
onChunk: (msg) => {
// msg is the SSE message object
messageCount++;
messages.push({
type: msg.type,
has_props: msg.props !== undefined
});
return 0; // Continue
}
}
);
return {
success: true,
agent_id: result.agent_id,
has_content: result.content && result.content.length > 0,
message_count: messageCount,
received_messages: messages.slice(0, 5), // First 5 messages
error: result.error || ""
};
} catch (error) {
return { success: false, error: error.message };
}
}`, ctx)
require.NoError(t, err)
result, ok := res.(map[string]interface{})
require.True(t, ok, "Result should be a map")
success, _ := result["success"].(bool)
if !success {
t.Fatalf("Test failed: %v", result["error"])
}
assert.Equal(t, "tests.simple-greeting", result["agent_id"])
// Should have received some messages via callback
messageCount, _ := result["message_count"].(float64)
t.Logf("Received %v messages via callback", messageCount)
assert.Greater(t, messageCount, float64(0), "Should have received messages via callback")
// Check that we received message objects with type and props
receivedMsgs, _ := result["received_messages"].([]interface{})
if len(receivedMsgs) > 0 {
firstMsg := receivedMsgs[0].(map[string]interface{})
t.Logf("First message type: %v", firstMsg["type"])
assert.NotEmpty(t, firstMsg["type"], "Message should have type")
}
}
// TestAgent_Call_WithCallback_Stop_V8 tests that callback can stop streaming
func TestAgent_Call_WithCallback_Stop_V8(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
testutils.Prepare(t)
defer testutils.Clean(t)
authorized := &types.AuthorizedInfo{
Subject: "test-user",
UserID: "test-123",
TenantID: "test-tenant",
}
ctx := context.New(stdContext.Background(), authorized, "test-chat-v8-callback-stop")
ctx.AssistantID = "tests.agent-caller"
defer ctx.Release()
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
let messageCount = 0;
const result = ctx.agent.Call(
"tests.simple-greeting",
[{ role: "user", content: "Hello" }],
{
onChunk: (msg) => {
messageCount++;
// Stop after receiving 3 messages
if (messageCount >= 3) {
return 1; // Stop
}
return 0; // Continue
}
}
);
return {
success: true,
message_count: messageCount,
stopped_early: messageCount <= 5 // Should have stopped early
};
} catch (error) {
return { success: false, error: error.message };
}
}`, ctx)
require.NoError(t, err)
result, ok := res.(map[string]interface{})
require.True(t, ok, "Result should be a map")
success, _ := result["success"].(bool)
if !success {
t.Fatalf("Test failed: %v", result["error"])
}
messageCount, _ := result["message_count"].(float64)
t.Logf("Received %v messages before stopping", messageCount)
// Note: The exact count may vary based on when the stop is processed
}
// TestAgent_All_WithGlobalCallback_V8 tests ctx.agent.All() with global onChunk callback
// Uses channel-based callback handling for V8 thread safety
func TestAgent_All_WithGlobalCallback_V8(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
testutils.Prepare(t)
defer testutils.Clean(t)
authorized := &types.AuthorizedInfo{
Subject: "test-user",
UserID: "test-123",
TenantID: "test-tenant",
}
ctx := context.New(stdContext.Background(), authorized, "test-chat-v8-all-callback")
ctx.AssistantID = "tests.agent-caller"
defer ctx.Release()
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
const messagesByAgent = {};
const results = ctx.agent.All(
[
{
agent: "tests.simple-greeting",
messages: [{ role: "user", content: "Hello from 1" }]
},
{
agent: "tests.simple-greeting",
messages: [{ role: "user", content: "Hello from 2" }]
}
],
{
// Global callback receives agentID, index, and message
onChunk: (agentID, index, msg) => {
const key = agentID + "_" + index;
if (!messagesByAgent[key]) {
messagesByAgent[key] = 0;
}
messagesByAgent[key]++;
return 0;
}
}
);
return {
success: true,
result_count: results.length,
messages_by_agent: messagesByAgent
};
} catch (error) {
return { success: false, error: error.message };
}
}`, ctx)
require.NoError(t, err)
result, ok := res.(map[string]interface{})
require.True(t, ok, "Result should be a map")
success, _ := result["success"].(bool)
if !success {
t.Fatalf("Test failed: %v", result["error"])
}
assert.Equal(t, float64(2), result["result_count"])
// Should have received messages from both agents
messagesByAgent, _ := result["messages_by_agent"].(map[string]interface{})
t.Logf("Messages by agent: %v", messagesByAgent)
// At least one agent should have sent messages
assert.Greater(t, len(messagesByAgent), 0, "Should have received messages from agents")
}