fix(vtags): respawn equipment virtualtag child on in-place plan change (H1b, stillpending §1)

This commit is contained in:
Joseph Doherty
2026-06-15 10:05:29 -04:00
parent 1dc713693a
commit ada01e1af8
2 changed files with 122 additions and 7 deletions
@@ -153,6 +153,96 @@ public sealed class VirtualTagHostActorTests : RuntimeActorTestBase
secondChild.ShouldNotBe(firstChild);
}
/// <summary>A plan with an explicit Expression + DependencyRefs (the H1b in-place-change case).</summary>
private static EquipmentVirtualTagPlan PlanWithRefs(
string vtagId, string equipmentId, string name, string expression, params string[] refs) =>
new(
VirtualTagId: vtagId,
EquipmentId: equipmentId,
FolderPath: "",
Name: name,
DataType: "Double",
Expression: expression,
DependencyRefs: refs);
/// <summary>
/// H1b respawn-on-change: re-applying the SAME vtagId with a changed Expression/DependencyRefs
/// must stop the old child (PostStop ⇒ UnregisterInterest on the old refs) and spawn a fresh one
/// (PreStart ⇒ RegisterInterest on the new refs). Proof: after the second apply the mux probe sees
/// an UnregisterInterest and a RegisterInterest carrying the NEW ref "B".
/// </summary>
[Fact]
public void ApplyVirtualTags_respawns_child_when_plan_changes_in_place()
{
var publish = CreateTestProbe();
var mux = CreateTestProbe();
var host = Sys.ActorOf(VirtualTagHostActor.Props(publish.Ref, mux.Ref, new StubEvaluator()));
// First apply — child self-registers interest in "A".
host.Tell(new VirtualTagHostActor.ApplyVirtualTags(
new[] { PlanWithRefs("vt-1", "eq-1", "speed-rpm", "ctx.GetTag(\"A\")", "A") }));
var reg1 = mux.ExpectMsg<DependencyMuxActor.RegisterInterest>();
reg1.TagRefs.ShouldContain("A");
var firstChild = mux.LastSender;
// Re-apply the SAME vtagId with a changed Expression + DependencyRefs.
host.Tell(new VirtualTagHostActor.ApplyVirtualTags(
new[] { PlanWithRefs("vt-1", "eq-1", "speed-rpm", "ctx.GetTag(\"B\")", "B") }));
// The old child is stopped (PostStop ⇒ UnregisterInterest) and a new one spawned
// (PreStart ⇒ RegisterInterest on "B"). Both messages arrive at the mux probe; order between
// the dying child's PostStop and the new child's PreStart is not guaranteed, so accept either.
DependencyMuxActor.RegisterInterest? reg2 = null;
var sawUnregister = false;
for (var i = 0; i < 2; i++)
{
var msg = mux.ReceiveOne(TimeSpan.FromSeconds(5));
switch (msg)
{
case DependencyMuxActor.RegisterInterest r:
reg2 = r;
break;
case DependencyMuxActor.UnregisterInterest:
sawUnregister = true;
break;
}
}
sawUnregister.ShouldBeTrue("old child's PostStop should have unregistered its interest");
reg2.ShouldNotBeNull();
reg2!.TagRefs.ShouldContain("B");
reg2.TagRefs.ShouldNotContain("A");
// The replacement is a different actor ref than the original (auto-named, so no collision).
mux.LastSender.ShouldNotBe(firstChild);
}
/// <summary>
/// H1b no-churn: re-applying an IDENTICAL plan must NOT respawn the child — the plan's value
/// equality diffs empty, so no second Unregister/Register pair hits the mux.
/// </summary>
[Fact]
public void ApplyVirtualTags_does_not_respawn_child_when_plan_unchanged()
{
var publish = CreateTestProbe();
var mux = CreateTestProbe();
var host = Sys.ActorOf(VirtualTagHostActor.Props(publish.Ref, mux.Ref, new StubEvaluator()));
var plan = new[] { PlanWithRefs("vt-1", "eq-1", "speed-rpm", "ctx.GetTag(\"A\")", "A") };
// First apply — exactly one RegisterInterest.
host.Tell(new VirtualTagHostActor.ApplyVirtualTags(plan));
var reg = mux.ExpectMsg<DependencyMuxActor.RegisterInterest>();
reg.TagRefs.ShouldContain("A");
// Re-apply an identical plan (fresh list instances, but value-equal) — no churn expected.
host.Tell(new VirtualTagHostActor.ApplyVirtualTags(
new[] { PlanWithRefs("vt-1", "eq-1", "speed-rpm", "ctx.GetTag(\"A\")", "A") }));
// No second Register and no Unregister: the child was left in place.
mux.ExpectNoMsg(TimeSpan.FromMilliseconds(400));
}
/// <summary>Deterministic no-op evaluator: keeps spawned children inert so tests drive the host's
/// OnResult path directly via synthetic EvaluationResults.</summary>
private sealed class StubEvaluator : IVirtualTagEvaluator