test(resilience): prove ResilienceConfig survives the real ConfigComposer round-trip (#456)

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.
This commit is contained in:
Joseph Doherty
2026-07-15 08:38:13 -04:00
parent 5908339d67
commit 809e7886d8
@@ -138,6 +138,59 @@ public sealed class ConfigComposerTests : ControlPlaneActorTestBase
node.DeviceHost.ShouldBe("10.9.9.9:8193");
}
/// <summary>
/// Verifies that a <see cref="DriverInstance"/>'s per-instance <c>ResilienceConfig</c> override
/// survives the real <see cref="ConfigComposer.SnapshotAndFlattenAsync"/> →
/// <see cref="DeploymentArtifact.ParseDriverInstances(ReadOnlySpan{byte})"/> seam (arch-review
/// follow-up #10 sub-gap). The composer serialises the whole entity, so the override rides the
/// artifact into <c>DriverInstanceSpec.ResilienceConfig</c> — which <c>DriverHostActor</c> layers
/// onto the driver's Polly pipeline. Guards the one leg the hand-rolled
/// <c>DeploymentArtifactTests.ParseDriverInstances_carries_ResilienceConfig_onto_the_spec</c>
/// cannot: if the composer ever stopped emitting the column (a projection, a <c>[JsonIgnore]</c>),
/// authored resilience policy would silently revert to tier defaults in production while the
/// hand-built artifact tests stayed green.
/// </summary>
[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()