File: CapabilityDispatcherTests.cs
Web Access
Project: src\tests\Aspire.Hosting.RemoteHost.Tests\Aspire.Hosting.RemoteHost.Tests.csproj (Aspire.Hosting.RemoteHost.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Text.Json.Nodes;
using Aspire.Hosting.Ats;
using Aspire.Hosting.RemoteHost.Ats;
using Xunit;
 
namespace Aspire.Hosting.RemoteHost.Tests;
 
public class CapabilityDispatcherTests
{
    [Fact]
    public void Register_AddsCapabilityThatCanBeInvoked()
    {
        var dispatcher = CreateDispatcher();
        CapabilityHandler handler = (args, handles) => Task.FromResult<JsonNode?>(JsonValue.Create("result"));
 
        dispatcher.Register("test/capability@1", handler);
 
        Assert.True(dispatcher.HasCapability("test/capability@1"));
    }
 
    [Fact]
    public void HasCapability_ReturnsFalseForUnregisteredCapability()
    {
        var dispatcher = CreateDispatcher();
 
        Assert.False(dispatcher.HasCapability("nonexistent/capability@1"));
    }
 
    [Fact]
    public void GetCapabilityIds_ReturnsAllRegisteredCapabilities()
    {
        var dispatcher = CreateDispatcher();
        dispatcher.Register("test/cap1@1", (_, _) => Task.FromResult<JsonNode?>(null));
        dispatcher.Register("test/cap2@1", (_, _) => Task.FromResult<JsonNode?>(null));
 
        var ids = dispatcher.GetCapabilityIds().ToList();
 
        Assert.Contains("test/cap1@1", ids);
        Assert.Contains("test/cap2@1", ids);
    }
 
    [Fact]
    public void Invoke_CallsRegisteredHandler()
    {
        var dispatcher = CreateDispatcher();
        var called = false;
        dispatcher.Register("test/capability@1", (args, handles) =>
        {
            called = true;
            return Task.FromResult<JsonNode?>(JsonValue.Create("success"));
        });
 
        dispatcher.Invoke("test/capability@1", null);
 
        Assert.True(called);
    }
 
    [Fact]
    public void Invoke_PassesArgumentsToHandler()
    {
        var dispatcher = CreateDispatcher();
        string? receivedName = null;
        dispatcher.Register("test/capability@1", (args, handles) =>
        {
            receivedName = args?["name"]?.GetValue<string>();
            return Task.FromResult<JsonNode?>(null);
        });
 
        dispatcher.Invoke("test/capability@1", new JsonObject { ["name"] = "test-value" });
 
        Assert.Equal("test-value", receivedName);
    }
 
    [Fact]
    public void Invoke_ReturnsHandlerResult()
    {
        var dispatcher = CreateDispatcher();
        dispatcher.Register("test/capability@1", (_, _) => Task.FromResult<JsonNode?>(JsonValue.Create(42)));
 
        var result = dispatcher.Invoke("test/capability@1", null);
 
        Assert.NotNull(result);
        Assert.Equal(42, result.GetValue<int>());
    }
 
    [Fact]
    public void Invoke_ThrowsCapabilityNotFoundForUnregisteredCapability()
    {
        var dispatcher = CreateDispatcher();
 
        var ex = Assert.Throws<CapabilityException>(() =>
            dispatcher.Invoke("nonexistent/capability@1", null));
 
        Assert.Equal(AtsErrorCodes.CapabilityNotFound, ex.Error.Code);
    }
 
    [Fact]
    public void Invoke_WrapsHandlerExceptionsAsInternalError()
    {
        var dispatcher = CreateDispatcher();
        dispatcher.Register("test/capability@1", (_, _) =>
        {
            throw new InvalidOperationException("Handler failed");
#pragma warning disable CS0162 // Unreachable code detected - needed for return type inference
            return Task.FromResult<JsonNode?>(null);
#pragma warning restore CS0162
        });
 
        var ex = Assert.Throws<CapabilityException>(() =>
            dispatcher.Invoke("test/capability@1", null));
 
        Assert.Equal(AtsErrorCodes.InternalError, ex.Error.Code);
        Assert.Contains("Handler failed", ex.Message);
    }
 
    [Fact]
    public void Invoke_PropagatesCapabilityExceptionsDirectly()
    {
        var dispatcher = CreateDispatcher();
        dispatcher.Register("test/capability@1", (_, _) =>
        {
            throw CapabilityException.InvalidArgument("test/capability@1", "param", "Bad value");
#pragma warning disable CS0162 // Unreachable code detected - needed for return type inference
            return Task.FromResult<JsonNode?>(null);
#pragma warning restore CS0162
        });
 
        var ex = Assert.Throws<CapabilityException>(() =>
            dispatcher.Invoke("test/capability@1", null));
 
        Assert.Equal(AtsErrorCodes.InvalidArgument, ex.Error.Code);
    }
 
    [Fact]
    public void Invoke_ConvertsInvalidCastToTypeMismatch()
    {
        var dispatcher = CreateDispatcher();
        dispatcher.Register("test/capability@1", (_, _) =>
        {
            throw new InvalidCastException("Cannot cast to expected type");
#pragma warning disable CS0162 // Unreachable code detected - needed for return type inference
            return Task.FromResult<JsonNode?>(null);
#pragma warning restore CS0162
        });
 
        var ex = Assert.Throws<CapabilityException>(() =>
            dispatcher.Invoke("test/capability@1", null));
 
        Assert.Equal(AtsErrorCodes.TypeMismatch, ex.Error.Code);
    }
 
    [Fact]
    public void Constructor_ScansAssemblyForAspireExportAttributes()
    {
        var dispatcher = CreateDispatcher(typeof(TestCapabilities).Assembly);
 
        Assert.True(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/testMethod"));
    }
 
    [Fact]
    public void Invoke_CanCallScannedCapability()
    {
        var dispatcher = CreateDispatcher(typeof(TestCapabilities).Assembly);
        var args = new JsonObject { ["value"] = "hello" };
 
        var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/testMethod", args);
 
        Assert.NotNull(result);
        Assert.Equal("HELLO", result.GetValue<string>());
    }
 
    [Fact]
    public void Invoke_HandlesOptionalParameters()
    {
        var dispatcher = CreateDispatcher(typeof(TestCapabilities).Assembly);
        var args = new JsonObject { ["required"] = "test" };
 
        var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/withOptional", args);
 
        Assert.NotNull(result);
        Assert.Equal("test:default", result.GetValue<string>());
    }
 
    [Fact]
    public void Invoke_UsesProvidedOptionalParameter()
    {
        var dispatcher = CreateDispatcher(typeof(TestCapabilities).Assembly);
        var args = new JsonObject { ["required"] = "test", ["optional"] = "custom" };
 
        var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/withOptional", args);
 
        Assert.NotNull(result);
        Assert.Equal("test:custom", result.GetValue<string>());
    }
 
    // Context type tests
    [Fact]
    public void Constructor_RegistersContextTypeProperties()
    {
        var dispatcher = CreateDispatcher(typeof(TestContextType).Assembly);
 
        // Properties should be registered with getter capabilities using derived type IDs
        // Type ID = {AssemblyName}/{FullTypeName}
        // Capability ID = {Package}/{TypeName}.{propertyName} (camelCase, no "get" prefix)
        Assert.True(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/TestContextType.name"));
        Assert.True(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/TestContextType.count"));
        Assert.True(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/TestContextType.isEnabled"));
    }
 
    [Fact]
    public void Constructor_SkipsNonAtsCompatibleProperties()
    {
        var dispatcher = CreateDispatcher(typeof(TestContextType).Assembly);
 
        // IDisposable is not ATS-compatible, so this property should be skipped
        Assert.False(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/TestContextType.nonAtsProperty"));
    }
 
    [Fact]
    public void Invoke_ContextTypePropertyReturnsValue()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestContextType).Assembly]);
 
        // Create and register a context object
        // Type ID = {AssemblyName}/{FullTypeName}
        var context = new TestContextType { Name = "test-name", Count = 100 };
        var handleId = handles.Register(context, "Aspire.Hosting.RemoteHost.Tests/Aspire.Hosting.RemoteHost.Tests.TestContextType");
        var args = new JsonObject { ["context"] = new JsonObject { ["$handle"] = handleId } };
 
        var nameResult = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/TestContextType.name", args);
        var countResult = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/TestContextType.count", args);
 
        Assert.NotNull(nameResult);
        Assert.Equal("test-name", nameResult.GetValue<string>());
        Assert.NotNull(countResult);
        Assert.Equal(100, countResult.GetValue<int>());
    }
 
    [Fact]
    public void Invoke_ContextTypePropertyThrowsWhenContextMissing()
    {
        var dispatcher = CreateDispatcher(typeof(TestContextType).Assembly);
 
        var ex = Assert.Throws<CapabilityException>(() =>
            dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/TestContextType.name", null));
 
        Assert.Equal(AtsErrorCodes.InvalidArgument, ex.Error.Code);
        Assert.Contains("context", ex.Message);
    }
 
    [Fact]
    public void Invoke_ContextTypePropertyThrowsWhenContextNotHandle()
    {
        var dispatcher = CreateDispatcher(typeof(TestContextType).Assembly);
        var args = new JsonObject { ["context"] = "not-a-handle" };
 
        var ex = Assert.Throws<CapabilityException>(() =>
            dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/TestContextType.name", args));
 
        Assert.Equal(AtsErrorCodes.InvalidArgument, ex.Error.Code);
    }
 
    [Fact]
    public void Invoke_ContextTypePropertyThrowsWhenHandleNotFound()
    {
        var dispatcher = CreateDispatcher(typeof(TestContextType).Assembly);
        var args = new JsonObject { ["context"] = new JsonObject { ["$handle"] = "Aspire.Hosting.RemoteHost.Tests/Aspire.Hosting.RemoteHost.Tests.TestContextType:999" } };
 
        var ex = Assert.Throws<CapabilityException>(() =>
            dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/TestContextType.name", args));
 
        Assert.Equal(AtsErrorCodes.HandleNotFound, ex.Error.Code);
    }
 
    [Fact]
    public void Constructor_RegistersVersionedContextTypeProperties()
    {
        var dispatcher = CreateDispatcher(typeof(VersionedContextType).Assembly);
 
        // Versioned context type properties should also be registered with derived type ID
        // Capability ID = {Package}/{TypeName}.{propertyName} (camelCase, no "get" prefix)
        Assert.True(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/VersionedContextType.value"));
    }
 
    /// <summary>
    /// Tests that context type properties whose type is another context type are registered.
    /// This simulates the EnvironmentCallbackContext.ExecutionContext scenario.
    /// </summary>
    [Fact]
    public void Constructor_RegistersNestedContextTypeProperties()
    {
        var dispatcher = CreateDispatcher(typeof(TestParentContextType).Assembly);
 
        // The TestParentContextType has a NestedContext property of type TestNestedContextType
        // This capability should be registered because TestNestedContextType is a valid ATS type
        Assert.True(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/TestParentContextType.nestedContext"),
            "NestedContext property capability should be registered");
        Assert.True(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/TestParentContextType.name"),
            "Name property capability should be registered");
 
        // The nested type's properties should also be registered
        Assert.True(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/TestNestedContextType.operation"),
            "Nested context type's Operation property should be registered");
        Assert.True(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/TestNestedContextType.isPublishMode"),
            "Nested context type's IsPublishMode property should be registered");
    }
 
    /// <summary>
    /// Tests that getting a context type property that returns another context type works.
    /// </summary>
    [Fact]
    public void Invoke_NestedContextTypePropertyReturnsHandle()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestParentContextType).Assembly]);
 
        // Create and register the parent context with a nested context
        var nestedContext = new TestNestedContextType { Operation = "Publish", IsPublishMode = true };
        var parentContext = new TestParentContextType(nestedContext) { Name = "parent-context" };
        var handleId = handles.Register(parentContext, "Aspire.Hosting.RemoteHost.Tests/Aspire.Hosting.RemoteHost.Tests.TestParentContextType");
        var args = new JsonObject { ["context"] = new JsonObject { ["$handle"] = handleId } };
 
        // Get the nested context property - should return a handle
        var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/TestParentContextType.nestedContext", args);
 
        Assert.NotNull(result);
        // The result should be a handle reference to the nested context
        var handleRef = result.AsObject();
        Assert.NotNull(handleRef);
        Assert.True(handleRef.ContainsKey("$handle"), "Result should be a handle reference");
        Assert.True(handleRef.ContainsKey("$type"), "Result should have a type");
        var nestedHandleId = handleRef["$handle"]!.GetValue<string>();
        var nestedTypeId = handleRef["$type"]!.GetValue<string>();
        // Handle ID is now just a numeric instance ID
        Assert.True(long.TryParse(nestedHandleId, out _), "Handle ID should be numeric");
        // Type ID is in the $type field
        Assert.StartsWith("Aspire.Hosting.RemoteHost.Tests/Aspire.Hosting.RemoteHost.Tests.TestNestedContextType", nestedTypeId);
 
        // Verify we can use the returned handle to access the nested context's properties
        var nestedArgs = new JsonObject { ["context"] = new JsonObject { ["$handle"] = nestedHandleId } };
        var operationResult = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/TestNestedContextType.operation", nestedArgs);
        Assert.NotNull(operationResult);
        Assert.Equal("Publish", operationResult.GetValue<string>());
    }
 
    /// <summary>
    /// Tests that EnvironmentCallbackContext.executionContext capability is registered.
    /// This is the exact scenario that was failing at runtime.
    /// </summary>
    [Fact]
    public void Constructor_RegistersEnvironmentCallbackContextExecutionContextCapability()
    {
        // Scan the Aspire.Hosting assembly which contains both:
        // - EnvironmentCallbackContext (has ExecutionContext property)
        // - DistributedApplicationExecutionContext (the property type)
        var dispatcher = CreateDispatcher(typeof(AspireExportAttribute).Assembly);
 
        // EnvironmentCallbackContext.ExecutionContext should be registered
        // Capability ID format: {Namespace}/{TypeName}.{camelCasePropertyName}
        Assert.True(dispatcher.HasCapability("Aspire.Hosting.ApplicationModel/EnvironmentCallbackContext.executionContext"),
            "EnvironmentCallbackContext.executionContext capability should be registered");
 
        // Other EnvironmentCallbackContext properties should also be registered
        Assert.True(dispatcher.HasCapability("Aspire.Hosting.ApplicationModel/EnvironmentCallbackContext.environmentVariables"),
            "EnvironmentCallbackContext.environmentVariables should be registered");
 
        // DistributedApplicationExecutionContext properties should also be registered
        Assert.True(dispatcher.HasCapability("Aspire.Hosting/DistributedApplicationExecutionContext.operation"),
            "DistributedApplicationExecutionContext.operation should be registered");
        Assert.True(dispatcher.HasCapability("Aspire.Hosting/DistributedApplicationExecutionContext.isPublishMode"),
            "DistributedApplicationExecutionContext.isPublishMode should be registered");
        Assert.True(dispatcher.HasCapability("Aspire.Hosting/DistributedApplicationExecutionContext.isRunMode"),
            "DistributedApplicationExecutionContext.isRunMode should be registered");
    }
 
    // Property setter tests
    [Fact]
    public void Constructor_RegistersPropertySetters()
    {
        var dispatcher = CreateDispatcher(typeof(TestContextType).Assembly);
 
        // Property setters should be registered with "set" prefix and PascalCase property name
        // Capability ID = {Package}/{TypeName}.set{PropertyName}
        Assert.True(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/TestContextType.setName"));
        Assert.True(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/TestContextType.setCount"));
        Assert.True(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/TestContextType.setIsEnabled"));
    }
 
    [Fact]
    public void Invoke_PropertySetterUpdatesValue()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestContextType).Assembly]);
 
        // Create and register a context object
        var context = new TestContextType { Name = "original-name" };
        var handleId = handles.Register(context, "Aspire.Hosting.RemoteHost.Tests/Aspire.Hosting.RemoteHost.Tests.TestContextType");
        var args = new JsonObject
        {
            ["context"] = new JsonObject { ["$handle"] = handleId },
            ["value"] = "new-name"
        };
 
        // Invoke the setter
        dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/TestContextType.setName", args);
 
        // Verify the value was updated
        Assert.Equal("new-name", context.Name);
    }
 
    [Fact]
    public void Invoke_PropertySetterUpdatesIntValue()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestContextType).Assembly]);
 
        var context = new TestContextType { Count = 10 };
        var handleId = handles.Register(context, "Aspire.Hosting.RemoteHost.Tests/Aspire.Hosting.RemoteHost.Tests.TestContextType");
        var args = new JsonObject
        {
            ["context"] = new JsonObject { ["$handle"] = handleId },
            ["value"] = 99
        };
 
        dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/TestContextType.setCount", args);
 
        Assert.Equal(99, context.Count);
    }
 
    [Fact]
    public void Invoke_PropertySetterUpdatesBoolValue()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestContextType).Assembly]);
 
        var context = new TestContextType { IsEnabled = true };
        var handleId = handles.Register(context, "Aspire.Hosting.RemoteHost.Tests/Aspire.Hosting.RemoteHost.Tests.TestContextType");
        var args = new JsonObject
        {
            ["context"] = new JsonObject { ["$handle"] = handleId },
            ["value"] = false
        };
 
        dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/TestContextType.setIsEnabled", args);
 
        Assert.False(context.IsEnabled);
    }
 
    [Fact]
    public void Invoke_PropertySetterThrowsWhenValueMissing()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestContextType).Assembly]);
 
        var context = new TestContextType();
        var handleId = handles.Register(context, "Aspire.Hosting.RemoteHost.Tests/Aspire.Hosting.RemoteHost.Tests.TestContextType");
        var args = new JsonObject
        {
            ["context"] = new JsonObject { ["$handle"] = handleId }
            // Missing "value" parameter
        };
 
        var ex = Assert.Throws<CapabilityException>(() =>
            dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/TestContextType.setName", args));
 
        Assert.Equal(AtsErrorCodes.InvalidArgument, ex.Error.Code);
        Assert.Contains("value", ex.Message);
    }
 
    // Instance method tests
    [Fact]
    public void Constructor_RegistersInstanceMethods()
    {
        var dispatcher = CreateDispatcher(typeof(TestTypeWithMethods).Assembly);
 
        // Instance methods should be registered with capability ID format: {Package}/{TypeName}.{methodName}
        Assert.True(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/TestTypeWithMethods.doSomething"));
        Assert.True(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/TestTypeWithMethods.calculateSum"));
        Assert.True(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/TestTypeWithMethods.processAsync"));
    }
 
    [Fact]
    public void Invoke_InstanceMethodCallsMethodOnContext()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestTypeWithMethods).Assembly]);
 
        var context = new TestTypeWithMethods();
        var handleId = handles.Register(context, "Aspire.Hosting.RemoteHost.Tests/Aspire.Hosting.RemoteHost.Tests.TestTypeWithMethods");
        var args = new JsonObject
        {
            ["context"] = new JsonObject { ["$handle"] = handleId }
        };
 
        dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/TestTypeWithMethods.doSomething", args);
 
        Assert.True(context.WasCalled);
    }
 
    [Fact]
    public void Invoke_InstanceMethodWithParameters()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestTypeWithMethods).Assembly]);
 
        var context = new TestTypeWithMethods();
        var handleId = handles.Register(context, "Aspire.Hosting.RemoteHost.Tests/Aspire.Hosting.RemoteHost.Tests.TestTypeWithMethods");
        var args = new JsonObject
        {
            ["context"] = new JsonObject { ["$handle"] = handleId },
            ["a"] = 5,
            ["b"] = 7
        };
 
        var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/TestTypeWithMethods.calculateSum", args);
 
        Assert.NotNull(result);
        Assert.Equal(12, result.GetValue<int>());
    }
 
    [Fact]
    public void Invoke_AsyncInstanceMethod()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestTypeWithMethods).Assembly]);
 
        var context = new TestTypeWithMethods();
        var handleId = handles.Register(context, "Aspire.Hosting.RemoteHost.Tests/Aspire.Hosting.RemoteHost.Tests.TestTypeWithMethods");
        var args = new JsonObject
        {
            ["context"] = new JsonObject { ["$handle"] = handleId },
            ["input"] = "test"
        };
 
        var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/TestTypeWithMethods.processAsync", args);
 
        Assert.NotNull(result);
        Assert.Equal("TEST", result.GetValue<string>());
    }
 
    [Fact]
    public void Constructor_SkipsNonPublicMethods()
    {
        var dispatcher = CreateDispatcher(typeof(TestTypeWithMethods).Assembly);
 
        // Private methods should not be exposed
        Assert.False(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/TestTypeWithMethods.privateMethod"));
    }
 
    [Fact]
    public void Constructor_SkipsSpecialMethods()
    {
        var dispatcher = CreateDispatcher(typeof(TestTypeWithMethods).Assembly);
 
        // Property getters/setters via ExposeMethods should not create duplicate capabilities
        // (they are handled by ExposeProperties separately)
        // Object methods like ToString, GetHashCode should be skipped
        Assert.False(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/TestTypeWithMethods.toString"));
        Assert.False(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/TestTypeWithMethods.getHashCode"));
        Assert.False(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/TestTypeWithMethods.equals"));
        Assert.False(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/TestTypeWithMethods.getType"));
    }
 
    // Callback parameter tests
    [Fact]
    public void Invoke_MethodWithCallbackParameter()
    {
        var handles = new HandleRegistry();
        var invoker = new TestCallbackInvoker();
        var (marshaller, callbackFactory) = CreateTestMarshallerWithCallbacks(handles, invoker);
        using var _ = callbackFactory;
        var dispatcher = new CapabilityDispatcher(handles, marshaller, [typeof(TestCapabilitiesWithCallback).Assembly]);
 
        var args = new JsonObject
        {
            ["value"] = "test-input",
            ["callback"] = "my-callback-id"
        };
 
        var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/withCallback", args);
 
        // The method should have been called with the callback proxy
        // The callback proxy invokes our TestCallbackInvoker
        Assert.NotNull(result);
        Assert.Equal("PROCESSED: test-input", result.GetValue<string>());
    }
 
    [Fact]
    public void Invoke_MethodWithCallbackParameter_InvokesCallback()
    {
        var handles = new HandleRegistry();
        var invoker = new TestCallbackInvoker();
        var (marshaller, callbackFactory) = CreateTestMarshallerWithCallbacks(handles, invoker);
        using var _ = callbackFactory;
        var dispatcher = new CapabilityDispatcher(handles, marshaller, [typeof(TestCapabilitiesWithCallback).Assembly]);
 
        var args = new JsonObject
        {
            ["callback"] = "progress-callback"
        };
 
        dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/invokeCallback", args);
 
        // Verify the callback was invoked
        Assert.Single(invoker.Invocations);
        Assert.Equal("progress-callback", invoker.Invocations[0].CallbackId);
    }
 
    [Fact]
    public void Invoke_MethodWithCallbackParameter_PassesArgsToCallback()
    {
        var handles = new HandleRegistry();
        var invoker = new TestCallbackInvoker();
        var (marshaller, callbackFactory) = CreateTestMarshallerWithCallbacks(handles, invoker);
        using var _ = callbackFactory;
        var dispatcher = new CapabilityDispatcher(handles, marshaller, [typeof(TestCapabilitiesWithCallback).Assembly]);
 
        var args = new JsonObject
        {
            ["callback"] = "typed-callback"
        };
 
        dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/invokeTypedCallback", args);
 
        Assert.Single(invoker.Invocations);
        var callbackArgs = invoker.Invocations[0].Args as JsonObject;
        Assert.NotNull(callbackArgs);
        // Callback arguments are passed with positional keys (p0, p1, p2, ...)
        Assert.Equal("hello from C#", callbackArgs["p0"]?.GetValue<string>());
    }
 
    [Fact]
    public void Invoke_MethodWithAsyncCallback()
    {
        var handles = new HandleRegistry();
        var invoker = new TestCallbackInvoker { ResultToReturn = JsonValue.Create(42) };
        var (marshaller, callbackFactory) = CreateTestMarshallerWithCallbacks(handles, invoker);
        using var _ = callbackFactory;
        var dispatcher = new CapabilityDispatcher(handles, marshaller, [typeof(TestCapabilitiesWithCallback).Assembly]);
 
        var args = new JsonObject
        {
            ["callback"] = "async-callback"
        };
 
        var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/withAsyncCallback", args);
 
        Assert.NotNull(result);
        Assert.Equal(42, result.GetValue<int>());
    }
 
    [Fact]
    public void Invoke_MethodWithoutCallbackFactory_ThrowsForCallbackParameter()
    {
        var handles = new HandleRegistry();
        // No callback factory provided - use marshaller that throws on callback access
        var marshaller = CreateTestMarshaller(handles);
        var dispatcher = new CapabilityDispatcher(handles, marshaller, [typeof(TestCapabilitiesWithCallback).Assembly]);
 
        var args = new JsonObject
        {
            ["value"] = "test",
            ["callback"] = "some-callback"
        };
 
        var ex = Assert.Throws<CapabilityException>(() =>
            dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/withCallback", args));
 
        Assert.Equal(AtsErrorCodes.InvalidArgument, ex.Error.Code);
        Assert.Contains("callback", ex.Message.ToLower());
    }
 
    // Async capability handler tests
    [Fact]
    public void Constructor_ScansAsyncMethods()
    {
        var dispatcher = CreateDispatcher(typeof(TestCapabilities).Assembly);
 
        Assert.True(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/asyncVoid"));
        Assert.True(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/asyncWithResult"));
        Assert.True(dispatcher.HasCapability("Aspire.Hosting.RemoteHost.Tests/asyncThrows"));
    }
 
    [Fact]
    public void Invoke_HandlesAsyncVoidMethod()
    {
        var dispatcher = CreateDispatcher(typeof(TestCapabilities).Assembly);
        var args = new JsonObject { ["value"] = "test" };
 
        // Should not throw - async void (Task) methods complete successfully
        // Note: may return the awaited Task's internal result or null
        dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/asyncVoid", args);
    }
 
    [Fact]
    public void Invoke_HandlesAsyncMethodWithResult()
    {
        var dispatcher = CreateDispatcher(typeof(TestCapabilities).Assembly);
        var args = new JsonObject { ["value"] = "hello" };
 
        var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/asyncWithResult", args);
 
        Assert.NotNull(result);
        Assert.Equal("HELLO", result.GetValue<string>());
    }
 
    [Fact]
    public void Invoke_HandlesAsyncMethodThatThrows()
    {
        var dispatcher = CreateDispatcher(typeof(TestCapabilities).Assembly);
        var args = new JsonObject { ["value"] = "test" };
 
        var ex = Assert.Throws<CapabilityException>(() =>
            dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/asyncThrows", args));
 
        Assert.Equal(AtsErrorCodes.InternalError, ex.Error.Code);
        Assert.Contains("Async error", ex.Message);
    }
 
    // Array Marshalling tests
    [Fact]
    public void Invoke_AcceptsArrayParameter()
    {
        var dispatcher = CreateDispatcher(typeof(TestTypeCategoryCapabilities).Assembly);
        var args = new JsonObject
        {
            ["values"] = new JsonArray { 1, 2, 3, 4, 5 }
        };
 
        var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/sumArray", args);
 
        Assert.NotNull(result);
        Assert.Equal(15, result.GetValue<int>());
    }
 
    [Fact]
    public void Invoke_ReturnsArrayResult()
    {
        var dispatcher = CreateDispatcher(typeof(TestTypeCategoryCapabilities).Assembly);
        var args = new JsonObject { ["count"] = 3 };
 
        var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/returnArray", args);
 
        Assert.NotNull(result);
        var resultArray = result as JsonArray;
        Assert.NotNull(resultArray);
        Assert.Equal(3, resultArray.Count);
        Assert.Equal("item0", resultArray[0]?.GetValue<string>());
        Assert.Equal("item1", resultArray[1]?.GetValue<string>());
        Assert.Equal("item2", resultArray[2]?.GetValue<string>());
    }
 
    [Fact]
    public void Invoke_AcceptsReadOnlyListParameter()
    {
        var dispatcher = CreateDispatcher(typeof(TestTypeCategoryCapabilities).Assembly);
        var args = new JsonObject
        {
            ["values"] = new JsonArray { 10, 20, 30 }
        };
 
        var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/acceptReadOnlyList", args);
 
        Assert.NotNull(result);
        Assert.Equal(60, result.GetValue<int>());
    }
 
    // Union type tests
    [Fact]
    public void Invoke_AcceptsUnionWithString()
    {
        var dispatcher = CreateDispatcher(typeof(TestTypeCategoryCapabilities).Assembly);
        var args = new JsonObject { ["value"] = "hello" };
 
        var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/acceptUnion", args);
 
        Assert.NotNull(result);
        Assert.Equal("hello", result.GetValue<string>());
    }
 
    [Fact]
    public void Invoke_AcceptsUnionWithInt()
    {
        var dispatcher = CreateDispatcher(typeof(TestTypeCategoryCapabilities).Assembly);
        var args = new JsonObject { ["value"] = 42 };
 
        var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/acceptUnion", args);
 
        Assert.NotNull(result);
        Assert.Equal("42", result.GetValue<string>());
    }
 
    // List operations tests
    [Fact]
    public void Invoke_ReturnsMutableListAsHandle()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestTypeCategoryCapabilities).Assembly]);
 
        var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/returnMutableList", null);
 
        // Mutable lists should be returned as handles
        Assert.NotNull(result);
        var resultObj = result as JsonObject;
        Assert.NotNull(resultObj);
        Assert.True(resultObj.ContainsKey("$handle"));
    }
 
    [Fact]
    public void Invoke_ListGet_ReturnsItemAtIndex()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestTypeCategoryCapabilities).Assembly, typeof(AspireExportAttribute).Assembly]);
 
        // Create a list and get its handle
        var listResult = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/returnMutableList", null);
        var listHandle = (listResult as JsonObject)?["$handle"]?.GetValue<string>();
        Assert.NotNull(listHandle);
 
        // Get item at index 1
        var args = new JsonObject
        {
            ["list"] = new JsonObject { ["$handle"] = listHandle },
            ["index"] = 1
        };
 
        var result = dispatcher.Invoke("Aspire.Hosting/List.get", args);
 
        Assert.NotNull(result);
        Assert.Equal("second", result.GetValue<string>());
    }
 
    [Fact]
    public void Invoke_ListRemoveAt_RemovesItem()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestTypeCategoryCapabilities).Assembly, typeof(AspireExportAttribute).Assembly]);
 
        // Create a list and get its handle
        var listResult = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/returnMutableList", null);
        var listHandle = (listResult as JsonObject)?["$handle"]?.GetValue<string>();
        Assert.NotNull(listHandle);
 
        // Remove item at index 1 ("second")
        var removeArgs = new JsonObject
        {
            ["list"] = new JsonObject { ["$handle"] = listHandle },
            ["index"] = 1
        };
        var removeResult = dispatcher.Invoke("Aspire.Hosting/List.removeAt", removeArgs);
 
        Assert.NotNull(removeResult);
        Assert.True(removeResult.GetValue<bool>());
 
        // Verify length is now 2
        var lengthArgs = new JsonObject
        {
            ["list"] = new JsonObject { ["$handle"] = listHandle }
        };
        var lengthResult = dispatcher.Invoke("Aspire.Hosting/List.length", lengthArgs);
 
        Assert.NotNull(lengthResult);
        Assert.Equal(2, lengthResult.GetValue<int>());
    }
 
    [Fact]
    public void Invoke_ListLength_ReturnsCount()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestTypeCategoryCapabilities).Assembly, typeof(AspireExportAttribute).Assembly]);
 
        // Create a list and get its handle
        var listResult = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/returnMutableList", null);
        var listHandle = (listResult as JsonObject)?["$handle"]?.GetValue<string>();
        Assert.NotNull(listHandle);
 
        var args = new JsonObject
        {
            ["list"] = new JsonObject { ["$handle"] = listHandle }
        };
 
        var result = dispatcher.Invoke("Aspire.Hosting/List.length", args);
 
        Assert.NotNull(result);
        Assert.Equal(3, result.GetValue<int>());
    }
 
    [Fact]
    public void Invoke_ListClear_RemovesAllItems()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestTypeCategoryCapabilities).Assembly, typeof(AspireExportAttribute).Assembly]);
 
        // Create a list and get its handle
        var listResult = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/returnMutableList", null);
        var listHandle = (listResult as JsonObject)?["$handle"]?.GetValue<string>();
        Assert.NotNull(listHandle);
 
        // Clear the list
        var clearArgs = new JsonObject
        {
            ["list"] = new JsonObject { ["$handle"] = listHandle }
        };
        dispatcher.Invoke("Aspire.Hosting/List.clear", clearArgs);
 
        // Verify length is 0
        var lengthArgs = new JsonObject
        {
            ["list"] = new JsonObject { ["$handle"] = listHandle }
        };
        var lengthResult = dispatcher.Invoke("Aspire.Hosting/List.length", lengthArgs);
 
        Assert.NotNull(lengthResult);
        Assert.Equal(0, lengthResult.GetValue<int>());
    }
 
    // Dict operations tests
    [Fact]
    public void Invoke_ReturnsMutableDictAsHandle()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestTypeCategoryCapabilities).Assembly]);
 
        var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/returnMutableDict", null);
 
        // Mutable dicts should be returned as handles
        Assert.NotNull(result);
        var resultObj = result as JsonObject;
        Assert.NotNull(resultObj);
        Assert.True(resultObj.ContainsKey("$handle"));
    }
 
    [Fact]
    public void Invoke_DictGet_ReturnsValueForKey()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestTypeCategoryCapabilities).Assembly, typeof(AspireExportAttribute).Assembly]);
 
        // Create a dict and get its handle
        var dictResult = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/returnMutableDict", null);
        var dictHandle = (dictResult as JsonObject)?["$handle"]?.GetValue<string>();
        Assert.NotNull(dictHandle);
 
        // Get value for "key1"
        var args = new JsonObject
        {
            ["dict"] = new JsonObject { ["$handle"] = dictHandle },
            ["key"] = "key1"
        };
 
        var result = dispatcher.Invoke("Aspire.Hosting/Dict.get", args);
 
        Assert.NotNull(result);
        Assert.Equal("value1", result.GetValue<string>());
    }
 
    [Fact]
    public void Invoke_DictRemove_RemovesKey()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestTypeCategoryCapabilities).Assembly, typeof(AspireExportAttribute).Assembly]);
 
        // Create a dict and get its handle
        var dictResult = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/returnMutableDict", null);
        var dictHandle = (dictResult as JsonObject)?["$handle"]?.GetValue<string>();
        Assert.NotNull(dictHandle);
 
        // Remove "key1"
        var removeArgs = new JsonObject
        {
            ["dict"] = new JsonObject { ["$handle"] = dictHandle },
            ["key"] = "key1"
        };
        var removeResult = dispatcher.Invoke("Aspire.Hosting/Dict.remove", removeArgs);
 
        Assert.NotNull(removeResult);
        Assert.True(removeResult.GetValue<bool>());
 
        // Verify count is now 1
        var countArgs = new JsonObject
        {
            ["dict"] = new JsonObject { ["$handle"] = dictHandle }
        };
        var countResult = dispatcher.Invoke("Aspire.Hosting/Dict.count", countArgs);
 
        Assert.NotNull(countResult);
        Assert.Equal(1, countResult.GetValue<int>());
    }
 
    [Fact]
    public void Invoke_DictHas_ReturnsTrueForExistingKey()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestTypeCategoryCapabilities).Assembly, typeof(AspireExportAttribute).Assembly]);
 
        // Create a dict and get its handle
        var dictResult = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/returnMutableDict", null);
        var dictHandle = (dictResult as JsonObject)?["$handle"]?.GetValue<string>();
        Assert.NotNull(dictHandle);
 
        // Check for existing key
        var hasArgs = new JsonObject
        {
            ["dict"] = new JsonObject { ["$handle"] = dictHandle },
            ["key"] = "key1"
        };
        var hasResult = dispatcher.Invoke("Aspire.Hosting/Dict.has", hasArgs);
 
        Assert.NotNull(hasResult);
        Assert.True(hasResult.GetValue<bool>());
 
        // Check for non-existing key
        var hasArgs2 = new JsonObject
        {
            ["dict"] = new JsonObject { ["$handle"] = dictHandle },
            ["key"] = "nonexistent"
        };
        var hasResult2 = dispatcher.Invoke("Aspire.Hosting/Dict.has", hasArgs2);
 
        Assert.NotNull(hasResult2);
        Assert.False(hasResult2.GetValue<bool>());
    }
 
    [Fact]
    public void Invoke_DictKeys_ReturnsAllKeys()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestTypeCategoryCapabilities).Assembly, typeof(AspireExportAttribute).Assembly]);
 
        // Create a dict and get its handle
        var dictResult = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/returnMutableDict", null);
        var dictHandle = (dictResult as JsonObject)?["$handle"]?.GetValue<string>();
        Assert.NotNull(dictHandle);
 
        var args = new JsonObject
        {
            ["dict"] = new JsonObject { ["$handle"] = dictHandle }
        };
 
        var result = dispatcher.Invoke("Aspire.Hosting/Dict.keys", args);
 
        Assert.NotNull(result);
        var keysArray = result as JsonArray;
        Assert.NotNull(keysArray);
        Assert.Equal(2, keysArray.Count);
        Assert.Contains("key1", keysArray.Select(k => k?.GetValue<string>()));
        Assert.Contains("key2", keysArray.Select(k => k?.GetValue<string>()));
    }
 
    [Fact]
    public void Invoke_DictCount_ReturnsEntryCount()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestTypeCategoryCapabilities).Assembly, typeof(AspireExportAttribute).Assembly]);
 
        // Create a dict and get its handle
        var dictResult = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/returnMutableDict", null);
        var dictHandle = (dictResult as JsonObject)?["$handle"]?.GetValue<string>();
        Assert.NotNull(dictHandle);
 
        var args = new JsonObject
        {
            ["dict"] = new JsonObject { ["$handle"] = dictHandle }
        };
 
        var result = dispatcher.Invoke("Aspire.Hosting/Dict.count", args);
 
        Assert.NotNull(result);
        Assert.Equal(2, result.GetValue<int>());
    }
 
    [Fact]
    public void Invoke_AcceptEnum_DispatchesEnumFromString()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestEnumCapabilities).Assembly]);
 
        var args = new JsonObject
        {
            ["value"] = "ValueB" // Enum as string
        };
 
        var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/acceptEnum", args);
 
        Assert.NotNull(result);
        Assert.Equal("Received: ValueB", result.GetValue<string>());
    }
 
    [Fact]
    public void Invoke_AcceptEnum_DispatchesEnumFromStringCaseInsensitive()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestEnumCapabilities).Assembly]);
 
        var args = new JsonObject
        {
            ["value"] = "valuec" // lowercase
        };
 
        var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/acceptEnum", args);
 
        Assert.NotNull(result);
        Assert.Equal("Received: ValueC", result.GetValue<string>());
    }
 
    [Fact]
    public void Invoke_ReturnEnum_ReturnsEnumAsString()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestEnumCapabilities).Assembly]);
 
        var args = new JsonObject
        {
            ["name"] = "ValueA"
        };
 
        var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/returnEnum", args);
 
        Assert.NotNull(result);
        Assert.Equal("ValueA", result.GetValue<string>());
    }
 
    [Fact]
    public void Invoke_AcceptOptionalEnum_WithValue()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestEnumCapabilities).Assembly]);
 
        var args = new JsonObject
        {
            ["value"] = "ValueB"
        };
 
        var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/acceptOptionalEnum", args);
 
        Assert.NotNull(result);
        Assert.Equal("Received: ValueB", result.GetValue<string>());
    }
 
    [Fact]
    public void Invoke_AcceptOptionalEnum_WithoutValue()
    {
        var handles = new HandleRegistry();
        var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestEnumCapabilities).Assembly]);
 
        var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/acceptOptionalEnum", new JsonObject());
 
        Assert.NotNull(result);
        Assert.Equal("No value", result.GetValue<string>());
    }
 
    private static CapabilityDispatcher CreateDispatcher(params System.Reflection.Assembly[] assemblies)
    {
        var handles = new HandleRegistry();
        var marshaller = CreateTestMarshaller(handles);
        return new CapabilityDispatcher(handles, marshaller, assemblies);
    }
 
    private static AtsMarshaller CreateTestMarshaller(HandleRegistry? handles = null, CancellationTokenRegistry? ctRegistry = null)
    {
        handles ??= new HandleRegistry();
        ctRegistry ??= new CancellationTokenRegistry();
        var context = new AtsContext { Capabilities = [], HandleTypes = [], DtoTypes = [], EnumTypes = [] };
        return new AtsMarshaller(handles, context, ctRegistry, new Lazy<AtsCallbackProxyFactory>(() => throw new NotImplementedException()));
    }
 
    private static (AtsMarshaller Marshaller, AtsCallbackProxyFactory CallbackFactory) CreateTestMarshallerWithCallbacks(
        HandleRegistry handles,
        ICallbackInvoker invoker)
    {
        var ctRegistry = new CancellationTokenRegistry();
        var context = new AtsContext { Capabilities = [], HandleTypes = [], DtoTypes = [], EnumTypes = [] };
        AtsCallbackProxyFactory? callbackFactory = null;
        var marshaller = new AtsMarshaller(handles, context, ctRegistry, new Lazy<AtsCallbackProxyFactory>(() => callbackFactory!));
        callbackFactory = new AtsCallbackProxyFactory(invoker, handles, ctRegistry, marshaller);
        return (marshaller, callbackFactory);
    }
}
 
/// <summary>
/// Test capabilities for scanning.
/// </summary>
internal static class TestCapabilities
{
    [AspireExport("testMethod", Description = "Test method")]
    public static string TestMethod(string value)
    {
        return value.ToUpperInvariant();
    }
 
    [AspireExport("withOptional", Description = "Method with optional parameter")]
    public static string WithOptional(string required, string optional = "default")
    {
        return $"{required}:{optional}";
    }
 
    [AspireExport("asyncVoid", Description = "Async method returning Task")]
    public static async Task AsyncVoidMethod(string value)
    {
        await Task.Delay(1);
        _ = value; // Use the parameter to avoid warning
    }
 
    [AspireExport("asyncWithResult", Description = "Async method returning Task<T>")]
    public static async Task<string> AsyncWithResult(string value)
    {
        await Task.Delay(1);
        return value.ToUpperInvariant();
    }
 
    [AspireExport("asyncThrows", Description = "Async method that throws")]
    public static async Task<string> AsyncThrows(string value)
    {
        await Task.Delay(1);
        throw new InvalidOperationException("Async error: " + value);
    }
}
 
/// <summary>
/// Test context type for context type tests.
/// </summary>
[AspireExport(ExposeProperties = true)]
internal sealed class TestContextType
{
    public string Name { get; set; } = "default-name";
    public int Count { get; set; } = 42;
    public bool IsEnabled { get; set; } = true;
 
    // This property should be skipped - IDisposable is not ATS-compatible
    public IDisposable? NonAtsProperty { get; set; }
}
 
/// <summary>
/// Test context type to verify context properties work.
/// </summary>
[AspireExport(ExposeProperties = true)]
internal sealed class VersionedContextType
{
    public string Value { get; set; } = "v2";
}
 
/// <summary>
/// Test nested context type - simulates DistributedApplicationExecutionContext.
/// This type is used as a property type in another context type.
/// </summary>
[AspireExport(ExposeProperties = true)]
internal sealed class TestNestedContextType
{
    public string Operation { get; set; } = "Run";
    public bool IsPublishMode { get; set; }
}
 
/// <summary>
/// Test parent context type - simulates EnvironmentCallbackContext.
/// Has a property whose type is another context type (TestNestedContextType).
/// </summary>
[AspireExport(ExposeProperties = true)]
internal sealed class TestParentContextType
{
    public TestParentContextType(TestNestedContextType nestedContext)
    {
        NestedContext = nestedContext ?? throw new ArgumentNullException(nameof(nestedContext));
    }
 
    /// <summary>
    /// Property whose type is another context type - simulates ExecutionContext property.
    /// </summary>
    public TestNestedContextType NestedContext { get; }
 
    public string Name { get; set; } = "parent";
}
 
/// <summary>
/// Test type for instance method tests.
/// </summary>
[AspireExport(ExposeMethods = true)]
internal sealed class TestTypeWithMethods
{
    public bool WasCalled { get; private set; }
 
    public void DoSomething()
    {
        WasCalled = true;
    }
 
#pragma warning disable CA1822 // Mark members as static - testing instance methods
    public int CalculateSum(int a, int b)
    {
        return a + b;
    }
 
    public async Task<string> ProcessAsync(string input)
    {
        await Task.Delay(1);
        return input.ToUpperInvariant();
    }
 
    // This should NOT be exposed - private method
#pragma warning disable IDE0051 // Remove unused private member - testing that private methods are not exposed
    private void PrivateMethod()
    {
    }
#pragma warning restore IDE0051
#pragma warning restore CA1822
}
 
/// <summary>
/// Test capabilities with callback parameters.
/// </summary>
internal static class TestCapabilitiesWithCallback
{
    /// <summary>
    /// A method that accepts a callback but doesn't invoke it.
    /// </summary>
    [AspireExport("withCallback", Description = "Method with callback parameter")]
    public static string WithCallback(string value, Action callback)
    {
        // The callback is provided but we don't invoke it in this test
        _ = callback;
        return $"PROCESSED: {value}";
    }
 
    /// <summary>
    /// A method that invokes the callback.
    /// </summary>
    [AspireExport("invokeCallback", Description = "Method that invokes callback")]
    public static void InvokeCallback(Func<Task> callback)
    {
        callback().GetAwaiter().GetResult();
    }
 
    /// <summary>
    /// A method that invokes a typed callback with arguments.
    /// </summary>
    [AspireExport("invokeTypedCallback", Description = "Method that invokes typed callback")]
    public static void InvokeTypedCallback(Func<string, Task> callback)
    {
        callback("hello from C#").GetAwaiter().GetResult();
    }
 
    /// <summary>
    /// A method with an async callback that returns a value.
    /// </summary>
    [AspireExport("withAsyncCallback", Description = "Method with async callback returning value")]
    public static int WithAsyncCallback(Func<Task<int>> callback)
    {
        return callback().GetAwaiter().GetResult();
    }
}
 
/// <summary>
/// Test capabilities for Array, Union type, List, and Dict marshalling.
/// </summary>
internal static class TestTypeCategoryCapabilities
{
    [AspireExport("sumArray", Description = "Sums an integer array")]
    public static int SumArray(int[] values)
    {
        return values.Sum();
    }
 
    [AspireExport("returnArray", Description = "Returns a string array")]
    public static string[] ReturnArray(int count)
    {
        return Enumerable.Range(0, count).Select(i => $"item{i}").ToArray();
    }
 
    [AspireExport("acceptReadOnlyList", Description = "Accepts a readonly list")]
    public static int SumReadOnlyList(IReadOnlyList<int> values)
    {
        return values.Sum();
    }
 
    [AspireExport("acceptUnion", Description = "Accepts a union of string or int")]
    public static string AcceptUnion([AspireUnion(typeof(string), typeof(int))] object value)
    {
        return value.ToString()!;
    }
 
    [AspireExport("returnMutableList", Description = "Returns a mutable List<string>")]
    public static List<object> ReturnMutableList()
    {
        return ["first", "second", "third"];
    }
 
    [AspireExport("returnMutableDict", Description = "Returns a mutable Dictionary<string, object>")]
    public static Dictionary<string, object> ReturnMutableDict()
    {
        return new Dictionary<string, object>
        {
            ["key1"] = "value1",
            ["key2"] = 42
        };
    }
}
 
/// <summary>
/// Test enum for enum dispatch tests.
/// </summary>
internal enum TestDispatchEnum
{
    ValueA,
    ValueB,
    ValueC
}
 
/// <summary>
/// Test capabilities for enum dispatch.
/// </summary>
internal static class TestEnumCapabilities
{
    [AspireExport("acceptEnum", Description = "Accepts an enum parameter")]
    public static string AcceptEnum(TestDispatchEnum value)
    {
        return $"Received: {value}";
    }
 
    [AspireExport("returnEnum", Description = "Returns an enum value")]
    public static TestDispatchEnum ReturnEnum(string name)
    {
        return Enum.Parse<TestDispatchEnum>(name);
    }
 
    [AspireExport("acceptOptionalEnum", Description = "Accepts an optional enum parameter")]
    public static string AcceptOptionalEnum(TestDispatchEnum? value = null)
    {
        return value.HasValue ? $"Received: {value.Value}" : "No value";
    }
}