diff --git a/TUnit.Engine/Framework/TUnitServiceProvider.cs b/TUnit.Engine/Framework/TUnitServiceProvider.cs index 2799b12d17..92e6a5ac82 100644 --- a/TUnit.Engine/Framework/TUnitServiceProvider.cs +++ b/TUnit.Engine/Framework/TUnitServiceProvider.cs @@ -57,7 +57,7 @@ public ITestExecutionFilter? Filter public CancellationTokenSource FailFastCancellationSource { get; } public ParallelLimitLockProvider ParallelLimitLockProvider { get; } public ObjectLifecycleService ObjectLifecycleService { get; } - public bool AfterSessionHooksFailed { get; set; } + public bool SessionFailed { get; set; } [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Reflection mode is not used in AOT/trimmed scenarios")] [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Reflection mode is not used in AOT scenarios")] diff --git a/TUnit.Engine/Framework/TUnitTestFramework.cs b/TUnit.Engine/Framework/TUnitTestFramework.cs index f2a334056a..c8f8acdd07 100644 --- a/TUnit.Engine/Framework/TUnitTestFramework.cs +++ b/TUnit.Engine/Framework/TUnitTestFramework.cs @@ -69,24 +69,23 @@ public async Task ExecuteRequestAsync(ExecuteRequestContext context) } catch (Exception e) when (IsCancellationException(e)) { - // Check if this is a normal cancellation or fail-fast cancellation - if (context.CancellationToken.IsCancellationRequested) - { - await GetOrCreateServiceProvider(context).Logger.LogErrorAsync("The test run was cancelled."); - } - else - { - // This is likely a fail-fast cancellation - await GetOrCreateServiceProvider(context).Logger.LogErrorAsync("Test execution stopped due to fail-fast."); - } + var message = context.CancellationToken.IsCancellationRequested + ? "The test run was cancelled." + : "Test execution stopped due to fail-fast."; + await GetOrCreateServiceProvider(context).Logger.LogErrorAsync(message); + // Re-throw is safe here — MTP handles OperationCanceledException specially. throw; } catch (Exception e) { - await GetOrCreateServiceProvider(context).Logger.LogErrorAsync(e); + var serviceProvider = GetOrCreateServiceProvider(context); + await serviceProvider.Logger.LogErrorAsync(e); await ReportUnhandledException(context, e); - throw; + + // Do NOT re-throw — MTP hosts expect errors via CloseTestSessionResult, + // not propagated exceptions. Re-throwing breaks JSON-RPC transports (#5263). + serviceProvider.SessionFailed = true; } finally { @@ -100,8 +99,7 @@ public async Task CloseTestSessionAsync(CloseTestSession if (_serviceProvidersPerSession.TryRemove(context.SessionUid.Value, out var serviceProvider)) { - // Check if After(TestSession) hooks failed - if (serviceProvider.AfterSessionHooksFailed) + if (serviceProvider.SessionFailed) { isSuccess = false; } diff --git a/TUnit.Engine/TestSessionCoordinator.cs b/TUnit.Engine/TestSessionCoordinator.cs index bcecb5e828..398378b947 100644 --- a/TUnit.Engine/TestSessionCoordinator.cs +++ b/TUnit.Engine/TestSessionCoordinator.cs @@ -99,10 +99,9 @@ private async Task ExecuteTestsCore(List testList, Cance // Schedule and execute tests (batch approach to preserve ExecutionContext) var success = await _testScheduler.ScheduleAndExecuteAsync(testList, linkedCts.Token); - // Track whether After(TestSession) hooks failed if (!success) { - _serviceProvider.AfterSessionHooksFailed = true; + _serviceProvider.SessionFailed = true; } }