From 809e7886d854f690d147a2bf820598e6f0da201f Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 15 Jul 2026 08:38:13 -0400 Subject: [PATCH] test(resilience): prove ResilienceConfig survives the real ConfigComposer round-trip (#456) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The arch-review #10 sub-gap worried that per-instance ResilienceConfig never reaches the runtime pipeline. The threading is in fact wired end-to-end (task #13): ConfigComposer serialises the whole DriverInstance entity, so ResilienceConfig rides the artifact into DriverInstanceSpec, which DriverHostActor layers onto the driver's Polly pipeline. The only leg with no test was the real composer->artifact serialization — the existing DeploymentArtifactTests hand-build the JSON. Adds ResilienceConfig_survives_ConfigComposer_to_ParseDriverInstances_round_trip (mirrors the DeviceHost round-trip guard): seeds a DriverInstance WITH a non-default override + one WITHOUT, runs the real SnapshotAndFlattenAsync, and asserts ParseDriverInstances recovers the override byte-for-byte (and null stays null). Guards against a future projection / [JsonIgnore] silently reverting authored resilience policy to tier defaults while hand-built artifact tests stay green. ControlPlane.Tests ConfigComposerTests 6/6 green. --- .../ConfigComposerTests.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/ConfigComposerTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/ConfigComposerTests.cs index 39ead635..f2d2c9c9 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/ConfigComposerTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/ConfigComposerTests.cs @@ -138,6 +138,59 @@ public sealed class ConfigComposerTests : ControlPlaneActorTestBase node.DeviceHost.ShouldBe("10.9.9.9:8193"); } + /// + /// Verifies that a 's per-instance ResilienceConfig override + /// survives the real → + /// seam (arch-review + /// follow-up #10 sub-gap). The composer serialises the whole entity, so the override rides the + /// artifact into DriverInstanceSpec.ResilienceConfig — which DriverHostActor layers + /// onto the driver's Polly pipeline. Guards the one leg the hand-rolled + /// DeploymentArtifactTests.ParseDriverInstances_carries_ResilienceConfig_onto_the_spec + /// cannot: if the composer ever stopped emitting the column (a projection, a [JsonIgnore]), + /// authored resilience policy would silently revert to tier defaults in production while the + /// hand-built artifact tests stayed green. + /// + [Fact] + public async Task ResilienceConfig_survives_ConfigComposer_to_ParseDriverInstances_round_trip() + { + const string resilienceJson = + "{\"capabilityPolicies\":{\"Read\":{\"timeoutSeconds\":5,\"retryCount\":7,\"breakerFailureThreshold\":3}}}"; + + var f = NewInMemoryDbFactory(); + await using (var db = f.CreateDbContext()) + { + db.ServerClusters.Add(NewCluster("c1")); + db.Namespaces.Add(new Namespace + { + NamespaceId = "ns-eq", ClusterId = "c1", + Kind = NamespaceKind.Equipment, NamespaceUri = "urn:eq", + }); + db.DriverInstances.Add(new DriverInstance + { + DriverInstanceId = "drv-1", ClusterId = "c1", NamespaceId = "ns-eq", + Name = "Modbus", DriverType = "Modbus", DriverConfig = "{}", + ResilienceConfig = resilienceJson, + }); + // A second instance with no override proves the null case rides the same real composer path. + db.DriverInstances.Add(new DriverInstance + { + DriverInstanceId = "drv-2", ClusterId = "c1", NamespaceId = "ns-eq", + Name = "Modbus2", DriverType = "Modbus", DriverConfig = "{}", + }); + await db.SaveChangesAsync(); + } + + await using var readDb = f.CreateDbContext(); + var artifact = await ConfigComposer.SnapshotAndFlattenAsync(readDb); + var specs = DeploymentArtifact.ParseDriverInstances(artifact.Blob); + + var withOverride = specs.Single(s => s.DriverInstanceId == "drv-1"); + withOverride.ResilienceConfig.ShouldBe(resilienceJson, "the authored override must ride the artifact byte-for-byte"); + + var withoutOverride = specs.Single(s => s.DriverInstanceId == "drv-2"); + withoutOverride.ResilienceConfig.ShouldBeNull("an unset override stays null ⇒ tier defaults at spawn"); + } + private static readonly DateTime FixedTimestamp = new(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc); private static ServerCluster NewCluster(string id) => new()