merge: equipment-namespace live values (VirtualTag route)
v2-ci / build (push) Failing after 36s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
v2-ci / build (push) Failing after 36s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
This commit is contained in:
@@ -251,6 +251,50 @@ public sealed class DeploymentArtifactTests
|
||||
c.GalaxyTags.ShouldContain(g => g.TagId == "tag-gx");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies ParseComposition surfaces Equipment-namespace VirtualTags (joined to their Script
|
||||
/// by ScriptId for the expression source) as <c>EquipmentVirtualTags</c>, with the
|
||||
/// <c>DependencyRefs</c> extracted from the script's <c>ctx.GetTag("…")</c> literals — the
|
||||
/// artifact-decode mirror of <c>Phase7Composer.Compose</c>'s VirtualTag producer.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ParseComposition_reads_EquipmentVirtualTags_from_virtualtags_and_scripts()
|
||||
{
|
||||
var blob = JsonSerializer.SerializeToUtf8Bytes(new
|
||||
{
|
||||
Scripts = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
ScriptId = "scr-1",
|
||||
SourceCode = "return ctx.GetTag(\"TestMachine_001.TestDouble\").Value;",
|
||||
},
|
||||
},
|
||||
VirtualTags = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
VirtualTagId = "vt-1",
|
||||
EquipmentId = "eq-1",
|
||||
Name = "Doubled",
|
||||
DataType = "Float",
|
||||
ScriptId = "scr-1",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var c = DeploymentArtifact.ParseComposition(blob);
|
||||
|
||||
var vt = c.EquipmentVirtualTags.ShouldHaveSingleItem();
|
||||
vt.VirtualTagId.ShouldBe("vt-1");
|
||||
vt.EquipmentId.ShouldBe("eq-1");
|
||||
vt.Name.ShouldBe("Doubled");
|
||||
vt.DataType.ShouldBe("Float");
|
||||
vt.FolderPath.ShouldBe("");
|
||||
vt.Expression.ShouldBe("return ctx.GetTag(\"TestMachine_001.TestDouble\").Value;");
|
||||
vt.DependencyRefs.ShouldBe(new[] { "TestMachine_001.TestDouble" });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies ParseComposition sets the equipment folder DisplayName to the UNS <c>Name</c>
|
||||
/// segment — the source the live rebuild actually uses — not the colloquial MachineCode, so
|
||||
@@ -377,6 +421,47 @@ public sealed class DeploymentArtifactTests
|
||||
comp.DriverInstancePlans.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>Verifies the cluster-scoped overload keeps only EquipmentVirtualTags whose EquipmentId
|
||||
/// belongs to an in-cluster driver (mirroring how EquipmentTags + ScriptedAlarms are filtered).</summary>
|
||||
[Fact]
|
||||
public void ParseComposition_scoped_keeps_only_my_clusters_virtual_tags()
|
||||
{
|
||||
var blob = BlobOf(new
|
||||
{
|
||||
Clusters = new[] { new { ClusterId = "MAIN" }, new { ClusterId = "SITE-A" } },
|
||||
Nodes = new[]
|
||||
{
|
||||
new { NodeId = "central-1:4053", ClusterId = "MAIN" },
|
||||
new { NodeId = "site-a-1:4053", ClusterId = "SITE-A" },
|
||||
},
|
||||
DriverInstances = new[]
|
||||
{
|
||||
new { DriverInstanceId = "main-modbus", DriverType = "Modbus", DriverConfig = "{}", ClusterId = "MAIN", NamespaceId = "main-ns" },
|
||||
new { DriverInstanceId = "sa-modbus", DriverType = "Modbus", DriverConfig = "{}", ClusterId = "SITE-A", NamespaceId = "sa-ns" },
|
||||
},
|
||||
Equipment = new[]
|
||||
{
|
||||
new { EquipmentId = "eq-main", Name = "eqm", UnsLineId = "l1", DriverInstanceId = "main-modbus" },
|
||||
new { EquipmentId = "eq-sa", Name = "eqs", UnsLineId = "l2", DriverInstanceId = "sa-modbus" },
|
||||
},
|
||||
Scripts = new[]
|
||||
{
|
||||
new { ScriptId = "scr", SourceCode = "return 1;" },
|
||||
},
|
||||
VirtualTags = new[]
|
||||
{
|
||||
new { VirtualTagId = "vt-main", EquipmentId = "eq-main", Name = "VM", DataType = "Float", ScriptId = "scr" },
|
||||
new { VirtualTagId = "vt-sa", EquipmentId = "eq-sa", Name = "VS", DataType = "Float", ScriptId = "scr" },
|
||||
},
|
||||
});
|
||||
|
||||
var main = DeploymentArtifact.ParseComposition(blob, "central-1:4053");
|
||||
main.EquipmentVirtualTags.Select(v => v.VirtualTagId).ShouldBe(new[] { "vt-main" });
|
||||
|
||||
var siteA = DeploymentArtifact.ParseComposition(blob, "site-a-1:4053");
|
||||
siteA.EquipmentVirtualTags.Select(v => v.VirtualTagId).ShouldBe(new[] { "vt-sa" });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseComposition_single_cluster_node_id_overload_matches_legacy()
|
||||
{
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
using System.Text.Json;
|
||||
using Akka.Actor;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Fleet;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the live-deploy wiring of <see cref="VirtualTagHostActor"/>: the
|
||||
/// <see cref="DriverHostActor"/> must forward the composition's
|
||||
/// <c>EquipmentVirtualTags</c> to its spawned VirtualTag host via
|
||||
/// <see cref="VirtualTagHostActor.ApplyVirtualTags"/> on BOTH the fresh-apply path and the
|
||||
/// bootstrap-restore path (both route through <c>PushDesiredSubscriptions</c>). The host is
|
||||
/// injected as a <see cref="Akka.TestKit.TestProbe"/> via the Props override seam so the
|
||||
/// ApplyVirtualTags can be intercepted.
|
||||
/// </summary>
|
||||
public sealed class DriverHostActorVirtualTagTests : RuntimeActorTestBase
|
||||
{
|
||||
private static readonly NodeId TestNode = NodeId.Parse("driver-vt-test");
|
||||
private static readonly RevisionHash RevA = RevisionHash.Parse(new string('a', 64));
|
||||
|
||||
/// <summary>Fresh apply: dispatching a deployment whose artifact carries one Equipment
|
||||
/// VirtualTag forwards an <see cref="VirtualTagHostActor.ApplyVirtualTags"/> carrying that
|
||||
/// plan to the injected VirtualTag host.</summary>
|
||||
[Fact]
|
||||
public void Apply_forwards_EquipmentVirtualTags_to_virtual_tag_host()
|
||||
{
|
||||
var db = NewInMemoryDbFactory();
|
||||
var deploymentId = SeedDeploymentWithVirtualTag(db, RevA);
|
||||
|
||||
var coordinator = CreateTestProbe();
|
||||
var vtHost = CreateTestProbe();
|
||||
var actor = Sys.ActorOf(DriverHostActor.Props(
|
||||
db, TestNode, coordinator.Ref,
|
||||
localRoles: new HashSet<string> { "driver" },
|
||||
virtualTagEvaluator: NullVirtualTagEvaluator.Instance,
|
||||
virtualTagHostOverride: vtHost.Ref));
|
||||
|
||||
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
|
||||
|
||||
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5)).Outcome.ShouldBe(ApplyAckOutcome.Applied);
|
||||
|
||||
var apply = vtHost.ExpectMsg<VirtualTagHostActor.ApplyVirtualTags>(TimeSpan.FromSeconds(5));
|
||||
var plan = apply.Plans.ShouldHaveSingleItem();
|
||||
plan.VirtualTagId.ShouldBe("vt-1");
|
||||
plan.EquipmentId.ShouldBe("eq-1");
|
||||
plan.Name.ShouldBe("Doubled");
|
||||
}
|
||||
|
||||
/// <summary>Bootstrap-restore: a node that already has an <c>Applied</c> NodeDeploymentState
|
||||
/// row for a VirtualTag-carrying deployment re-forwards the
|
||||
/// <see cref="VirtualTagHostActor.ApplyVirtualTags"/> on PreStart (no dispatch needed), so a
|
||||
/// restarted node restores its live VirtualTag children.</summary>
|
||||
[Fact]
|
||||
public void Restore_on_bootstrap_forwards_EquipmentVirtualTags_to_virtual_tag_host()
|
||||
{
|
||||
var db = NewInMemoryDbFactory();
|
||||
var deploymentId = SeedDeploymentWithVirtualTag(db, RevA);
|
||||
SeedAppliedNodeState(db, deploymentId);
|
||||
|
||||
var coordinator = CreateTestProbe();
|
||||
var vtHost = CreateTestProbe();
|
||||
// No DispatchDeployment — Bootstrap() should detect the Applied row and run RestoreApplied,
|
||||
// which routes through PushDesiredSubscriptions and forwards ApplyVirtualTags.
|
||||
Sys.ActorOf(DriverHostActor.Props(
|
||||
db, TestNode, coordinator.Ref,
|
||||
localRoles: new HashSet<string> { "driver" },
|
||||
virtualTagEvaluator: NullVirtualTagEvaluator.Instance,
|
||||
virtualTagHostOverride: vtHost.Ref));
|
||||
|
||||
var apply = vtHost.ExpectMsg<VirtualTagHostActor.ApplyVirtualTags>(TimeSpan.FromSeconds(5));
|
||||
apply.Plans.ShouldHaveSingleItem().VirtualTagId.ShouldBe("vt-1");
|
||||
}
|
||||
|
||||
private static DeploymentId SeedDeploymentWithVirtualTag(
|
||||
IDbContextFactory<OtOpcUaConfigDbContext> db, RevisionHash rev)
|
||||
{
|
||||
var artifact = JsonSerializer.SerializeToUtf8Bytes(new
|
||||
{
|
||||
Scripts = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
ScriptId = "scr-1",
|
||||
SourceCode = "return ctx.GetTag(\"TestMachine_001.TestDouble\").Value;",
|
||||
},
|
||||
},
|
||||
VirtualTags = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
VirtualTagId = "vt-1",
|
||||
EquipmentId = "eq-1",
|
||||
Name = "Doubled",
|
||||
DataType = "Float",
|
||||
ScriptId = "scr-1",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var id = DeploymentId.NewId();
|
||||
using var ctx = db.CreateDbContext();
|
||||
ctx.Deployments.Add(new Deployment
|
||||
{
|
||||
DeploymentId = id.Value,
|
||||
RevisionHash = rev.Value,
|
||||
Status = DeploymentStatus.Sealed,
|
||||
CreatedBy = "test",
|
||||
SealedAtUtc = DateTime.UtcNow,
|
||||
ArtifactBlob = artifact,
|
||||
});
|
||||
ctx.SaveChanges();
|
||||
return id;
|
||||
}
|
||||
|
||||
private static void SeedAppliedNodeState(
|
||||
IDbContextFactory<OtOpcUaConfigDbContext> db, DeploymentId deploymentId)
|
||||
{
|
||||
using var ctx = db.CreateDbContext();
|
||||
ctx.NodeDeploymentStates.Add(new NodeDeploymentState
|
||||
{
|
||||
NodeId = TestNode.Value,
|
||||
DeploymentId = deploymentId.Value,
|
||||
Status = NodeDeploymentStatus.Applied,
|
||||
StartedAtUtc = DateTime.UtcNow,
|
||||
AppliedAtUtc = DateTime.UtcNow,
|
||||
});
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
using Akka.Actor;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.OpcUa;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.VirtualTags;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies <see cref="VirtualTagHostActor"/> reconciles a desired set of
|
||||
/// <see cref="EquipmentVirtualTagPlan"/> into child <see cref="VirtualTagActor"/>s and bridges each
|
||||
/// child's <see cref="VirtualTagActor.EvaluationResult"/> onto an
|
||||
/// <see cref="OpcUaPublishActor.AttributeValueUpdate"/> carrying the folder-scoped NodeId computed by
|
||||
/// the materialiser.
|
||||
/// </summary>
|
||||
public sealed class VirtualTagHostActorTests : RuntimeActorTestBase
|
||||
{
|
||||
/// <summary>A plan with no FolderPath maps onto NodeId "EquipmentId/Name".</summary>
|
||||
private static EquipmentVirtualTagPlan Plan(
|
||||
string vtagId, string equipmentId, string name, string folderPath = "") =>
|
||||
new(
|
||||
VirtualTagId: vtagId,
|
||||
EquipmentId: equipmentId,
|
||||
FolderPath: folderPath,
|
||||
Name: name,
|
||||
DataType: "Double",
|
||||
Expression: "ctx.GetTag(\"a\")",
|
||||
DependencyRefs: new[] { "a" });
|
||||
|
||||
/// <summary>Spawn: an apply with one plan spins up exactly one live child VirtualTagActor.</summary>
|
||||
[Fact]
|
||||
public void ApplyVirtualTags_spawns_one_child_per_plan()
|
||||
{
|
||||
var publish = CreateTestProbe();
|
||||
var mux = CreateTestProbe();
|
||||
var host = Sys.ActorOf(VirtualTagHostActor.Props(publish.Ref, mux.Ref, new StubEvaluator()));
|
||||
|
||||
host.Tell(new VirtualTagHostActor.ApplyVirtualTags(new[] { Plan("vt-1", "eq-1", "speed-rpm") }));
|
||||
|
||||
// The child self-registers with the mux in PreStart, so a RegisterInterest landing on the
|
||||
// mux probe is proof the host spawned a live child.
|
||||
var reg = mux.ExpectMsg<DependencyMuxActor.RegisterInterest>();
|
||||
reg.TagRefs.ShouldContain("a");
|
||||
}
|
||||
|
||||
/// <summary>KEY TEST: a child EvaluationResult is bridged to the publish actor with the
|
||||
/// folder-scoped NodeId, Value, Good quality, and source timestamp preserved.</summary>
|
||||
[Fact]
|
||||
public void EvaluationResult_is_bridged_with_folder_scoped_NodeId()
|
||||
{
|
||||
var publish = CreateTestProbe();
|
||||
var host = Sys.ActorOf(VirtualTagHostActor.Props(publish.Ref, mux: null, new StubEvaluator()));
|
||||
|
||||
host.Tell(new VirtualTagHostActor.ApplyVirtualTags(new[] { Plan("vt-1", "eq-1", "speed-rpm") }));
|
||||
|
||||
var ts = new DateTime(2026, 6, 7, 12, 0, 0, DateTimeKind.Utc);
|
||||
host.Tell(new VirtualTagActor.EvaluationResult("vt-1", 42.0, ts, CorrelationId.NewId()));
|
||||
|
||||
var update = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>();
|
||||
update.NodeId.ShouldBe("eq-1/speed-rpm");
|
||||
update.Value.ShouldBe(42.0);
|
||||
update.Quality.ShouldBe(OpcUaQuality.Good);
|
||||
update.TimestampUtc.ShouldBe(ts);
|
||||
}
|
||||
|
||||
/// <summary>FolderPath is honoured in the published NodeId (EquipmentId/FolderPath/Name).</summary>
|
||||
[Fact]
|
||||
public void EvaluationResult_NodeId_includes_folder_path_when_set()
|
||||
{
|
||||
var publish = CreateTestProbe();
|
||||
var host = Sys.ActorOf(VirtualTagHostActor.Props(publish.Ref, mux: null, new StubEvaluator()));
|
||||
|
||||
host.Tell(new VirtualTagHostActor.ApplyVirtualTags(
|
||||
new[] { Plan("vt-1", "eq-1", "speed-rpm", folderPath: "metrics") }));
|
||||
|
||||
host.Tell(new VirtualTagActor.EvaluationResult("vt-1", 1.0, DateTime.UtcNow, CorrelationId.NewId()));
|
||||
|
||||
var update = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>();
|
||||
update.NodeId.ShouldBe("eq-1/metrics/speed-rpm");
|
||||
}
|
||||
|
||||
/// <summary>Stop-removed: a vtag dropped from the desired set is unmapped, so a later result for
|
||||
/// it produces NO publish.</summary>
|
||||
[Fact]
|
||||
public void Removed_vtag_is_no_longer_bridged()
|
||||
{
|
||||
var publish = CreateTestProbe();
|
||||
var host = Sys.ActorOf(VirtualTagHostActor.Props(publish.Ref, mux: null, new StubEvaluator()));
|
||||
|
||||
host.Tell(new VirtualTagHostActor.ApplyVirtualTags(new[] { Plan("vt-1", "eq-1", "speed-rpm") }));
|
||||
// Re-apply without vt-1 — it should be stopped + unmapped.
|
||||
host.Tell(new VirtualTagHostActor.ApplyVirtualTags(Array.Empty<EquipmentVirtualTagPlan>()));
|
||||
|
||||
host.Tell(new VirtualTagActor.EvaluationResult("vt-1", 99.0, DateTime.UtcNow, CorrelationId.NewId()));
|
||||
|
||||
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
||||
}
|
||||
|
||||
/// <summary>Unmapped result dropped: a result for an unknown vtagId is silently ignored.</summary>
|
||||
[Fact]
|
||||
public void Result_for_unknown_vtag_is_dropped()
|
||||
{
|
||||
var publish = CreateTestProbe();
|
||||
var host = Sys.ActorOf(VirtualTagHostActor.Props(publish.Ref, mux: null, new StubEvaluator()));
|
||||
|
||||
host.Tell(new VirtualTagHostActor.ApplyVirtualTags(new[] { Plan("vt-1", "eq-1", "speed-rpm") }));
|
||||
|
||||
host.Tell(new VirtualTagActor.EvaluationResult("vt-unknown", 7.0, DateTime.UtcNow, CorrelationId.NewId()));
|
||||
|
||||
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// After a child actor terminates unexpectedly, a subsequent ApplyVirtualTags (still containing
|
||||
/// that vtag) must re-spawn it. Proof: two distinct RegisterInterest messages arrive at the mux
|
||||
/// probe — one for the original child and one for the replacement.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Child_is_respawned_after_unexpected_termination()
|
||||
{
|
||||
var publish = CreateTestProbe();
|
||||
var mux = CreateTestProbe();
|
||||
var host = Sys.ActorOf(VirtualTagHostActor.Props(publish.Ref, mux.Ref, new StubEvaluator()));
|
||||
var plan = new[] { Plan("vt-1", "eq-1", "speed-rpm") };
|
||||
|
||||
// First apply — child self-registers; capture the child ref from the message sender.
|
||||
host.Tell(new VirtualTagHostActor.ApplyVirtualTags(plan));
|
||||
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>();
|
||||
var firstChild = mux.LastSender;
|
||||
|
||||
// Watch the child from the test side so we can await its death deterministically before
|
||||
// re-applying, avoiding any race between Terminated delivery to the host and the re-apply.
|
||||
Watch(firstChild);
|
||||
Sys.Stop(firstChild);
|
||||
ExpectTerminated(firstChild);
|
||||
|
||||
// The dying child's PostStop sends UnregisterInterest to the mux — drain it so the mux probe
|
||||
// mailbox is clean before we look for the new RegisterInterest.
|
||||
mux.ExpectMsg<DependencyMuxActor.UnregisterInterest>(TimeSpan.FromSeconds(5));
|
||||
|
||||
// Re-apply with the same plan — host should see vt-1 absent from _children and spawn fresh.
|
||||
host.Tell(new VirtualTagHostActor.ApplyVirtualTags(plan));
|
||||
var reg2 = mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(TimeSpan.FromSeconds(5));
|
||||
reg2.TagRefs.ShouldContain("a");
|
||||
|
||||
// The new child must be a different actor ref than the one we killed.
|
||||
var secondChild = mux.LastSender;
|
||||
secondChild.ShouldNotBe(firstChild);
|
||||
}
|
||||
|
||||
/// <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
|
||||
{
|
||||
/// <summary>Returns NoChange so the child never emits on its own.</summary>
|
||||
/// <param name="id">The tag identifier.</param>
|
||||
/// <param name="expr">The expression string.</param>
|
||||
/// <param name="deps">The dependency values.</param>
|
||||
/// <returns>A NoChange result.</returns>
|
||||
public VirtualTagEvalResult Evaluate(string id, string expr, IReadOnlyDictionary<string, object?> deps)
|
||||
=> VirtualTagEvalResult.NoChange;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user