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:
@@ -138,6 +138,59 @@ public sealed class ConfigComposerTests : ControlPlaneActorTestBase
|
|||||||
node.DeviceHost.ShouldBe("10.9.9.9:8193");
|
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 readonly DateTime FixedTimestamp = new(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
private static ServerCluster NewCluster(string id) => new()
|
private static ServerCluster NewCluster(string id) => new()
|
||||||
|
|||||||
Reference in New Issue
Block a user