feat(runtime): announce added nodes after the materialise passes on non-rebuild applies (R2-07 T4b)

This commit is contained in:
Joseph Doherty
2026-07-13 11:58:40 -04:00
parent a86d9f65bf
commit e6280cb1da
4 changed files with 157 additions and 26 deletions
@@ -25,7 +25,10 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.OpcUa;
/// </summary>
public sealed class OpcUaPublishActorApplyFailureTests : RuntimeActorTestBase
{
/// <summary>A rebuild whose sink throws increments otopcua.opcua.apply.failed (kind=rebuild).</summary>
/// <summary>A rebuild whose sink throws increments otopcua.opcua.apply.failed (kind=rebuild). R2-07 — a
/// pure-add no longer rebuilds, so the degraded-rebuild path is exercised via a rebuild-forcing change:
/// a first PureAdd deploy establishes the applied composition, then a rename (ChangedEquipment ⇒ Rebuild)
/// drives the throwing SafeRebuild.</summary>
[Fact]
public void Rebuild_when_sink_throws_increments_apply_failed_meter()
{
@@ -33,10 +36,17 @@ public sealed class OpcUaPublishActorApplyFailureTests : RuntimeActorTestBase
var db = NewInMemoryDbFactory();
var sink = new ThrowOnRebuildSink();
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
SeedEquipmentDeployment(db, "eq-1");
var dep1 = SeedEquipmentDeployment(db, ("eq-1", "eq-1"));
var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(sink: sink, dbFactory: db, applier: applier));
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId()));
// First deploy: PureAdd — no rebuild, so the throwing sink is not hit yet.
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId(), new DeploymentId(dep1)));
Thread.Sleep(200);
recorder.Total.ShouldBe(0);
// Second deploy: eq-1 RENAMED ⇒ ChangedEquipment ⇒ Rebuild ⇒ the sink's RebuildAddressSpace throws.
var dep2 = SeedEquipmentDeployment(db, ("eq-1", "eq-1-renamed"));
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId(), new DeploymentId(dep2)));
AwaitAssert(() =>
{
@@ -53,7 +63,7 @@ public sealed class OpcUaPublishActorApplyFailureTests : RuntimeActorTestBase
var db = NewInMemoryDbFactory();
var sink = new NoopSink();
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
SeedEquipmentDeployment(db, "eq-1");
SeedEquipmentDeployment(db, ("eq-1", "eq-1"));
var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(sink: sink, dbFactory: db, applier: applier));
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId()));
@@ -75,25 +85,26 @@ public sealed class OpcUaPublishActorApplyFailureTests : RuntimeActorTestBase
if (last is not null) throw last;
}
private static void SeedEquipmentDeployment(IDbContextFactory<OtOpcUaConfigDbContext> dbFactory, params string[] equipmentIds)
private static Guid SeedEquipmentDeployment(IDbContextFactory<OtOpcUaConfigDbContext> dbFactory, params (string Id, string Name)[] equipment)
{
var artifact = JsonSerializer.SerializeToUtf8Bytes(new
{
Equipment = equipmentIds.Select(id => new
Equipment = equipment.Select(e => new
{
EquipmentId = id,
MachineCode = id.ToUpperInvariant(),
EquipmentId = e.Id,
MachineCode = e.Id.ToUpperInvariant(),
UnsLineId = "line-1",
Name = id,
Name = e.Name,
}).ToArray(),
DriverInstances = Array.Empty<object>(),
ScriptedAlarms = Array.Empty<object>(),
});
var id = Guid.NewGuid();
using var ctx = dbFactory.CreateDbContext();
ctx.Deployments.Add(new Deployment
{
DeploymentId = Guid.NewGuid(),
DeploymentId = id,
RevisionHash = new string('a', 64),
Status = DeploymentStatus.Sealed,
CreatedBy = "test",
@@ -101,6 +112,7 @@ public sealed class OpcUaPublishActorApplyFailureTests : RuntimeActorTestBase
ArtifactBlob = artifact,
});
ctx.SaveChanges();
return id;
}
/// <summary>A sink whose RebuildAddressSpace throws (drives the applier's SafeRebuild catch → RebuildFailed).</summary>