diff --git a/agent/assistant/mcp_test.go b/agent/assistant/mcp_test.go index 2b2339cd..fc4d1e18 100644 --- a/agent/assistant/mcp_test.go +++ b/agent/assistant/mcp_test.go @@ -302,9 +302,24 @@ func TestMCPToolContextPassing(t *testing.T) { assert.NotEmpty(t, assistantID, "assistant_id should have a value") assert.Equal(t, "test-assistant-mcptest", assistantID, "assistant_id should match") + // Verify authorized information + authorizedData, ok := contextData["authorized"].(map[string]interface{}) + assert.True(t, ok, "Context should have authorized field") + assert.NotNil(t, authorizedData, "Authorized data should not be nil") + + userID, ok := authorizedData["user_id"].(string) + assert.True(t, ok, "Authorized should have user_id field") + assert.Equal(t, "test-user-123", userID, "User ID should match") + + tenantID, ok := authorizedData["tenant_id"].(string) + assert.True(t, ok, "Authorized should have tenant_id field") + assert.Equal(t, "test-tenant-456", tenantID, "Tenant ID should match") + t.Logf("✓ Context successfully passed to MCP tool") t.Logf(" - ChatID: %s", chatID) t.Logf(" - AssistantID: %s", assistantID) + t.Logf(" - UserID: %s", userID) + t.Logf(" - TenantID: %s", tenantID) } // TestMCPToolContextPassingParallel tests that agent context is correctly passed in parallel calls @@ -372,6 +387,13 @@ func TestMCPToolContextPassingParallel(t *testing.T) { assert.True(t, ok, "Context %d should have chat_id field", i) assert.Equal(t, "parallel-chat-789", chatID, "Chat ID in result %d should match", i) + // Verify authorized information in parallel call + authorizedData, ok := contextData["authorized"].(map[string]interface{}) + assert.True(t, ok, "Context %d should have authorized field", i) + if userID, ok := authorizedData["user_id"].(string); ok { + assert.Equal(t, "parallel-user-123", userID, "User ID in result %d should match", i) + } + t.Logf("✓ Result %d successfully received context", i) } diff --git a/agent/context/jsapi.go b/agent/context/jsapi.go index 93610438..2de74a69 100644 --- a/agent/context/jsapi.go +++ b/agent/context/jsapi.go @@ -102,22 +102,32 @@ func (ctx *Context) NewObject(v8ctx *v8go.Context) (*v8go.Value, error) { clientVal.Release() // Release Go-side Persistent handle, V8 internal reference remains } - // Metadata object - if ctx.Metadata != nil { - metadataVal, err := bridge.JsValue(v8ctx, ctx.Metadata) - if err == nil { - obj.Set("metadata", metadataVal) - metadataVal.Release() // Release Go-side Persistent handle, V8 internal reference remains - } + // Metadata object - always set to empty map if nil + metadataData := ctx.Metadata + if metadataData == nil { + metadataData = map[string]interface{}{} + } + metadataVal, err := bridge.JsValue(v8ctx, metadataData) + if err == nil { + obj.Set("metadata", metadataVal) + metadataVal.Release() // Release Go-side Persistent handle, V8 internal reference remains } - // Authorized object + // Authorized object - set individual fields to ensure proper structure + var authorizedData map[string]interface{} if ctx.Authorized != nil { - authorizedVal, err := bridge.JsValue(v8ctx, ctx.Authorized) - if err == nil { - obj.Set("authorized", authorizedVal) - authorizedVal.Release() // Release Go-side Persistent handle, V8 internal reference remains + authorizedData = map[string]interface{}{ + "user_id": ctx.Authorized.UserID, + "tenant_id": ctx.Authorized.TenantID, + "client_id": ctx.Authorized.ClientID, } + } else { + authorizedData = map[string]interface{}{} + } + authorizedVal, err := bridge.JsValue(v8ctx, authorizedData) + if err == nil { + obj.Set("authorized", authorizedVal) + authorizedVal.Release() // Release Go-side Persistent handle, V8 internal reference remains } return instance.Value, nil diff --git a/agent/context/jsapi_test.go b/agent/context/jsapi_test.go index 2c430d9b..9ce1f21e 100644 --- a/agent/context/jsapi_test.go +++ b/agent/context/jsapi_test.go @@ -463,3 +463,159 @@ func TestJsValueTrace(t *testing.T) { assert.NotEmpty(t, result["node_id"], "node_id should not be empty") assert.Equal(t, true, result["success"], "operation should succeed") } + +// TestJsValueAuthorizedAndMetadata test the authorized and metadata fields +func TestJsValueAuthorizedAndMetadata(t *testing.T) { + + test.Prepare(t, config.Conf) + defer test.Clean() + + cxt := &context.Context{ + ChatID: "test-chat-id", + AssistantID: "test-assistant-id", + Context: stdContext.Background(), + IDGenerator: message.NewIDGenerator(), + Authorized: &types.AuthorizedInfo{ + UserID: "user-123", + TenantID: "tenant-456", + ClientID: "client-789", + }, + Metadata: map[string]interface{}{ + "request_id": "req-001", + "source": "api", + "version": "1.0.0", + }, + } + + v8.RegisterFunction("testAuthorizedMetadata", testAuthorizedMetadataEmbed) + res, err := v8.Call(v8.CallOptions{}, ` + function test(cxt) { + return testAuthorizedMetadata(cxt) + }`, 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) + } + + // Verify authorized object + authorized, ok := result["authorized"].(map[string]interface{}) + assert.True(t, ok, "authorized should be an object") + assert.Equal(t, "user-123", authorized["user_id"], "authorized.user_id mismatch") + assert.Equal(t, "tenant-456", authorized["tenant_id"], "authorized.tenant_id mismatch") + assert.Equal(t, "client-789", authorized["client_id"], "authorized.client_id mismatch") + + // Verify metadata object + metadata, ok := result["metadata"].(map[string]interface{}) + assert.True(t, ok, "metadata should be an object") + assert.Equal(t, "req-001", metadata["request_id"], "metadata.request_id mismatch") + assert.Equal(t, "api", metadata["source"], "metadata.source mismatch") + assert.Equal(t, "1.0.0", metadata["version"], "metadata.version mismatch") +} + +func testAuthorizedMetadataEmbed(iso *v8go.Isolate) *v8go.FunctionTemplate { + return v8go.NewFunctionTemplate(iso, testAuthorizedMetadataFunction) +} + +func testAuthorizedMetadataFunction(info *v8go.FunctionCallbackInfo) *v8go.Value { + var args = info.Args() + if len(args) < 1 { + return bridge.JsException(info.Context(), "Missing parameters") + } + + ctx, err := args[0].AsObject() + if err != nil { + return bridge.JsException(info.Context(), err) + } + + // Extract authorized and metadata fields + result := map[string]interface{}{} + + // Get authorized + authorizedVal, err := ctx.Get("authorized") + if err != nil { + return bridge.JsException(info.Context(), err) + } + if !authorizedVal.IsUndefined() && !authorizedVal.IsNull() { + authorized, err := bridge.GoValue(authorizedVal, info.Context()) + if err != nil { + return bridge.JsException(info.Context(), err) + } + result["authorized"] = authorized + } + + // Get metadata + metadataVal, err := ctx.Get("metadata") + if err != nil { + return bridge.JsException(info.Context(), err) + } + if !metadataVal.IsUndefined() && !metadataVal.IsNull() { + metadata, err := bridge.GoValue(metadataVal, info.Context()) + if err != nil { + return bridge.JsException(info.Context(), err) + } + result["metadata"] = metadata + } + + jsVal, err := bridge.JsValue(info.Context(), result) + if err != nil { + return bridge.JsException(info.Context(), err) + } + return jsVal +} + +// TestJsValueAuthorizedNil test when authorized is nil +func TestJsValueAuthorizedNil(t *testing.T) { + + test.Prepare(t, config.Conf) + defer test.Clean() + + cxt := &context.Context{ + ChatID: "test-chat-id", + AssistantID: "test-assistant-id", + Context: stdContext.Background(), + IDGenerator: message.NewIDGenerator(), + Authorized: nil, // Explicitly nil + Metadata: nil, // Explicitly nil (should be empty object) + } + + res, err := v8.Call(v8.CallOptions{}, ` + function test(cxt) { + // Debug: check the actual values + const authorized = cxt.authorized; + const metadata = cxt.metadata; + + return { + authorized_type: typeof authorized, + authorized_is_null: authorized === null, + authorized_is_undefined: authorized === undefined, + metadata_type: typeof metadata, + metadata_is_object: typeof metadata === 'object' && metadata !== null, + metadata_is_empty: metadata && Object.keys(metadata).length === 0, + has_authorized: 'authorized' in cxt, + has_metadata: 'metadata' in cxt + } + }`, 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) + } + + // Verify authorized exists and is an empty object when nil + assert.Equal(t, true, result["has_authorized"], "authorized property should exist") + assert.Equal(t, "object", result["authorized_type"], "authorized should be an object") + assert.Equal(t, true, result["metadata_is_object"], "authorized should be an object (not null)") + + // Verify metadata is an empty object when not set + assert.Equal(t, true, result["has_metadata"], "metadata property should exist") + assert.Equal(t, "object", result["metadata_type"], "metadata should be an object") + assert.Equal(t, true, result["metadata_is_object"], "metadata should be an object") + assert.Equal(t, true, result["metadata_is_empty"], "metadata should be empty object when not set") +}