From bc9cd464b5201128b8fd6c47a88047c72b0d9dcb Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 13 Jun 2026 08:14:04 -0400 Subject: [PATCH] =?UTF-8?q?docs(opcuaclient):=20plan=20=E2=80=94=20registe?= =?UTF-8?q?r=20OpcUaClient=20driver=20factory=20(3=20tasks)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-06-13-opcuaclient-factory-plan.md | 196 ++++++++++++++++++ ...-13-opcuaclient-factory-plan.md.tasks.json | 9 + 2 files changed, 205 insertions(+) create mode 100644 docs/plans/2026-06-13-opcuaclient-factory-plan.md create mode 100644 docs/plans/2026-06-13-opcuaclient-factory-plan.md.tasks.json diff --git a/docs/plans/2026-06-13-opcuaclient-factory-plan.md b/docs/plans/2026-06-13-opcuaclient-factory-plan.md new file mode 100644 index 00000000..7e72e995 --- /dev/null +++ b/docs/plans/2026-06-13-opcuaclient-factory-plan.md @@ -0,0 +1,196 @@ +# OpcUaClient Driver Factory — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task. + +**Goal:** Register the OpcUaClient driver factory so `DriverType="OpcUaClient"` instances are constructed (not stubbed) — fixing a real bug and yielding the first end-to-end live equipment-tag value (live-proving the `FullName→NodeId` router shipped in `c4435e4f`). + +**Architecture:** Mirror `ModbusDriverFactoryExtensions`: a new `OpcUaClientDriverFactoryExtensions` (`Register` + `CreateInstance` that deserialises `OpcUaClientDriverOptions` exactly as the existing probe does) plus one registration line in `DriverFactoryBootstrap.Register`. OpcUaClient is cross-platform, so once a factory exists it runs as a real driver in docker-dev. + +**Tech Stack:** .NET 10, System.Text.Json, OPC UA Foundation stack, xUnit + Shouldly. Design: `docs/plans/2026-06-13-opcuaclient-factory-design.md` (master `5aa1030b`). + +**Branch:** `feat/opcuaclient-factory` off master `5aa1030b`. + +--- + +## Hard rules (every task) +- Stage by path; never `git add .`. Never stage `sql_login.txt`, `src/Server/ZB.MOM.WW.OtOpcUa.Host/pki/`, `pending.md`, or `current.md`. +- Never echo secrets. No force-push, no `--no-verify`. No Configuration/EF migration change. + +## Task order +T0 branch → T1 factory + register + unit test → T2 verify (build + test + live `/run`). + +--- + +### Task 0: Create feature branch + +**Classification:** trivial +**Estimated implement time:** ~1 min +**Parallelizable with:** none + +**Files:** git only + +```bash +git checkout master && git rev-parse --short HEAD # expect 5aa1030b +git checkout -b feat/opcuaclient-factory +``` +Confirm clean tree (ignore untracked `pending.md` / `current.md` / `sql_login.txt` / `pki/`). + +--- + +### Task 1: `OpcUaClientDriverFactoryExtensions` + register it + unit test + +**Classification:** standard +**Estimated implement time:** ~5 min +**Parallelizable with:** none + +**Files:** +- Create: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverFactoryExtensions.cs` +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Host/Drivers/DriverFactoryBootstrap.cs` (the `Register` method, ~lines 100-106) +- Test: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientDriverFactoryTests.cs` + +**Context:** Read `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriverFactoryExtensions.cs` as the template (its `Register` + `CreateInstance` + `JsonOptions`), and `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverProbe.cs` (it already deserialises the config). Confirm the `OpcUaClientDriver` ctor: `OpcUaClientDriver(OpcUaClientDriverOptions options, string driverInstanceId, ILogger? logger = null)`. Look at `GalaxyDriverFactoryTests.cs` / the Modbus factory tests for the test conventions (xUnit v3 + Shouldly). + +**Step 1: Write the failing test** (`OpcUaClientDriverFactoryTests.cs`) + +```csharp +using Shouldly; +using Microsoft.Extensions.Logging.Abstractions; +using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient; +using Xunit; + +namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests; + +public class OpcUaClientDriverFactoryTests +{ + private const string SampleConfig = + """{"EndpointUrl":"opc.tcp://host:4840","SecurityMode":"None","AutoAcceptCertificates":true}"""; + + [Fact] + public void CreateInstance_builds_an_OpcUaClient_driver_with_the_right_identity() + { + var driver = OpcUaClientDriverFactoryExtensions.CreateInstance("drv-1", SampleConfig, NullLoggerFactory.Instance); + driver.DriverType.ShouldBe("OpcUaClient"); + driver.DriverInstanceId.ShouldBe("drv-1"); + } + + [Fact] + public void DriverTypeName_is_OpcUaClient() + => OpcUaClientDriverFactoryExtensions.DriverTypeName.ShouldBe("OpcUaClient"); + + [Fact] + public void CreateInstance_throws_on_null_json_deserialisation() + => Should.Throw( + () => OpcUaClientDriverFactoryExtensions.CreateInstance("drv-1", "null", NullLoggerFactory.Instance)); +} +``` +(Confirm `OpcUaClientDriver` exposes `DriverType` / `DriverInstanceId` properties — it must, per `IDriver`. Adapt the assertions to the real property names if they differ. The OpcUaClient.Tests project must reference the driver project + `Microsoft.Extensions.Logging.Abstractions` — both almost certainly already present; add if missing.) + +**Step 2: Run — expect FAIL** (`OpcUaClientDriverFactoryExtensions` not defined). +`dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests --filter "FullyQualifiedName~OpcUaClientDriverFactory"` + +**Step 3: Implement the factory** (`OpcUaClientDriverFactoryExtensions.cs`) + +```csharp +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; +using ZB.MOM.WW.OtOpcUa.Core.Hosting; + +namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient; + +/// +/// Registers the OPC UA Client driver with the . The driver's +/// DriverConfig JSON deserialises directly into +/// (the same shape reads), so no separate DTO is needed. +/// +public static class OpcUaClientDriverFactoryExtensions +{ + /// Driver type name — matches DriverInstance.DriverType values. + public const string DriverTypeName = "OpcUaClient"; + + // Match OpcUaClientDriverProbe exactly so factory + probe parse a config identically. + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true, + UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, + }; + + /// Register the OpcUaClient factory with the driver registry. + /// The driver factory registry to register with. + /// Optional logger factory used to create per-instance loggers. + public static void Register(DriverFactoryRegistry registry, ILoggerFactory? loggerFactory = null) + { + ArgumentNullException.ThrowIfNull(registry); + registry.Register(DriverTypeName, (id, json) => CreateInstance(id, json, loggerFactory)); + } + + /// Public for the Server-side bootstrapper + test consumers. + /// The unique identifier for the driver instance. + /// The JSON configuration string for the driver. + /// Optional logger factory for the per-instance logger. + /// A configured . + public static OpcUaClientDriver CreateInstance( + string driverInstanceId, string driverConfigJson, ILoggerFactory? loggerFactory = null) + { + ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId); + ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson); + + var options = JsonSerializer.Deserialize(driverConfigJson, JsonOptions) + ?? throw new InvalidOperationException( + $"OpcUaClient driver config for '{driverInstanceId}' deserialised to null"); + + return new OpcUaClientDriver(options, driverInstanceId, loggerFactory?.CreateLogger()); + } +} +``` +(Confirm the `using` for `OpcUaClientDriverOptions` — it's in `…Driver.OpcUaClient.Contracts`; the driver project already references it. Confirm `DriverFactoryRegistry`'s namespace is `ZB.MOM.WW.OtOpcUa.Core.Hosting` — check the Modbus factory's usings.) + +**Step 4: Register it in the bootstrap** — add to `DriverFactoryBootstrap.Register` (alongside the other lines): +```csharp +Driver.OpcUaClient.OpcUaClientDriverFactoryExtensions.Register(registry, loggerFactory); +``` + +**Step 5: Run tests + full build** +```bash +dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests --filter "FullyQualifiedName~OpcUaClientDriverFactory" +dotnet build src/Server/ZB.MOM.WW.OtOpcUa.Host +``` +Expected: tests PASS; Host builds 0 errors. + +**Step 6: Commit** (stage by path) +```bash +git add src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverFactoryExtensions.cs src/Server/ZB.MOM.WW.OtOpcUa.Host/Drivers/DriverFactoryBootstrap.cs tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientDriverFactoryTests.cs +git commit -m "fix(opcuaclient): register the OpcUaClient driver factory (was always stubbed)" +``` + +--- + +### Task 2: Verify — build, test, agent-driven live `/run` (the payoff) + +**Classification:** verification +**Estimated implement time:** ~5 min + live +**Parallelizable with:** none +**blockedBy:** Task 1 + +**Files:** none + +**Step 1:** Full solution build + the touched suites: +```bash +dotnet build ZB.MOM.WW.OtOpcUa.slnx +dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests +``` +Expect 0 errors; green. + +**Step 2: Live docker-dev `/run` (agent drives; dev login DISABLED).** The `MAIN-opcua-eq` OpcUaClient driver + `FastUInt1` tag (`{"FullName":"ns=3;s=FastUInt1"}`) are **already authored + deployed** in the dev rig; the opc-plc sim is up at `opc.tcp://10.100.0.35:50000` (start it if down: `ssh 10.100.0.35 'cd /opt/otopcua-opcuaclient && docker compose up -d'`). +- Rebuild central on the branch: `docker compose -f docker-dev/docker-compose.yml up -d --build migrator central-1 central-2` (no new migration; the rebuild ships the branch code). +- Redeploy: `curl -sS -X POST http://localhost:9200/api/deployments -H "X-Api-Key: docker-dev-deploy-key" -H "Content-Type: application/json" -d '{}'`. +- **Confirm the fix:** `docker logs --since 3m otopcua-dev-central-1-1 2>&1 | grep -iE "OpcUaClient|opc-plc|no factory|session|subscrib"` — must **NOT** show "no factory for driver type OpcUaClient"; should show the driver connecting/subscribing. +- **Confirm the live value (the router payoff):** retry-read `ns=2;s=EQ-55297329838d/FastUInt1` from `opc.tcp://localhost:4840` via Client.CLI until it shows a Good, changing value (not `BadWaitingForInitialData`): + `dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- read -u opc.tcp://localhost:4840 -n "ns=2;s=EQ-55297329838d/FastUInt1"` (read twice ~2s apart — `FastUInt1` increments, so the value should change). + +**Step 3:** On green, finish via `superpowers-extended-cc:finishing-a-development-branch` (intent: merge-to-master + push). + +--- + +## Out of scope +Protocol-driver (Modbus/S7) equipment-tag↔tag-table linkage; Galaxy gateway; Phase B/C — separate milestones (see `pending.md`). diff --git a/docs/plans/2026-06-13-opcuaclient-factory-plan.md.tasks.json b/docs/plans/2026-06-13-opcuaclient-factory-plan.md.tasks.json new file mode 100644 index 00000000..d4d1c11f --- /dev/null +++ b/docs/plans/2026-06-13-opcuaclient-factory-plan.md.tasks.json @@ -0,0 +1,9 @@ +{ + "planPath": "docs/plans/2026-06-13-opcuaclient-factory-plan.md", + "tasks": [ + {"id": 338, "subject": "Task 0: Create feature branch", "status": "pending"}, + {"id": 339, "subject": "Task 1: OpcUaClientDriverFactoryExtensions + register + unit test", "status": "pending", "blockedBy": [338]}, + {"id": 340, "subject": "Task 2: Verify — build, test, live /run", "status": "pending", "blockedBy": [339]} + ], + "lastUpdated": "2026-06-13" +}