fix(vtags): respawn equipment virtualtag child on in-place plan change (H1b, stillpending §1)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user