tests/Client/PendingCommandRegistryTests.cs

Unit tests for PendingCommandRegistry in the client layer. Exercises registration, correlation of results to requests, timeouts, caller cancellation, disposal behavior, and pending-request limits.

#nullable enable

using System;
using System.Threading;
using System.Threading.Tasks;
using Hexagon.V2.Client;
using Hexagon.V2.Kernel;

namespace Hexagon.V2.Tests.Client;

[TestClass]
public sealed class PendingCommandRegistryTests
{
	[TestMethod]
	public async Task CorrelatedResultsCompleteOnlyTheirMatchingRequests()
	{
		using var registry = new PendingCommandRegistry( TimeSpan.FromSeconds( 5 ) );
		var first = registry.Register().Value;
		var second = registry.Register().Value;
		Assert.IsFalse( first.Completion.IsCompleted );
		Assert.IsFalse( second.Completion.IsCompleted );

		var denied = OperationResult.Failure( ErrorCode.PolicyDenied, "Denied." );
		Assert.IsTrue( registry.Complete( second.RequestId, denied ) );
		Assert.AreEqual( ErrorCode.PolicyDenied, (await second.Completion).Error!.Code );
		Assert.IsFalse( first.Completion.IsCompleted );

		Assert.IsTrue( registry.Complete( first.RequestId, OperationResult.Success() ) );
		Assert.IsTrue( (await first.Completion).Succeeded );
		Assert.AreEqual( 0, registry.Count );
	}

	[TestMethod]
	public async Task TimeoutProducesAStableFailureAndRemovesPendingRequest()
	{
		using var registry = new PendingCommandRegistry( TimeSpan.FromMilliseconds( 25 ) );
		var pending = registry.Register().Value;

		var result = await pending.Completion;

		Assert.IsTrue( result.Failed );
		Assert.AreEqual( ErrorCode.InternalError, result.Error!.Code );
		StringAssert.Contains( result.Error.Message, "timed out" );
		Assert.AreEqual( 0, registry.Count );
		Assert.IsFalse( registry.Complete( pending.RequestId, OperationResult.Success() ) );
	}

	[TestMethod]
	public async Task CallerCancellationCancelsCompletionAndRemovesPendingRequest()
	{
		using var cancellation = new CancellationTokenSource();
		using var registry = new PendingCommandRegistry( TimeSpan.FromSeconds( 5 ) );
		var pending = registry.Register( cancellation.Token ).Value;

		cancellation.Cancel();

		await Assert.ThrowsExactlyAsync<TaskCanceledException>( () => pending.Completion.AsTask() );
		Assert.AreEqual( 0, registry.Count );
	}

	[TestMethod]
	public async Task DisposeCompletesEveryOutstandingRequest()
	{
		var registry = new PendingCommandRegistry( TimeSpan.FromSeconds( 5 ) );
		var first = registry.Register().Value;
		var second = registry.Register().Value;

		registry.Dispose();

		Assert.IsTrue( (await first.Completion).Failed );
		Assert.IsTrue( (await second.Completion).Failed );
		Assert.AreEqual( 0, registry.Count );
	}

	[TestMethod]
	public void PendingLimitFailsBeforeAllocatingAnotherRequest()
	{
		using var registry = new PendingCommandRegistry( TimeSpan.FromSeconds( 5 ), 1 );
		Assert.IsTrue( registry.Register().Succeeded );

		var rejected = registry.Register();

		Assert.IsTrue( rejected.Failed );
		Assert.AreEqual( ErrorCode.Conflict, rejected.Error!.Code );
		Assert.AreEqual( 1, registry.Count );
	}
}