From 12b2ce078b927df0db3dd4af228edd626cd57ed2 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 13 Jul 2026 10:42:18 -0400 Subject: [PATCH] fix(r2-10): register the tracker's production reader on driver nodes + wiring guard --- ...esilience-observability-plan.md.tasks.json | 2 +- .../Drivers/DriverFactoryBootstrap.cs | 7 +++++ ...ResilienceStatusReaderRegistrationTests.cs | 30 +++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/ResilienceStatusReaderRegistrationTests.cs diff --git a/archreview/plans/R2-10-resilience-observability-plan.md.tasks.json b/archreview/plans/R2-10-resilience-observability-plan.md.tasks.json index 882d5331..7c6e7eb1 100644 --- a/archreview/plans/R2-10-resilience-observability-plan.md.tasks.json +++ b/archreview/plans/R2-10-resilience-observability-plan.md.tasks.json @@ -7,7 +7,7 @@ { "id": "T3", "subject": "Invoker: successful Execute* resets ConsecutiveFailures (TDD in CapabilityInvokerTests)", "status": "completed", "blockedBy": ["T1"] }, { "id": "T4", "subject": "Commons: DriverResilienceStatusChanged record + driver-resilience-status TopicName", "status": "completed", "blockedBy": [] }, { "id": "T5", "subject": "Host: DriverResilienceStatusPublisherService (BuildMessages mapping + 5s DPS timer shell; TDD mapping in Host.IntegrationTests)", "status": "completed", "blockedBy": ["T1", "T4"] }, - { "id": "T6", "subject": "Host DI: register the publisher in AddOtOpcUaDriverFactories + ResilienceStatusReaderRegistrationTests wiring guard (tracker HAS a production reader)", "status": "pending", "blockedBy": ["T5"] }, + { "id": "T6", "subject": "Host DI: register the publisher in AddOtOpcUaDriverFactories + ResilienceStatusReaderRegistrationTests wiring guard (tracker HAS a production reader)", "status": "completed", "blockedBy": ["T5"] }, { "id": "T7", "subject": "AdminUI: IDriverResilienceStatusStore + InMemory impl + AddOtOpcUaDriverStatusServices registration (TDD mirroring DriverStatusSnapshotStoreTests)", "status": "pending", "blockedBy": ["T4"] }, { "id": "T8", "subject": "AdminUI: DriverResilienceStatusBridge DPS actor + spawn in WithOtOpcUaSignalRBridges", "status": "pending", "blockedBy": ["T7"] }, { "id": "T9", "subject": "AdminUI: resilience chip section in DriverStatusPanel.razor (in-process store read; no bUnit — verified by T10)", "status": "pending", "blockedBy": ["T7"] }, diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Drivers/DriverFactoryBootstrap.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Drivers/DriverFactoryBootstrap.cs index bf6a5f37..1d4fd573 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Drivers/DriverFactoryBootstrap.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Drivers/DriverFactoryBootstrap.cs @@ -70,6 +70,13 @@ public static class DriverFactoryBootstrap logger: sp.GetService()?.CreateLogger("ZB.MOM.WW.OtOpcUa.Core.Resilience.DriverCapabilityInvokerFactory")); }); + // The tracker's operator-facing reader: a periodic DPS publisher that mirrors each snapshot to the + // AdminUI resilience panel (R2-10 — closes the "tracker fed by everything, read by nothing" gap). + // Lives on driver nodes exactly where the tracker exists. The lazy Func is idempotent + // with the one Program.cs registers for the alarm-command router. + services.TryAddSingleton>(sp => () => sp.GetRequiredService()); + services.AddHostedService(); + // Driver nodes also carry the probe set so a fused admin,driver node has it; the admin-only // case is covered by Program.cs calling AddOtOpcUaDriverProbes() in the hasAdmin block. services.AddOtOpcUaDriverProbes(); diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/ResilienceStatusReaderRegistrationTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/ResilienceStatusReaderRegistrationTests.cs new file mode 100644 index 00000000..19b4ae72 --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/ResilienceStatusReaderRegistrationTests.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Host.Drivers; + +namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests; + +/// +/// Guards that the resilience-status tracker now HAS a production reader — the exact +/// "built-but-never-wired" class of bug the whole arch-review targets (mirrors +/// ). AddOtOpcUaDriverFactories (the +/// driver-node bootstrap) must register as an +/// . A refactor that drops the registration fails this test, not production. +/// +public sealed class ResilienceStatusReaderRegistrationTests +{ + [Fact] + public void AddOtOpcUaDriverFactories_registers_the_resilience_status_reader_hosted_service() + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddOtOpcUaDriverFactories(); + + services.ShouldContain( + d => d.ServiceType == typeof(IHostedService) + && d.ImplementationType == typeof(DriverResilienceStatusPublisherService), + "the resilience-status tracker must have a production reader — the periodic DPS publisher hosted service on driver nodes"); + } +}