package context_test import ( "bytes" stdContext "context" "net/http" "testing" "github.com/stretchr/testify/assert" v8 "github.com/yaoapp/gou/runtime/v8" "github.com/yaoapp/yao/agent/context" "github.com/yaoapp/yao/config" "github.com/yaoapp/yao/test" ) // testMockResponseWriter is a mock implementation of http.ResponseWriter for testing type testMockResponseWriter struct { headers http.Header buffer *bytes.Buffer status int } func newTestMockResponseWriter() *testMockResponseWriter { return &testMockResponseWriter{ headers: make(http.Header), buffer: &bytes.Buffer{}, status: http.StatusOK, } } func (m *testMockResponseWriter) Header() http.Header { return m.headers } func (m *testMockResponseWriter) Write(b []byte) (int, error) { return m.buffer.Write(b) } func (m *testMockResponseWriter) 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.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.AcceptStandard cxt.Locale = "en" cxt.Writer = newTestMockResponseWriter() // 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.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.AcceptStandard cxt.Locale = "en" cxt.Writer = newTestMockResponseWriter() 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.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.AcceptStandard cxt.Locale = "en" cxt.Writer = newTestMockResponseWriter() 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.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.AcceptStandard cxt.Locale = "en" cxt.Writer = newTestMockResponseWriter() // 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 := []context.Accept{context.AcceptWebCUI, context.AccepNativeCUI, context.AcceptDesktopCUI} for _, acceptType := range acceptTypes { t.Run(string(acceptType), func(t *testing.T) { cxt := context.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = acceptType cxt.Locale = "en" cxt.Writer = newTestMockResponseWriter() 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 "+string(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.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.AcceptStandard cxt.Locale = "en" cxt.Writer = newTestMockResponseWriter() 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.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.AcceptStandard cxt.Locale = "en" cxt.Writer = newTestMockResponseWriter() 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.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.AcceptStandard cxt.Locale = "en" cxt.Writer = newTestMockResponseWriter() 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.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.AcceptStandard cxt.Locale = "en" cxt.Writer = newTestMockResponseWriter() 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.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.AcceptStandard cxt.Locale = "en" cxt.Writer = newTestMockResponseWriter() 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.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.AcceptStandard cxt.Locale = "en" cxt.Writer = newTestMockResponseWriter() 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.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.AcceptStandard cxt.Locale = "en" cxt.Writer = newTestMockResponseWriter() 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.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.AcceptStandard cxt.Locale = "en" cxt.Writer = newTestMockResponseWriter() 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 := newTestMockResponseWriter() // Use New() to properly initialize messageMetadata cxt := context.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.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") // Close SafeWriter to wait for all async writes to complete cxt.CloseSafeWriter() // 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 := newTestMockResponseWriter() // Use New() to properly initialize messageMetadata cxt := context.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.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") // Close SafeWriter to wait for all async writes to complete cxt.CloseSafeWriter() // 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 := newTestMockResponseWriter() cxt := context.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.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") // Close SafeWriter to wait for all async writes to complete cxt.CloseSafeWriter() // 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 := newTestMockResponseWriter() cxt := context.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.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") // Close SafeWriter to wait for all async writes to complete cxt.CloseSafeWriter() // 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 := newTestMockResponseWriter() cxt := context.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.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") // Close SafeWriter to wait for all async writes to complete cxt.CloseSafeWriter() // 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 := newTestMockResponseWriter() cxt := context.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.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") // Close SafeWriter to flush all pending async writes before reading buffer. // SafeWriter processes writes in a background goroutine via channel; // without this, the buffer may still be empty on slow CI runners. cxt.CloseSafeWriter() // 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 := newTestMockResponseWriter() cxt := context.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.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 := newTestMockResponseWriter() cxt := context.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.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 := newTestMockResponseWriter() cxt := context.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.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 := newTestMockResponseWriter() cxt := context.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.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 := newTestMockResponseWriter() cxt := context.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.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 := newTestMockResponseWriter() cxt := context.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.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) } // Close SafeWriter to flush all pending async writes before reading buffer. // SafeWriter processes writes in a background goroutine via channel; // without this, the buffer may still be empty on slow CI runners. cxt.CloseSafeWriter() 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 := newTestMockResponseWriter() cxt := context.New(stdContext.Background(), nil, "test-chat-id") cxt.AssistantID = "test-assistant-id" cxt.Accept = context.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) } // Close SafeWriter to flush all pending async writes before reading buffer. cxt.CloseSafeWriter() 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") }) }