diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.IntegrationTests/RetentionTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.IntegrationTests/RetentionTests.cs index 50870b4a..35309fdb 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.IntegrationTests/RetentionTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.IntegrationTests/RetentionTests.cs @@ -195,16 +195,33 @@ public sealed class RetentionTests(SqlHistorianFixture fx) cmd.CommandText = $"SELECT COUNT(*) FROM [{s}].Sample WITH (TABLOCKX, HOLDLOCK)"; await cmd.ExecuteScalarAsync(); } + short holderSpid; + await using (var cmd = holder.CreateCommand()) + { + cmd.Transaction = tx; + cmd.CommandText = "SELECT CAST(@@SPID AS smallint)"; + holderSpid = (short)(await cmd.ExecuteScalarAsync())!; + } using var cts = new CancellationTokenSource(); var purger = new SqlServerHistorianRetention(c, b, TimeSpan.FromHours(1), 30, 4000, time: new FixedTime(Now)); var purge = purger.PurgeAsync(cts.Token); - await Task.Delay(500); - purge.IsCompleted.ShouldBeFalse("the DELETE must be blocked behind the holder's table lock"); + + // Cancel only once the server reports a DELETE actually blocked behind the holder — a fixed delay could + // cancel during bootstrap/applock acquisition on a slow host and never exercise the mid-command path. + var deadline = DateTime.UtcNow.AddSeconds(15); + while (await CountAsync( + $"SELECT COUNT(*) FROM sys.dm_exec_requests WHERE blocking_session_id = {holderSpid} AND command = 'DELETE'") == 0) + { + purge.IsCompleted.ShouldBeFalse("the purge finished without ever blocking on the holder's table lock"); + DateTime.UtcNow.ShouldBeLessThan(deadline, "the purge's DELETE never blocked behind the holder"); + await Task.Delay(50); + } cts.Cancel(); - // SqlClient may surface the attention as a SqlException rather than OperationCanceledException. - await Should.ThrowAsync(() => purge); + // SqlClient surfaces the attention as OperationCanceledException or as a SqlException. + var ex = await Should.ThrowAsync(() => purge); + (ex is OperationCanceledException or SqlException).ShouldBeTrue($"unexpected {ex.GetType().Name}: {ex.Message}"); await tx.RollbackAsync(); (await LockIsFreeAsync(s)).ShouldBeTrue();