diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/VirtualTags/VirtualTagHostActorTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/VirtualTags/VirtualTagHostActorTests.cs index 76bc7396..9d6838a2 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/VirtualTags/VirtualTagHostActorTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/VirtualTags/VirtualTagHostActorTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using Akka.Actor; using Shouldly; using Xunit; @@ -191,7 +192,8 @@ public sealed class VirtualTagHostActorTests : RuntimeActorTestBase // Wait for the history record to land (delivered on the same actor turn as the publish). AwaitAssert(() => writer.Calls.Count.ShouldBe(1)); - var (path, value) = writer.Calls[0]; + writer.Calls.TryPeek(out var captured).ShouldBeTrue(); + var (path, value) = captured; path.ShouldBe("eq-1/speed-rpm"); value.Value.ShouldBe(42.0); value.StatusCode.ShouldBe(0u); // OPC UA Good @@ -213,11 +215,10 @@ public sealed class VirtualTagHostActorTests : RuntimeActorTestBase host.Tell(new VirtualTagActor.EvaluationResult("vt-1", 42.0, DateTime.UtcNow, CorrelationId.NewId())); - // The value is still published… + // The value is still published — and the publish completing means OnResult's turn is done, + // so the historian (which would have been called on the same turn) was never touched. publish.ExpectMsg(); - // …but the historian was never touched. - publish.ExpectNoMsg(TimeSpan.FromMilliseconds(200)); - writer.Calls.ShouldBeEmpty(); + writer.Calls.IsEmpty.ShouldBeTrue(); } /// A plan with an explicit Expression + DependencyRefs (the H1b in-place-change case). @@ -348,12 +349,14 @@ public sealed class VirtualTagHostActorTests : RuntimeActorTestBase /// assert the host historizes (or does not) and with what path + snapshot. private sealed class CapturingHistoryWriter : IHistoryWriter { - public readonly List<(string Path, DataValueSnapshot Value)> Calls = new(); + // ConcurrentQueue: Record runs on the actor thread, the test asserts on the test thread — + // a plain List would be a cross-thread data race under load. + public readonly ConcurrentQueue<(string Path, DataValueSnapshot Value)> Calls = new(); /// Captures the path + snapshot of a Record call. /// The virtual tag path. /// The data value snapshot. - public void Record(string path, DataValueSnapshot value) => Calls.Add((path, value)); + public void Record(string path, DataValueSnapshot value) => Calls.Enqueue((path, value)); } /// Deterministic no-op evaluator: keeps spawned children inert so tests drive the host's