- 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.
497 lines
14 KiB
Go
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")
|
|
}
|