From 37664e69927406090065b94b9844bc28bfaea4d5 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 13 Jul 2026 10:14:50 -0400 Subject: [PATCH] =?UTF-8?q?test(runtime):=20red=20=E2=80=94=20apply-bounda?= =?UTF-8?q?ry=20compile-cache=20clear=20must=20be=20gated=20on=20a=20chang?= =?UTF-8?q?ed=20expression=20set=20(02/P7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...2-03-vt-failure-quality-plan.md.tasks.json | 2 +- .../VirtualTags/VirtualTagHostActorTests.cs | 50 +++++++++++++++---- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/archreview/plans/R2-03-vt-failure-quality-plan.md.tasks.json b/archreview/plans/R2-03-vt-failure-quality-plan.md.tasks.json index 595d60b5..8b4e615f 100644 --- a/archreview/plans/R2-03-vt-failure-quality-plan.md.tasks.json +++ b/archreview/plans/R2-03-vt-failure-quality-plan.md.tasks.json @@ -63,7 +63,7 @@ { "id": "R2-03-T9", "subject": "P7 wiring-guard rewrite (RED): ApplyVirtualTags clears the compile cache only when the expression set changes — identical redeploy must NOT clear; expression edit must; shared-expression removal must not", - "status": "pending", + "status": "completed", "blockedBy": [ "R2-03-T3" ] 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 1741028f..e71a290a 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 @@ -345,24 +345,52 @@ public sealed class VirtualTagHostActorTests : RuntimeActorTestBase mux.LastSender.ShouldNotBe(firstChild); } - /// Wiring guard (arch-review 02/U3, theme #1): every ApplyVirtualTags calls - /// on the evaluator at the apply boundary. - /// This is the load-bearing wire — the production evaluator roots one collectible ALC per compiled - /// script, and without this per-generation clear they accrete across deploys. A pure unit test of - /// the evaluator can't catch a missing call here; this asserts the host actor actually makes it. + /// Wiring guard (arch-review 02/U3 + 02/P7, theme #1): the host actor calls + /// at the apply boundary — but, per P7, ONLY + /// when the deployed expression set actually changed. The clear stays the load-bearing wire (the + /// production evaluator roots one collectible ALC per compiled script), yet a byte-identical + /// redeploy — or a rename / add / remove that leaves the expression set unchanged — no longer forces + /// a full recompilation. This pins that the host makes the call, and gates it correctly. + /// + /// The final ClearCount is read at a deterministic sync point: after all applies are + /// queued we drive one EvaluationResult through the actor and wait for the bridged publish — the + /// FIFO mailbox guarantees every prior apply is processed by then, so the counter is final (a + /// polling AwaitAssert on a monotonic counter would pass transiently as it climbs, hiding the + /// unconditional-clear bug). [Fact] - public void ApplyVirtualTags_clears_the_evaluator_compile_cache_each_generation() + public void ApplyVirtualTags_clears_the_compile_cache_only_when_the_expression_set_changes() { var publish = CreateTestProbe(); var evaluator = new CacheOwningStubEvaluator(); var host = Sys.ActorOf(VirtualTagHostActor.Props(publish.Ref, mux: null, evaluator)); - host.Tell(new VirtualTagHostActor.ApplyVirtualTags(new[] { Plan("vt-1", "eq-1", "speed-rpm") })); - AwaitAssert(() => evaluator.ClearCount.ShouldBe(1)); + // 1) First generation: previous set {} → {E1} differs → clears. + host.Tell(new VirtualTagHostActor.ApplyVirtualTags( + new[] { PlanWithRefs("vt-1", "eq-1", "speed-rpm", "E1", "a") })); + // 2) Byte-identical redeploy: {E1} → {E1} → NO clear (the P7 win). + host.Tell(new VirtualTagHostActor.ApplyVirtualTags( + new[] { PlanWithRefs("vt-1", "eq-1", "speed-rpm", "E1", "a") })); + // 3) Edited expression: {E1} → {E2} differs → clears. + host.Tell(new VirtualTagHostActor.ApplyVirtualTags( + new[] { PlanWithRefs("vt-1", "eq-1", "speed-rpm", "E2", "a") })); + // 4) Add a second vtag SHARING the same expression E2: {E2} → {E2} → NO clear. + host.Tell(new VirtualTagHostActor.ApplyVirtualTags(new[] + { + PlanWithRefs("vt-1", "eq-1", "speed-rpm", "E2", "a"), + PlanWithRefs("vt-2", "eq-1", "torque", "E2", "a"), + })); + // 5) Remove that second vtag — expression E2 still deployed by vt-1: {E2} → {E2} → NO clear. + host.Tell(new VirtualTagHostActor.ApplyVirtualTags( + new[] { PlanWithRefs("vt-1", "eq-1", "speed-rpm", "E2", "a") })); - // A second apply generation clears again — proves it's per-generation, not one-shot. - host.Tell(new VirtualTagHostActor.ApplyVirtualTags(new[] { Plan("vt-1", "eq-1", "speed-rpm") })); - AwaitAssert(() => evaluator.ClearCount.ShouldBe(2)); + // Sync point: a result for the still-mapped vt-1 bridges to a publish; when it lands, every + // apply above has been processed. + host.Tell(new VirtualTagActor.EvaluationResult("vt-1", 1.0, DateTime.UtcNow, CorrelationId.NewId())); + publish.ExpectMsg(); + + // Exactly two clears: the first generation and the E1→E2 edit. The identical redeploy and the + // shared-expression add/remove did not clear. + evaluator.ClearCount.ShouldBe(2); } ///