yao/agent/context/jsapi_output_test.go
Max 17b71ee4cc Refactor message handling to support individual message lifecycle events
- Replaced group-related event types with message-specific types, enhancing clarity in event management.
- Updated stream handling functions to utilize new message start and end events, improving the granularity of message tracking.
- Refactored context management to integrate a context-scoped ID generator for unique message identifiers.
- Removed deprecated group handling methods and streamlined message processing logic.
- Updated tests and documentation to reflect changes in message handling and ensure proper functionality.
2025-11-26 19:03:47 +08:00

374 lines
8.6 KiB
Go

package context
import (
"bytes"
"context"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
v8 "github.com/yaoapp/gou/runtime/v8"
"github.com/yaoapp/yao/agent/output/message"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/test"
)
// mockResponseWriter is a mock implementation of http.ResponseWriter for testing
type mockResponseWriter struct {
headers http.Header
buffer *bytes.Buffer
status int
}
func newMockResponseWriter() *mockResponseWriter {
return &mockResponseWriter{
headers: make(http.Header),
buffer: &bytes.Buffer{},
status: http.StatusOK,
}
}
func (m *mockResponseWriter) Header() http.Header {
return m.headers
}
func (m *mockResponseWriter) Write(b []byte) (int, error) {
return m.buffer.Write(b)
}
func (m *mockResponseWriter) WriteHeader(statusCode int) {
m.status = statusCode
}
// TestJsValueSend test the Send method on Context
func TestJsValueSend(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
cxt := &Context{
ChatID: "test-chat-id",
AssistantID: "test-assistant-id",
Context: context.Background(),
Accept: "standard",
Locale: "en",
Writer: newMockResponseWriter(),
IDGenerator: message.NewIDGenerator(),
}
// Test sending string shorthand
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
// Send simple string
ctx.Send("Hello World");
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
}`, cxt)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
result, ok := res.(map[string]interface{})
if !ok {
t.Fatalf("Expected map result, got %T", res)
}
assert.Equal(t, true, result["success"], "Send string should succeed")
// Test sending message object
res, err = v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
// Send message object
ctx.Send({
type: "text",
props: {
content: "Hello from JavaScript"
},
id: "msg_123",
metadata: {
timestamp: Date.now(),
sequence: 1
}
});
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
}`, cxt)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
result, ok = res.(map[string]interface{})
if !ok {
t.Fatalf("Expected map result, got %T", res)
}
assert.Equal(t, true, result["success"], "Send message object should succeed")
}
// TestJsValueSendGroup test the SendGroup method on Context
// TestJsValueSendDeltaUpdates test delta updates in Send
func TestJsValueSendDeltaUpdates(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
cxt := &Context{
ChatID: "test-chat-id",
AssistantID: "test-assistant-id",
Context: context.Background(),
Accept: "standard",
Locale: "en",
Writer: newMockResponseWriter(),
IDGenerator: message.NewIDGenerator(),
}
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
// Send initial message
ctx.Send({
type: "text",
props: { content: "Hello" },
id: "msg_1",
delta: false
});
// Send delta update (append)
ctx.Send({
type: "text",
props: { content: " World" },
id: "msg_1",
delta: true,
delta_path: "content",
delta_action: "append"
});
// Send completion (no done field needed)
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
}`, cxt)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
result, ok := res.(map[string]interface{})
if !ok {
t.Fatalf("Expected map result, got %T", res)
}
assert.Equal(t, true, result["success"], "Delta updates should succeed")
}
// TestJsValueSendMultipleTypes test sending different message types
func TestJsValueSendMultipleTypes(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
cxt := &Context{
ChatID: "test-chat-id",
AssistantID: "test-assistant-id",
Context: context.Background(),
Accept: "standard",
Locale: "en",
Writer: newMockResponseWriter(),
IDGenerator: message.NewIDGenerator(),
}
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
// Text message
ctx.Send({
type: "text",
props: { content: "Hello" }
});
// Thinking message
ctx.Send({
type: "thinking",
props: { content: "Let me think..." }
});
// Loading message
ctx.Send({
type: "loading",
props: { message: "Processing..." }
});
// Tool call message
ctx.Send({
type: "tool_call",
props: {
id: "call_123",
name: "get_weather",
arguments: '{"location": "San Francisco"}'
}
});
// Error message
ctx.Send({
type: "error",
props: {
message: "Something went wrong",
code: "ERR_500"
}
});
// Image message
ctx.Send({
type: "image",
props: {
url: "https://example.com/image.jpg",
alt: "Example image",
width: 800,
height: 600
}
});
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
}`, cxt)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
result, ok := res.(map[string]interface{})
if !ok {
t.Fatalf("Expected map result, got %T", res)
}
assert.Equal(t, true, result["success"], "Multiple message types should succeed")
}
// TestJsValueSendErrorHandling test error handling in Send
func TestJsValueSendErrorHandling(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
cxt := &Context{
ChatID: "test-chat-id",
AssistantID: "test-assistant-id",
Context: context.Background(),
Accept: "standard",
Locale: "en",
Writer: newMockResponseWriter(),
IDGenerator: message.NewIDGenerator(),
}
// Test invalid argument - no arguments
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
ctx.Send();
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
}`, cxt)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
result, ok := res.(map[string]interface{})
if !ok {
t.Fatalf("Expected map result, got %T", res)
}
assert.Equal(t, false, result["success"], "Send without arguments should fail")
assert.Contains(t, result["error"], "Send requires a message argument", "Error should mention missing message")
}
// TestJsValueSendGroupErrorHandling test error handling in SendGroup
// TestJsValueSendWithCUIAccept test Send with CUI accept types
func TestJsValueSendWithCUIAccept(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
acceptTypes := []string{"cui-web", "cui-native", "cui-desktop"}
for _, acceptType := range acceptTypes {
t.Run(acceptType, func(t *testing.T) {
cxt := &Context{
ChatID: "test-chat-id",
AssistantID: "test-assistant-id",
Context: context.Background(),
Accept: Accept(acceptType),
Locale: "en",
Writer: newMockResponseWriter(),
}
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
ctx.Send({
type: "text",
props: { content: "Hello CUI" }
});
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
}`, cxt)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
result, ok := res.(map[string]interface{})
if !ok {
t.Fatalf("Expected map result, got %T", res)
}
assert.Equal(t, true, result["success"], "Send with "+acceptType+" should succeed")
})
}
}
// TestJsValueSendGroupWithMetadata test SendGroup with various metadata
// TestJsValueSendChainedCalls test chained Send calls
func TestJsValueSendChainedCalls(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
cxt := &Context{
ChatID: "test-chat-id",
AssistantID: "test-assistant-id",
Context: context.Background(),
Accept: "standard",
Locale: "en",
Writer: newMockResponseWriter(),
IDGenerator: message.NewIDGenerator(),
}
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
// Multiple sequential sends (each auto-flushes)
ctx.Send("Step 1");
ctx.Send("Step 2");
ctx.Send("Step 3");
ctx.Send("Step 4");
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
}`, cxt)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
result, ok := res.(map[string]interface{})
if !ok {
t.Fatalf("Expected map result, got %T", res)
}
assert.Equal(t, true, result["success"], "Chained Send calls should succeed")
}