- Updated the `ChatBuffer` to support streaming messages, allowing for content to be appended and finalized with `SendStream` and `End` methods. - Modified the `AddAssistantMessage` method to include a message ID, improving message tracking and retrieval. - Implemented new methods for appending content to streaming messages and completing them, ensuring accurate message storage and event handling. - Revised tests to validate the new streaming functionality and ensure proper integration with existing message handling processes. - Updated `CHAT_STORAGE_DESIGN.md` to reflect changes in message storage and indexing, including unique constraints for message IDs within requests.
1343 lines
34 KiB
Go
1343 lines
34 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")
|
|
}
|
|
|
|
// TestJsValueIDGenerators test ID generator methods
|
|
func TestJsValueIDGenerators(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 {
|
|
// Test MessageID generator
|
|
const msgId1 = ctx.MessageID();
|
|
const msgId2 = ctx.MessageID();
|
|
|
|
// Test BlockID generator
|
|
const blockId1 = ctx.BlockID();
|
|
const blockId2 = ctx.BlockID();
|
|
|
|
// Test ThreadID generator
|
|
const threadId1 = ctx.ThreadID();
|
|
const threadId2 = ctx.ThreadID();
|
|
|
|
// Verify IDs are strings and sequential
|
|
if (typeof msgId1 !== 'string' || typeof msgId2 !== 'string') {
|
|
throw new Error('MessageID should return string');
|
|
}
|
|
if (typeof blockId1 !== 'string' || typeof blockId2 !== 'string') {
|
|
throw new Error('BlockID should return string');
|
|
}
|
|
if (typeof threadId1 !== 'string' || typeof threadId2 !== 'string') {
|
|
throw new Error('ThreadID should return string');
|
|
}
|
|
|
|
// Verify they follow the pattern (M1, M2, B1, B2, T1, T2)
|
|
if (!msgId1.startsWith('M') || !msgId2.startsWith('M')) {
|
|
throw new Error('MessageID should start with M');
|
|
}
|
|
if (!blockId1.startsWith('B') || !blockId2.startsWith('B')) {
|
|
throw new Error('BlockID should start with B');
|
|
}
|
|
if (!threadId1.startsWith('T') || !threadId2.startsWith('T')) {
|
|
throw new Error('ThreadID should start with T');
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
msgId1: msgId1,
|
|
msgId2: msgId2,
|
|
blockId1: blockId1,
|
|
blockId2: blockId2,
|
|
threadId1: threadId1,
|
|
threadId2: threadId2
|
|
};
|
|
} 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"], "ID generators should succeed")
|
|
}
|
|
|
|
// TestJsValueSendWithBlockID test Send with block_id parameter
|
|
func TestJsValueSendWithBlockID(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 {
|
|
// Generate block ID manually
|
|
const blockId = ctx.BlockID();
|
|
|
|
// Send multiple messages with same block ID
|
|
const msg1 = ctx.Send("Message 1", blockId);
|
|
const msg2 = ctx.Send("Message 2", blockId);
|
|
const msg3 = ctx.Send("Message 3", blockId);
|
|
|
|
// Send message with block_id in object (higher priority)
|
|
const msg4 = ctx.Send({
|
|
type: "text",
|
|
props: { content: "Message 4" },
|
|
block_id: "B_custom"
|
|
}, blockId); // blockId parameter should be ignored
|
|
|
|
return {
|
|
success: true,
|
|
msg1: msg1,
|
|
msg2: msg2,
|
|
msg3: msg3,
|
|
msg4: msg4,
|
|
blockId: blockId
|
|
};
|
|
} 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 blockId should succeed")
|
|
}
|
|
|
|
// TestJsValueReplace test ctx.Replace method
|
|
func TestJsValueReplace(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
|
|
const msgId = ctx.Send("Initial content");
|
|
|
|
// Replace with new content
|
|
ctx.Replace(msgId, "Updated content");
|
|
|
|
// Replace with object
|
|
ctx.Replace(msgId, {
|
|
type: "text",
|
|
props: { content: "Final content" }
|
|
});
|
|
|
|
return { success: true, msgId: msgId };
|
|
} 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"], "Replace should succeed")
|
|
}
|
|
|
|
// TestJsValueAppend test ctx.Append method
|
|
func TestJsValueAppend(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
|
|
const msgId = ctx.Send("Hello");
|
|
|
|
// Append to default path
|
|
ctx.Append(msgId, " World");
|
|
ctx.Append(msgId, "!");
|
|
|
|
// Append to specific path
|
|
const msgId2 = ctx.Send({
|
|
type: "data",
|
|
props: { content: "Line 1\n" }
|
|
});
|
|
ctx.Append(msgId2, "Line 2\n", "props.content");
|
|
ctx.Append(msgId2, "Line 3\n", "props.content");
|
|
|
|
return { success: true, msgId: msgId, msgId2: msgId2 };
|
|
} 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"], "Append should succeed")
|
|
}
|
|
|
|
// TestJsValueMerge test ctx.Merge method
|
|
func TestJsValueMerge(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 with object
|
|
const msgId = ctx.Send({
|
|
type: "status",
|
|
props: {
|
|
status: "running",
|
|
progress: 0,
|
|
started: true
|
|
}
|
|
});
|
|
|
|
// Merge updates (keeps other fields)
|
|
ctx.Merge(msgId, {
|
|
type: "status",
|
|
props: { progress: 50 }
|
|
}, "props");
|
|
ctx.Merge(msgId, {
|
|
type: "status",
|
|
props: { progress: 100, status: "completed" }
|
|
}, "props");
|
|
|
|
return { success: true, msgId: msgId };
|
|
} 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)
|
|
}
|
|
if !result["success"].(bool) {
|
|
t.Logf("Error: %v", result["error"])
|
|
}
|
|
assert.Equal(t, true, result["success"], "Merge should succeed")
|
|
}
|
|
|
|
// TestJsValueSet test ctx.Set method
|
|
func TestJsValueSet(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
|
|
const msgId = ctx.Send({
|
|
type: "result",
|
|
props: { content: "Initial" }
|
|
});
|
|
|
|
// Set new fields
|
|
ctx.Set(msgId, {
|
|
type: "result",
|
|
props: { status: "success" }
|
|
}, "props.status");
|
|
ctx.Set(msgId, {
|
|
type: "result",
|
|
props: { timestamp: Date.now() }
|
|
}, "props.timestamp");
|
|
ctx.Set(msgId, {
|
|
type: "result",
|
|
props: { metadata: { duration: 1500 } }
|
|
}, "props.metadata");
|
|
|
|
return { success: true, msgId: msgId };
|
|
} 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)
|
|
}
|
|
if !result["success"].(bool) {
|
|
t.Logf("Error: %v", result["error"])
|
|
}
|
|
assert.Equal(t, true, result["success"], "Set should succeed")
|
|
}
|
|
|
|
// TestJsValueBlockIDInheritance test that delta operations inherit block_id
|
|
func TestJsValueBlockIDInheritance(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 message with block_id
|
|
const blockId = ctx.BlockID();
|
|
const msgId = ctx.Send("Initial message", blockId);
|
|
|
|
// Delta operations should inherit block_id automatically
|
|
ctx.Append(msgId, " appended");
|
|
ctx.Replace(msgId, "Replaced message");
|
|
ctx.Merge(msgId, {
|
|
type: "text",
|
|
props: { status: "done" }
|
|
}, "props");
|
|
ctx.Set(msgId, {
|
|
type: "text",
|
|
props: { state: "final" }
|
|
}, "props.state");
|
|
|
|
return { success: true, msgId: msgId, blockId: blockId };
|
|
} 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)
|
|
}
|
|
if !result["success"].(bool) {
|
|
t.Logf("Error: %v", result["error"])
|
|
}
|
|
assert.Equal(t, true, result["success"], "Delta operations should inherit block_id")
|
|
}
|
|
|
|
// TestJsValueEndBlock tests the EndBlock method on Context
|
|
func TestJsValueEndBlock(t *testing.T) {
|
|
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
// Setup mock writer
|
|
mockWriter := newMockResponseWriter()
|
|
|
|
// Use New() to properly initialize messageMetadata
|
|
cxt := New(context.Background(), nil, "test-chat-id")
|
|
cxt.AssistantID = "test-assistant-id"
|
|
cxt.Accept = AcceptWebCUI
|
|
cxt.Locale = "en"
|
|
cxt.Writer = mockWriter
|
|
|
|
// Test EndBlock method
|
|
res, err := v8.Call(v8.CallOptions{}, `
|
|
function test(ctx) {
|
|
try {
|
|
// Create a block and send messages
|
|
const block_id = ctx.BlockID(); // "B1"
|
|
|
|
ctx.Send("Message 1", block_id);
|
|
ctx.Send("Message 2", block_id);
|
|
ctx.Send("Message 3", block_id);
|
|
|
|
// End the block manually
|
|
ctx.EndBlock(block_id);
|
|
|
|
return { success: true, block_id: block_id };
|
|
} 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)
|
|
}
|
|
if !result["success"].(bool) {
|
|
t.Logf("Error: %v", result["error"])
|
|
}
|
|
assert.Equal(t, true, result["success"], "EndBlock should work correctly")
|
|
|
|
// Verify that block_end event was sent
|
|
output := mockWriter.buffer.String()
|
|
assert.Contains(t, output, "block_end", "Output should contain block_end event")
|
|
}
|
|
|
|
// TestJsValueSendStream tests the SendStream method on Context
|
|
func TestJsValueSendStream(t *testing.T) {
|
|
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
// Setup mock writer
|
|
mockWriter := newMockResponseWriter()
|
|
|
|
// Use New() to properly initialize messageMetadata
|
|
cxt := New(context.Background(), nil, "test-chat-id")
|
|
cxt.AssistantID = "test-assistant-id"
|
|
cxt.Accept = AcceptWebCUI
|
|
cxt.Locale = "en"
|
|
cxt.Writer = mockWriter
|
|
|
|
// Test SendStream method
|
|
res, err := v8.Call(v8.CallOptions{}, `
|
|
function test(ctx) {
|
|
try {
|
|
// Start a streaming message
|
|
const msgId = ctx.SendStream({
|
|
type: "text",
|
|
props: { content: "Initial content" }
|
|
});
|
|
|
|
// Verify msgId is returned
|
|
if (typeof msgId !== 'string' || msgId === '') {
|
|
throw new Error('SendStream should return a message ID');
|
|
}
|
|
|
|
return { success: true, msgId: msgId };
|
|
} 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)
|
|
}
|
|
if !result["success"].(bool) {
|
|
t.Logf("Error: %v", result["error"])
|
|
}
|
|
assert.Equal(t, true, result["success"], "SendStream should work correctly")
|
|
|
|
// Verify message_start was sent but NOT message_end
|
|
output := mockWriter.buffer.String()
|
|
assert.Contains(t, output, "message_start", "Output should contain message_start event")
|
|
assert.NotContains(t, output, "message_end", "Output should NOT contain message_end event (streaming)")
|
|
}
|
|
|
|
// TestJsValueSendStreamWithBlockID tests SendStream with block_id parameter
|
|
func TestJsValueSendStreamWithBlockID(t *testing.T) {
|
|
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
mockWriter := newMockResponseWriter()
|
|
|
|
cxt := New(context.Background(), nil, "test-chat-id")
|
|
cxt.AssistantID = "test-assistant-id"
|
|
cxt.Accept = AcceptWebCUI
|
|
cxt.Locale = "en"
|
|
cxt.Writer = mockWriter
|
|
|
|
res, err := v8.Call(v8.CallOptions{}, `
|
|
function test(ctx) {
|
|
try {
|
|
// Generate block ID
|
|
const blockId = ctx.BlockID();
|
|
|
|
// Start streaming with block_id
|
|
const msgId = ctx.SendStream({
|
|
type: "text",
|
|
props: { content: "Streaming with block" },
|
|
block_id: blockId
|
|
});
|
|
|
|
return { success: true, msgId: msgId, blockId: blockId };
|
|
} 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"], "SendStream with blockId should succeed")
|
|
|
|
// Verify block_start was also sent
|
|
output := mockWriter.buffer.String()
|
|
assert.Contains(t, output, "block_start", "Output should contain block_start event")
|
|
}
|
|
|
|
// TestJsValueEnd tests the End method on Context
|
|
func TestJsValueEnd(t *testing.T) {
|
|
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
mockWriter := newMockResponseWriter()
|
|
|
|
cxt := New(context.Background(), nil, "test-chat-id")
|
|
cxt.AssistantID = "test-assistant-id"
|
|
cxt.Accept = AcceptWebCUI
|
|
cxt.Locale = "en"
|
|
cxt.Writer = mockWriter
|
|
|
|
res, err := v8.Call(v8.CallOptions{}, `
|
|
function test(ctx) {
|
|
try {
|
|
// Start a streaming message
|
|
const msgId = ctx.SendStream({
|
|
type: "text",
|
|
props: { content: "Hello" }
|
|
});
|
|
|
|
// End the message
|
|
ctx.End(msgId);
|
|
|
|
return { success: true, msgId: msgId };
|
|
} 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)
|
|
}
|
|
if !result["success"].(bool) {
|
|
t.Logf("Error: %v", result["error"])
|
|
}
|
|
assert.Equal(t, true, result["success"], "End should work correctly")
|
|
|
|
// Verify message_end was sent
|
|
output := mockWriter.buffer.String()
|
|
assert.Contains(t, output, "message_end", "Output should contain message_end event after End()")
|
|
}
|
|
|
|
// TestJsValueEndWithFinalContent tests End with final content parameter
|
|
func TestJsValueEndWithFinalContent(t *testing.T) {
|
|
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
mockWriter := newMockResponseWriter()
|
|
|
|
cxt := New(context.Background(), nil, "test-chat-id")
|
|
cxt.AssistantID = "test-assistant-id"
|
|
cxt.Accept = AcceptWebCUI
|
|
cxt.Locale = "en"
|
|
cxt.Writer = mockWriter
|
|
|
|
res, err := v8.Call(v8.CallOptions{}, `
|
|
function test(ctx) {
|
|
try {
|
|
// Start a streaming message
|
|
const msgId = ctx.SendStream({
|
|
type: "text",
|
|
props: { content: "Start" }
|
|
});
|
|
|
|
// End with final content
|
|
ctx.End(msgId, " End");
|
|
|
|
return { success: true, msgId: msgId };
|
|
} 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)
|
|
}
|
|
if !result["success"].(bool) {
|
|
t.Logf("Error: %v", result["error"])
|
|
}
|
|
assert.Equal(t, true, result["success"], "End with final content should work correctly")
|
|
|
|
// Verify message_end was sent
|
|
output := mockWriter.buffer.String()
|
|
assert.Contains(t, output, "message_end", "Output should contain message_end event")
|
|
}
|
|
|
|
// TestJsValueStreamingWorkflow tests the complete streaming workflow: SendStream -> Append -> End
|
|
func TestJsValueStreamingWorkflow(t *testing.T) {
|
|
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
mockWriter := newMockResponseWriter()
|
|
|
|
cxt := New(context.Background(), nil, "test-chat-id")
|
|
cxt.AssistantID = "test-assistant-id"
|
|
cxt.Accept = AcceptWebCUI
|
|
cxt.Locale = "en"
|
|
cxt.Writer = mockWriter
|
|
|
|
res, err := v8.Call(v8.CallOptions{}, `
|
|
function test(ctx) {
|
|
try {
|
|
// Start a streaming message
|
|
const msgId = ctx.SendStream({
|
|
type: "text",
|
|
props: { content: "# Title\n\n" }
|
|
});
|
|
|
|
// Append content in chunks (simulating streaming)
|
|
ctx.Append(msgId, "First paragraph. ");
|
|
ctx.Append(msgId, "Second sentence. ");
|
|
ctx.Append(msgId, "Third sentence.\n\n");
|
|
ctx.Append(msgId, "Second paragraph.");
|
|
|
|
// Finalize the message
|
|
ctx.End(msgId);
|
|
|
|
return { success: true, msgId: msgId };
|
|
} 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)
|
|
}
|
|
if !result["success"].(bool) {
|
|
t.Logf("Error: %v", result["error"])
|
|
}
|
|
assert.Equal(t, true, result["success"], "Streaming workflow should work correctly")
|
|
|
|
// Verify the complete workflow events
|
|
output := mockWriter.buffer.String()
|
|
assert.Contains(t, output, "message_start", "Output should contain message_start")
|
|
assert.Contains(t, output, "message_end", "Output should contain message_end")
|
|
assert.Contains(t, output, "# Title", "Output should contain initial content")
|
|
assert.Contains(t, output, "First paragraph", "Output should contain appended content")
|
|
}
|
|
|
|
// TestJsValueSendStreamStringShorthand tests SendStream with string shorthand
|
|
func TestJsValueSendStreamStringShorthand(t *testing.T) {
|
|
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
mockWriter := newMockResponseWriter()
|
|
|
|
cxt := New(context.Background(), nil, "test-chat-id")
|
|
cxt.AssistantID = "test-assistant-id"
|
|
cxt.Accept = AcceptWebCUI
|
|
cxt.Locale = "en"
|
|
cxt.Writer = mockWriter
|
|
|
|
res, err := v8.Call(v8.CallOptions{}, `
|
|
function test(ctx) {
|
|
try {
|
|
// SendStream with string shorthand
|
|
const msgId = ctx.SendStream("Hello streaming");
|
|
|
|
if (typeof msgId !== 'string' || msgId === '') {
|
|
throw new Error('SendStream should return a message ID');
|
|
}
|
|
|
|
ctx.End(msgId);
|
|
|
|
return { success: true, msgId: msgId };
|
|
} 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"], "SendStream with string shorthand should succeed")
|
|
}
|
|
|
|
// TestJsValueEndErrorHandling tests error handling in End method
|
|
func TestJsValueEndErrorHandling(t *testing.T) {
|
|
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
mockWriter := newMockResponseWriter()
|
|
|
|
cxt := New(context.Background(), nil, "test-chat-id")
|
|
cxt.AssistantID = "test-assistant-id"
|
|
cxt.Accept = AcceptWebCUI
|
|
cxt.Locale = "en"
|
|
cxt.Writer = mockWriter
|
|
|
|
// Test End without arguments
|
|
res, err := v8.Call(v8.CallOptions{}, `
|
|
function test(ctx) {
|
|
try {
|
|
ctx.End();
|
|
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"], "End without arguments should fail")
|
|
assert.Contains(t, result["error"], "messageId", "Error should mention missing messageId")
|
|
}
|
|
|
|
// TestJsValueEndWithInvalidMessageID tests End with invalid messageId type
|
|
func TestJsValueEndWithInvalidMessageID(t *testing.T) {
|
|
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
mockWriter := newMockResponseWriter()
|
|
|
|
cxt := New(context.Background(), nil, "test-chat-id")
|
|
cxt.AssistantID = "test-assistant-id"
|
|
cxt.Accept = AcceptWebCUI
|
|
cxt.Locale = "en"
|
|
cxt.Writer = mockWriter
|
|
|
|
// Test End with non-string messageId
|
|
res, err := v8.Call(v8.CallOptions{}, `
|
|
function test(ctx) {
|
|
try {
|
|
ctx.End(123);
|
|
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"], "End with non-string messageId should fail")
|
|
assert.Contains(t, result["error"], "string", "Error should mention messageId must be string")
|
|
}
|
|
|
|
// TestJsValueSendStreamErrorHandling tests error handling in SendStream method
|
|
func TestJsValueSendStreamErrorHandling(t *testing.T) {
|
|
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
mockWriter := newMockResponseWriter()
|
|
|
|
cxt := New(context.Background(), nil, "test-chat-id")
|
|
cxt.AssistantID = "test-assistant-id"
|
|
cxt.Accept = AcceptWebCUI
|
|
cxt.Locale = "en"
|
|
cxt.Writer = mockWriter
|
|
|
|
// Test SendStream without arguments
|
|
res, err := v8.Call(v8.CallOptions{}, `
|
|
function test(ctx) {
|
|
try {
|
|
ctx.SendStream();
|
|
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"], "SendStream without arguments should fail")
|
|
assert.Contains(t, result["error"], "SendStream requires a message argument", "Error should mention missing message")
|
|
}
|
|
|
|
// TestJsValueMultipleStreams tests handling multiple concurrent streaming messages
|
|
func TestJsValueMultipleStreams(t *testing.T) {
|
|
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
mockWriter := newMockResponseWriter()
|
|
|
|
cxt := New(context.Background(), nil, "test-chat-id")
|
|
cxt.AssistantID = "test-assistant-id"
|
|
cxt.Accept = AcceptWebCUI
|
|
cxt.Locale = "en"
|
|
cxt.Writer = mockWriter
|
|
|
|
res, err := v8.Call(v8.CallOptions{}, `
|
|
function test(ctx) {
|
|
try {
|
|
// Start multiple streaming messages
|
|
const msg1 = ctx.SendStream({ type: "text", props: { content: "Stream 1: " } });
|
|
const msg2 = ctx.SendStream({ type: "text", props: { content: "Stream 2: " } });
|
|
|
|
// Interleave appends
|
|
ctx.Append(msg1, "A");
|
|
ctx.Append(msg2, "X");
|
|
ctx.Append(msg1, "B");
|
|
ctx.Append(msg2, "Y");
|
|
ctx.Append(msg1, "C");
|
|
ctx.Append(msg2, "Z");
|
|
|
|
// End both streams
|
|
ctx.End(msg1);
|
|
ctx.End(msg2);
|
|
|
|
return { success: true, msg1: msg1, msg2: msg2 };
|
|
} 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)
|
|
}
|
|
if !result["success"].(bool) {
|
|
t.Logf("Error: %v", result["error"])
|
|
}
|
|
assert.Equal(t, true, result["success"], "Multiple streams should work correctly")
|
|
assert.NotEqual(t, result["msg1"], result["msg2"], "Message IDs should be different")
|
|
}
|
|
|
|
// TestJsValueSendVsSendStream tests the difference between Send and SendStream
|
|
func TestJsValueSendVsSendStream(t *testing.T) {
|
|
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
// Test Send - should auto-send message_end
|
|
t.Run("Send auto-ends", func(t *testing.T) {
|
|
mockWriter := newMockResponseWriter()
|
|
cxt := New(context.Background(), nil, "test-chat-id")
|
|
cxt.AssistantID = "test-assistant-id"
|
|
cxt.Accept = AcceptWebCUI
|
|
cxt.Locale = "en"
|
|
cxt.Writer = mockWriter
|
|
|
|
_, err := v8.Call(v8.CallOptions{}, `
|
|
function test(ctx) {
|
|
ctx.Send("Complete message");
|
|
return true;
|
|
}`, cxt)
|
|
if err != nil {
|
|
t.Fatalf("Call failed: %v", err)
|
|
}
|
|
|
|
output := mockWriter.buffer.String()
|
|
assert.Contains(t, output, "message_start", "Send should emit message_start")
|
|
assert.Contains(t, output, "message_end", "Send should auto-emit message_end")
|
|
})
|
|
|
|
// Test SendStream - should NOT auto-send message_end
|
|
t.Run("SendStream requires explicit End", func(t *testing.T) {
|
|
mockWriter := newMockResponseWriter()
|
|
cxt := New(context.Background(), nil, "test-chat-id")
|
|
cxt.AssistantID = "test-assistant-id"
|
|
cxt.Accept = AcceptWebCUI
|
|
cxt.Locale = "en"
|
|
cxt.Writer = mockWriter
|
|
|
|
_, err := v8.Call(v8.CallOptions{}, `
|
|
function test(ctx) {
|
|
const msgId = ctx.SendStream("Streaming message");
|
|
// Intentionally NOT calling ctx.End(msgId)
|
|
return msgId;
|
|
}`, cxt)
|
|
if err != nil {
|
|
t.Fatalf("Call failed: %v", err)
|
|
}
|
|
|
|
output := mockWriter.buffer.String()
|
|
assert.Contains(t, output, "message_start", "SendStream should emit message_start")
|
|
assert.NotContains(t, output, "message_end", "SendStream should NOT auto-emit message_end")
|
|
})
|
|
}
|