yao/agent/context/jsapi_llm_v8_test.go
Max 02e813af0b Update Dependencies and Enhance Agent Context with Forked Context Support
- Updated `logrus` dependency from v1.9.3 to v1.9.4 and `golang.org/x/sys` from v0.38.0 to v0.40.0 for improved functionality and security.
- Introduced a `Fork` method in the agent context to create child contexts for concurrent agent and LLM calls, preventing race conditions on shared state during batch operations.
- Enhanced the `Orchestrator` methods to utilize forked contexts, ensuring independent execution of agent calls without interference.
2026-01-25 20:32:02 +08:00

497 lines
14 KiB
Go

package context_test
import (
stdContext "context"
"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 LlmAPIFactory
_ "github.com/yaoapp/yao/agent/assistant"
)
// TestLlm_Stream_V8 tests basic ctx.llm.Stream functionality with real V8 execution
func TestLlm_Stream_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",
}
// Create a context
ctx := context.New(stdContext.Background(), authorized, "test-chat-llm-stream")
ctx.AssistantID = "tests.simple-greeting"
defer ctx.Release()
// Test basic Stream call with real connector
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
const result = ctx.llm.Stream("gpt-4o-mini", [
{ role: "user", content: "Say hello in one word" }
], {
temperature: 0.1,
max_tokens: 10
});
return {
success: true,
connector: result.connector,
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)
require.NotNil(t, res)
result, ok := res.(map[string]interface{})
require.True(t, ok, "result should be a map")
success, _ := result["success"].(bool)
if !success {
t.Logf("Test result: %v", result)
}
require.True(t, success, "Test should succeed, error: %v", result["error"])
assert.Equal(t, "gpt-4o-mini", result["connector"])
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")
}
// TestLlm_Stream_WithCallback_V8 tests ctx.llm.Stream with onChunk callback
func TestLlm_Stream_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-llm-callback")
ctx.AssistantID = "tests.simple-greeting"
defer ctx.Release()
// Test Stream call with callback
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
let callbackCount = 0;
let receivedTypes = [];
const result = ctx.llm.Stream("gpt-4o-mini", [
{ role: "user", content: "Say hi" }
], {
temperature: 0.1,
max_tokens: 10,
onChunk: function(msg) {
callbackCount++;
if (msg && msg.type) {
receivedTypes.push(msg.type);
}
return 0; // Continue
}
});
return {
success: true,
connector: result.connector,
callbackCount: callbackCount,
receivedTypes: receivedTypes,
has_content: result.content && result.content.length > 0,
error: result.error || ""
};
} catch (error) {
return { success: false, error: error.message };
}
}`, ctx)
require.NoError(t, err)
require.NotNil(t, res)
result, ok := res.(map[string]interface{})
require.True(t, ok, "result should be a map")
success, _ := result["success"].(bool)
if !success {
t.Logf("Test result: %v", result)
}
require.True(t, success, "Test should succeed, error: %v", result["error"])
// Callback should have been called at least once
callbackCount, _ := result["callbackCount"].(float64)
assert.Greater(t, callbackCount, float64(0), "Callback should be called at least once")
}
// TestLlm_All_V8 tests ctx.llm.All with multiple connectors
func TestLlm_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-llm-all")
ctx.AssistantID = "tests.simple-greeting"
defer ctx.Release()
// Test All with multiple requests to same connector (different prompts)
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
const results = ctx.llm.All([
{
connector: "gpt-4o-mini",
messages: [{ role: "user", content: "Say 'one'" }],
options: { temperature: 0.1, max_tokens: 5 }
},
{
connector: "gpt-4o-mini",
messages: [{ role: "user", content: "Say 'two'" }],
options: { temperature: 0.1, max_tokens: 5 }
}
]);
return {
success: true,
count: results.length,
results: results.map(r => ({
connector: r.connector,
has_content: r.content && r.content.length > 0,
error: r.error || ""
}))
};
} catch (error) {
return { success: false, error: error.message };
}
}`, ctx)
require.NoError(t, err)
require.NotNil(t, res)
result, ok := res.(map[string]interface{})
require.True(t, ok, "result should be a map")
success, _ := result["success"].(bool)
if !success {
t.Logf("Test result: %v", result)
}
require.True(t, success, "Test should succeed, error: %v", result["error"])
// Should have 2 results
count, _ := result["count"].(float64)
assert.Equal(t, float64(2), count, "Should have 2 results")
// Check individual results
results, _ := result["results"].([]interface{})
require.Len(t, results, 2)
for i, r := range results {
rMap, _ := r.(map[string]interface{})
hasContent, _ := rMap["has_content"].(bool)
assert.True(t, hasContent, "Result %d should have content", i)
errorStr, _ := rMap["error"].(string)
assert.Empty(t, errorStr, "Result %d should not have error", i)
}
}
// TestLlm_All_WithCallback_V8 tests ctx.llm.All with global callback
func TestLlm_All_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-llm-all-callback")
ctx.AssistantID = "tests.simple-greeting"
defer ctx.Release()
// Test All with global callback
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
let callbackCount = 0;
let indexesSeen = new Set();
const results = ctx.llm.All([
{
connector: "gpt-4o-mini",
messages: [{ role: "user", content: "Say 'A'" }],
options: { temperature: 0.1, max_tokens: 5 }
},
{
connector: "gpt-4o-mini",
messages: [{ role: "user", content: "Say 'B'" }],
options: { temperature: 0.1, max_tokens: 5 }
}
], {
onChunk: function(connectorID, index, msg) {
callbackCount++;
indexesSeen.add(index);
return 0;
}
});
return {
success: true,
count: results.length,
callbackCount: callbackCount,
indexesSeen: indexesSeen.size
};
} catch (error) {
return { success: false, error: error.message };
}
}`, ctx)
require.NoError(t, err)
require.NotNil(t, res)
result, ok := res.(map[string]interface{})
require.True(t, ok, "result should be a map")
success, _ := result["success"].(bool)
if !success {
t.Logf("Test result: %v", result)
}
require.True(t, success, "Test should succeed, error: %v", result["error"])
// Callback should have been called
callbackCount, _ := result["callbackCount"].(float64)
assert.Greater(t, callbackCount, float64(0), "Callback should be called")
// Should have seen at least one index (both requests may complete so fast that only one is tracked)
// Note: Due to V8 thread safety with channel-based approach, callbacks are serialized
indexesSeen, _ := result["indexesSeen"].(float64)
assert.GreaterOrEqual(t, indexesSeen, float64(1), "Should have seen callbacks from at least one request")
}
// TestLlm_Any_V8 tests ctx.llm.Any - returns first success
func TestLlm_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-llm-any")
ctx.AssistantID = "tests.simple-greeting"
defer ctx.Release()
// Test Any - should return first successful result
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
const results = ctx.llm.Any([
{
connector: "gpt-4o-mini",
messages: [{ role: "user", content: "Say 'hello'" }],
options: { temperature: 0.1, max_tokens: 5 }
},
{
connector: "gpt-4o-mini",
messages: [{ role: "user", content: "Say 'world'" }],
options: { temperature: 0.1, max_tokens: 5 }
}
]);
// Any returns array with single successful result
return {
success: true,
count: results.length,
first_has_content: results[0] && results[0].content && results[0].content.length > 0,
first_error: results[0] ? (results[0].error || "") : "no result"
};
} catch (error) {
return { success: false, error: error.message };
}
}`, ctx)
require.NoError(t, err)
require.NotNil(t, res)
result, ok := res.(map[string]interface{})
require.True(t, ok, "result should be a map")
success, _ := result["success"].(bool)
if !success {
t.Logf("Test result: %v", result)
}
require.True(t, success, "Test should succeed, error: %v", result["error"])
// Any returns single result on success
count, _ := result["count"].(float64)
assert.Equal(t, float64(1), count, "Should have 1 result (first success)")
firstHasContent, _ := result["first_has_content"].(bool)
assert.True(t, firstHasContent, "First result should have content")
firstError, _ := result["first_error"].(string)
assert.Empty(t, firstError, "First result should not have error")
}
// TestLlm_Race_V8 tests ctx.llm.Race - returns first completion
func TestLlm_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-llm-race")
ctx.AssistantID = "tests.simple-greeting"
defer ctx.Release()
// Test Race - should return first completed result
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
const results = ctx.llm.Race([
{
connector: "gpt-4o-mini",
messages: [{ role: "user", content: "Say 'fast'" }],
options: { temperature: 0.1, max_tokens: 5 }
},
{
connector: "gpt-4o-mini",
messages: [{ role: "user", content: "Say 'slow'" }],
options: { temperature: 0.1, max_tokens: 5 }
}
]);
// Race returns array with single result (first to complete)
return {
success: true,
count: results.length,
has_result: results[0] !== undefined,
first_connector: results[0] ? results[0].connector : "",
first_has_content: results[0] && results[0].content && results[0].content.length > 0
};
} catch (error) {
return { success: false, error: error.message };
}
}`, ctx)
require.NoError(t, err)
require.NotNil(t, res)
result, ok := res.(map[string]interface{})
require.True(t, ok, "result should be a map")
success, _ := result["success"].(bool)
if !success {
t.Logf("Test result: %v", result)
}
require.True(t, success, "Test should succeed, error: %v", result["error"])
// Race returns single result
count, _ := result["count"].(float64)
assert.Equal(t, float64(1), count, "Should have 1 result (first to complete)")
hasResult, _ := result["has_result"].(bool)
assert.True(t, hasResult, "Should have a result")
firstConnector, _ := result["first_connector"].(string)
assert.Equal(t, "gpt-4o-mini", firstConnector, "First result should have connector")
}
// TestLlm_Stream_InvalidConnector_V8 tests error handling for invalid connector
func TestLlm_Stream_InvalidConnector_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-llm-invalid")
ctx.AssistantID = "tests.simple-greeting"
defer ctx.Release()
// Test Stream with invalid connector
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
const result = ctx.llm.Stream("invalid-connector-that-does-not-exist", [
{ role: "user", content: "Hello" }
]);
return {
has_error: result.error && result.error.length > 0,
error: result.error || ""
};
} catch (error) {
return { has_error: true, error: error.message };
}
}`, ctx)
require.NoError(t, err)
require.NotNil(t, res)
result, ok := res.(map[string]interface{})
require.True(t, ok, "result should be a map")
hasError, _ := result["has_error"].(bool)
assert.True(t, hasError, "Should have error for invalid connector")
}