tests/Kernel/AsyncOperationCaptureTests.cs

Unit tests for AsyncOperation helpers in Hexagon.V2.Kernel. Verifies Capture and CaptureSynchronous behavior for synchronous and asynchronous success, exceptions, AggregateException unwrapping, cancellation, and ValueTask fast-paths.

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

namespace Hexagon.V2.Tests.Kernel;

[TestClass]
public sealed class AsyncOperationCaptureTests
{
	[TestMethod]
	public async Task SynchronousThrowIsCapturedAsAFailureOutcome()
	{
		var thrown = new InvalidOperationException( "boom" );
		var outcome = await AsyncOperation.Capture( () => throw thrown );

		Assert.IsFalse( outcome.Succeeded );
		Assert.AreSame( thrown, outcome.Exception );
	}

	[TestMethod]
	public async Task SynchronousThrowIsCapturedAsAFailureOutcomeWithValue()
	{
		var thrown = new InvalidOperationException( "boom" );
		var outcome = await AsyncOperation.Capture<int>( () => throw thrown );

		Assert.IsFalse( outcome.Succeeded );
		Assert.AreSame( thrown, outcome.Exception );
	}

	[TestMethod]
	public void CompletedOperationTakesTheSynchronousFastPath()
	{
		var task = AsyncOperation.Capture( () => ValueTask.CompletedTask );

		Assert.IsTrue( task.IsCompletedSuccessfully, "A completed ValueTask must not allocate a continuation." );
		Assert.IsTrue( task.Result.Succeeded );
	}

	[TestMethod]
	public void CompletedOperationWithValueTakesTheSynchronousFastPath()
	{
		var task = AsyncOperation.Capture( () => ValueTask.FromResult( 42 ) );

		Assert.IsTrue( task.IsCompletedSuccessfully, "A completed ValueTask must not allocate a continuation." );
		Assert.IsTrue( task.Result.Succeeded );
		Assert.AreEqual( 42, task.Result.Value );
	}

	[TestMethod]
	public async Task AsynchronousFaultIsUnwrappedFromItsAggregateException()
	{
		var thrown = new InvalidOperationException( "inner" );
		var outcome = await AsyncOperation.Capture(
			() => new ValueTask( Task.FromException( thrown ) ) );

		Assert.IsFalse( outcome.Succeeded );
		Assert.AreSame( thrown, outcome.Exception );
	}

	[TestMethod]
	public async Task AsynchronousFaultWithValueIsUnwrappedFromItsAggregateException()
	{
		var thrown = new InvalidOperationException( "inner" );
		var outcome = await AsyncOperation.Capture(
			() => new ValueTask<int>( Task.FromException<int>( thrown ) ) );

		Assert.IsFalse( outcome.Succeeded );
		Assert.AreSame( thrown, outcome.Exception );
	}

	[TestMethod]
	public async Task ThrowAfterAnAwaitPointIsCaptured()
	{
		var thrown = new InvalidOperationException( "late" );
		var outcome = await AsyncOperation.Capture( async () =>
		{
			await Task.Yield();
			throw thrown;
		} );

		Assert.IsFalse( outcome.Succeeded );
		Assert.AreSame( thrown, outcome.Exception );
	}

	[TestMethod]
	public async Task CancellationBecomesAFailureOutcomeInsteadOfAThrow()
	{
		var canceled = new CancellationToken( canceled: true );
		var outcome = await AsyncOperation.Capture(
			() => new ValueTask( Task.FromCanceled( canceled ) ) );

		Assert.IsFalse( outcome.Succeeded );
		Assert.IsInstanceOfType<OperationCanceledException>( outcome.Exception );
	}

	[TestMethod]
	public async Task CancellationWithValueBecomesAFailureOutcomeInsteadOfAThrow()
	{
		var canceled = new CancellationToken( canceled: true );
		var outcome = await AsyncOperation.Capture(
			() => new ValueTask<int>( Task.FromCanceled<int>( canceled ) ) );

		Assert.IsFalse( outcome.Succeeded );
		Assert.IsInstanceOfType<OperationCanceledException>( outcome.Exception );
	}

	[TestMethod]
	public void CaptureSynchronousConvertsAThrowingActionIntoAFailure()
	{
		var thrown = new InvalidOperationException( "boom" );
		var outcome = AsyncOperation.CaptureSynchronous( () => throw thrown );

		Assert.IsFalse( outcome.Succeeded );
		Assert.AreSame( thrown, outcome.Exception );
	}

	[TestMethod]
	public void CaptureSynchronousReturnsTheProducedValueOnSuccess()
	{
		var outcome = AsyncOperation.CaptureSynchronous( () => 42 );

		Assert.IsTrue( outcome.Succeeded );
		Assert.AreEqual( 42, outcome.Value );
	}
}