diff --git a/archreview/plans/R2-02-resilience-config-hardening-plan.md.tasks.json b/archreview/plans/R2-02-resilience-config-hardening-plan.md.tasks.json index 6a0551fc..7fc196b3 100644 --- a/archreview/plans/R2-02-resilience-config-hardening-plan.md.tasks.json +++ b/archreview/plans/R2-02-resilience-config-hardening-plan.md.tasks.json @@ -5,7 +5,7 @@ { "id": 1, "subject": "Add ResiliencePolicyRanges constants to Core.Abstractions (single source of truth for parser/builder/AdminUI, derived from Polly.Core 8.6.6 validation)", "status": "completed", "blockedBy": [] }, { "id": 2, "subject": "S-6 parser: clamp timeout/retry/breaker overrides with appending diagnostics (timeout<=0 -> tier default; breaker==1 -> 2; caps) + 8 parser tests", "status": "completed", "blockedBy": [0, 1] }, { "id": 3, "subject": "S-6 builder: belt-and-braces clamp in Build so pipeline construction can never throw ValidationException + Build_NeverThrows_ForHostileDirectOptions (turns task 0 green)", "status": "completed", "blockedBy": [0, 1] }, - { "id": 4, "subject": "S-6 production-wiring guard: DriverCapabilityInvokerFactory.Create with hostile JSON yields a working invoker + logged clamp diagnostic; re-run ResilienceInvokerFactoryRegistrationTests", "status": "pending", "blockedBy": [2, 3] }, + { "id": 4, "subject": "S-6 production-wiring guard: DriverCapabilityInvokerFactory.Create with hostile JSON yields a working invoker + logged clamp diagnostic; re-run ResilienceInvokerFactoryRegistrationTests", "status": "completed", "blockedBy": [2, 3] }, { "id": 5, "subject": "S-6 AdminUI: ResilienceFormModel.Validate() range messages (via ResiliencePolicyRanges through the transitive Core.Abstractions ref) + warning block in DriverResilienceSection", "status": "pending", "blockedBy": [1] }, { "id": 6, "subject": "S-8 parser layer: force Write/AlarmAcknowledge retryCount overrides to 0 with diagnostic (spec invariant, honest dead-knob surfacing) + tests + GetTierDefaults doc", "status": "pending", "blockedBy": [2] }, { "id": 7, "subject": "S-8 dispatch layer: flip HandleWriteAsync to isIdempotent:false — RED the DriverInstanceActorResilienceWiringTests expectation first, then flip; full Runtime suite green", "status": "pending", "blockedBy": [6] }, diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests/Resilience/DriverCapabilityInvokerFactoryTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests/Resilience/DriverCapabilityInvokerFactoryTests.cs index eb2c58d5..04aeecc0 100644 --- a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests/Resilience/DriverCapabilityInvokerFactoryTests.cs +++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests/Resilience/DriverCapabilityInvokerFactoryTests.cs @@ -136,6 +136,31 @@ public sealed class DriverCapabilityInvokerFactoryTests logger.Entries.ShouldContain(e => e.Level == LogLevel.Warning && e.Message.Contains("resilience config", StringComparison.OrdinalIgnoreCase)); } + /// + /// 01/S-6 production-wiring proof: the exact DriverHostActor.SpawnChild path + /// (factory.Create(id, type, hostileJson)) must yield a WORKING invoker whose capability + /// call succeeds, and must log the clamp diagnostic as a warning — a hostile ResilienceConfig can + /// never brick the spawn path. + /// + [Fact] + public async Task Create_with_out_of_range_config_yields_working_invoker() + { + var logger = new CapturingLogger(); + var factory = MakeFactory(new DriverResiliencePipelineBuilder(), logger: logger); + + var invoker = factory.Create("i1", "Modbus", + resilienceConfigJson: "{\"capabilityPolicies\":{\"Subscribe\":{\"timeoutSeconds\":0,\"breakerFailureThreshold\":1}}}"); + + var result = await invoker.ExecuteAsync( + DriverCapability.Subscribe, "host-1", _ => ValueTask.FromResult(99), CancellationToken.None); + + result.ShouldBe(99, "a hostile ResilienceConfig must not brick the Subscribe capability call"); + logger.Entries.ShouldContain(e => + e.Level == LogLevel.Warning && + e.Message.Contains("resilience config", StringComparison.OrdinalIgnoreCase) && + (e.Message.Contains("timeoutSeconds") || e.Message.Contains("breakerFailureThreshold"))); + } + private sealed class CapturingLogger : ILogger { public List<(LogLevel Level, string Message)> Entries { get; } = new();