- 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.
669 lines
18 KiB
Go
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")
|
|
}
|