docs(archreview): remediation plans + fix flagged doc drift
Add the 7 per-domain design+implementation plans (archreview/plans/) with an index, produced from the 2026-07-08 architecture review. Fix two confirmed doc drifts the review flagged (theme #5): - CLAUDE.md KNOWN LIMITATION 2: the continuous-historization historized-ref feed IS wired (AddressSpaceApplier.FeedHistorizedRefs -> UpdateHistorizedRefs -> recorder); rewrite to reflect that value-capture is code-complete and only the live end-to-end + restart-convergence verification remains. - CLAUDE.md ScriptAnalysis gating: endpoints use Roles=Administrator,Designer via RequireAuthorization, not the FleetAdmin policy.
This commit is contained in:
@@ -0,0 +1,411 @@
|
||||
# Architecture Review 07 — Client Tooling, Analyzers, and the Cross-Cutting Engineering System
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Date** | 2026-07-08 |
|
||||
| **Commit** | `9cad9ed0` (master) |
|
||||
| **Reviewer** | Architecture review agent (deep-review sweep, slice 07) |
|
||||
| **Scope** | `src/Client/*` (Client.CLI, Client.Shared, Client.UI) + `tests/Client/*`; `src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers` + `tests/Tooling/*`; cross-cutting: `Directory.Build.props`, `Directory.Packages.props`, `NuGet.config`, `ZB.MOM.WW.OtOpcUa.slnx`, `.github/workflows/`, `ci/`, `scripts/`, `docker-dev/`, overall test architecture, docs freshness, repo-root hygiene |
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Client stack
|
||||
|
||||
Three projects form a clean layered client stack, all .NET 10:
|
||||
|
||||
- **`Client.Shared`** — the OPC UA client library. `OpcUaClientService`
|
||||
(`src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/OpcUaClientService.cs`, 975 lines) implements
|
||||
`IOpcUaClientService` behind five adapter seams (`IApplicationConfigurationFactory`,
|
||||
`IEndpointDiscovery`, `ISessionFactory`, `ISessionAdapter`, `ISubscriptionAdapter` under
|
||||
`Adapters/`) so the OPC Foundation SDK is fully fakeable — the test project supplies
|
||||
`Fakes/Fake*` for every seam. The service owns connect/disconnect, read/write with
|
||||
string→typed value coercion, browse with continuation points, data + alarm subscriptions
|
||||
with **failover replay** (multi-endpoint round-robin on keep-alive failure), Part 9 alarm
|
||||
method calls (Acknowledge/Confirm/Shelve/Enable/Disable), HistoryRead (raw + aggregate),
|
||||
and redundancy-info reads.
|
||||
- **`Client.CLI`** — CliFx-based terminal client (`otopcua-cli`). 14 commands
|
||||
(`Commands/`: connect, read, write, browse, subscribe, historyread, alarms, redundancy,
|
||||
acknowledge, confirm, shelve, enable, disable + base). `CommandBase` centralises
|
||||
connection options; `Program.cs` is a 14-line CliFx bootstrap with a type-activator that
|
||||
injects a shared `IOpcUaClientServiceFactory`. Commands are session-per-invocation
|
||||
(create → connect → work → disconnect/dispose in `finally`).
|
||||
- **`Client.UI`** — Avalonia 11.2 desktop app (CommunityToolkit.Mvvm). **Real, not
|
||||
vestigial**: ~2,905 lines of C# + 551 lines of AXAML across 10 viewmodels, 9 views, a
|
||||
custom `DateTimeRangePicker` control, JSON settings persistence, and a UI-dispatcher
|
||||
seam (`IUiDispatcher` with a synchronous test double). Covers browse tree, read/write,
|
||||
subscriptions, alarms (ack/confirm/shelve dialogs), and history. 126 headless
|
||||
(Avalonia.Headless) tests.
|
||||
|
||||
### Tooling
|
||||
|
||||
`src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers` ships exactly one Roslyn analyzer:
|
||||
**OTOPCUA0001 `UnwrappedCapabilityCallAnalyzer`** — flags async calls to the seven guarded
|
||||
driver-capability interfaces (`IReadable`, `IWritable`, `ITagDiscovery`, `ISubscribable`,
|
||||
`IHostConnectivityProbe`, `IAlarmSource`, `IHistoryProvider`) that are not wrapped in
|
||||
`CapabilityInvoker.ExecuteAsync/ExecuteWriteAsync` (the Polly breaker/retry/bulkhead
|
||||
pipeline). Semantically sophisticated (symbol-identity matching, DIM handling,
|
||||
wrapper-lambda containment), netstandard2.0, `EnforceExtendedAnalyzerRules`, tracked
|
||||
`AnalyzerReleases.*.md`, 31 tests. **But it is wired into zero consuming projects** — see
|
||||
finding C-1.
|
||||
|
||||
### Test architecture map
|
||||
|
||||
47 test projects mirror `src/` under `tests/<module>/` with three tiers:
|
||||
|
||||
1. **`*.Tests`** — pure unit suites (fakes/in-memory), 40 projects. xunit.v3 + Shouldly
|
||||
everywhere except three legacy v2 holdouts still on xunit 2.9.2 (`AdminUI.Tests`,
|
||||
`ControlPlane.Tests`, `Runtime.Tests`).
|
||||
2. **`*.IntegrationTests`** — 10 projects needing a live fixture. Pattern: a collection
|
||||
fixture does a **one-shot TCP reachability probe** against an env-var endpoint whose
|
||||
default is the shared Docker host `10.100.0.35` (e.g.
|
||||
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusSimulatorFixture.cs:37`
|
||||
— `MODBUS_SIM_ENDPOINT` default `10.100.0.35:5020`), and every test `Assert.Skip`s when
|
||||
unreachable. `dotnet test` therefore passes cleanly offline — by *skipping*.
|
||||
3. **`Category=LiveIntegration`** — env-gated live suites against real infrastructure
|
||||
(`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Live/GatewayLiveIntegrationTests.cs`,
|
||||
gated on `HISTGW_GATEWAY_ENDPOINT` etc.). Skip-clean when env vars absent.
|
||||
4. **`Category=E2E`** — reserved; **no project or test carries it yet** (the nightly E2E
|
||||
workflow is a documented no-op).
|
||||
|
||||
### Build / CI pipeline shape
|
||||
|
||||
- `Directory.Build.props` — global net10.0 / nullable / implicit usings / latest lang, plus
|
||||
the CVE-2025-6965 `NuGetAuditSuppress` carve-out. `TreatWarningsAsErrors` is deliberately
|
||||
**opt-in per project** (legacy xUnit1051 debt), with a written promotion plan.
|
||||
- `Directory.Packages.props` — central package management, all 100+ versions pinned, no
|
||||
floating versions, three inline security-pin rationales (Roslyn 5.0.0 CS9057 pin,
|
||||
OpenTelemetry 1.15.3, Tmds.DBus.Protocol 0.21.3).
|
||||
- `NuGet.config` — three sources with **strict packageSourceMapping** (nuget.org wildcard,
|
||||
repo-local `nuget-packages/` for MxGateway, Gitea feed for the ZB.MOM.WW.* shared libs).
|
||||
- CI: two workflow files in `.github/workflows/` (remote is Gitea; header claims
|
||||
Gitea-Actions compatibility). `v2-ci.yml`: build → 5-project unit matrix → 2-project
|
||||
integration matrix. `v2-e2e.yml`: nightly docker-dev fleet + `Category=E2E` filter that
|
||||
matches zero tests.
|
||||
- `ci/` contains only `ab-server.lock.json` (pinned libplctag release for the AB fixture).
|
||||
- `scripts/` — PowerShell operational tooling: `compliance/` (6 phase-gate scripts), `e2e/`
|
||||
(per-driver E2E harnesses + sample config), `install/` (Windows service + Traefik),
|
||||
`migration/`, `smoke/` (SQL seeds), `focas/` (protocol capture), and
|
||||
`check-code-reviews-readme.ps1` (review-index consistency check — not run by CI).
|
||||
- `docker-dev/` — the local 8-service dev fleet (SQL, migrator, cluster-seed, central-1/2,
|
||||
site-a/b pairs, Traefik).
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
Severity scale: **Critical** (broken now, corrupts trust) / **High** (material gap, fix soon)
|
||||
/ **Medium** (real debt) / **Low** (polish).
|
||||
|
||||
### 1. STABILITY
|
||||
|
||||
#### S-1 (High) — CI gates ~15% of the test matrix; client, tooling, driver, and most core suites are never run by CI
|
||||
`.github/workflows/v2-ci.yml:47-52` enumerates **5** unit-test projects (Cluster,
|
||||
ControlPlane, Runtime, Security, OpcUaServer) out of **47** in the solution. Everything in
|
||||
this review's scope — `Client.CLI.Tests` (104 tests), `Client.Shared.Tests` (158),
|
||||
`Client.UI.Tests` (126), `Analyzers.Tests` (31) — plus all driver unit suites, driver CLI
|
||||
suites, and most Core suites (Core, Core.Scripting, VirtualTags, ScriptedAlarms,
|
||||
AlarmHistorian, Commons, Configuration, Abstractions) run only when a developer remembers
|
||||
to run them. The matrix predates most of these projects and was never widened.
|
||||
**Recommendation:** replace the hand-maintained matrix with a single
|
||||
`dotnet test ZB.MOM.WW.OtOpcUa.slnx --filter "Category!=E2E&Category!=LiveIntegration"`
|
||||
leg (the skip-gated integration fixtures already tolerate missing endpoints), or generate
|
||||
the matrix from the slnx. Any new-project drift then becomes impossible.
|
||||
|
||||
#### S-2 (High) — "green CI" for integration tests means "skipped", and nothing distinguishes skip from pass
|
||||
The integration job (`v2-ci.yml:61-76`) runs `Host.IntegrationTests` and
|
||||
`OpcUaServer.IntegrationTests` on `ubuntu-latest` with **no service containers**, while
|
||||
the fixtures default to `10.100.0.35` (unreachable from any hosted runner —
|
||||
`ModbusSimulatorFixture.cs:37`, `AbServerFixture.cs`, `Snap7ServerFixture.cs`,
|
||||
`OpcPlcFixture.cs`, and `Host.IntegrationTests/DriverTestConnectE2eTests.cs` all
|
||||
hard-default to it). Probe-fails → `Assert.Skip` → job green. The design is intentional
|
||||
and well-documented for dev boxes, but in CI it silently converts the entire integration
|
||||
tier into a no-op with a passing badge. **Recommendation:** in CI, either start the
|
||||
fixtures as workflow `services:` (modbus/opc-plc images exist) and set the `*_ENDPOINT`
|
||||
env vars to `localhost`, or add a post-test step that fails the job when skipped-count >
|
||||
threshold (`--logger trx` + parse), so a silent fixture outage cannot masquerade as green.
|
||||
|
||||
#### S-3 (Medium) — the nightly E2E workflow is a permanent green no-op
|
||||
`v2-e2e.yml:6-10` says it plainly: the E2E test project "does not yet exist… this workflow
|
||||
is a green no-op". No test in the tree carries `Category=E2E` (verified by grep). A
|
||||
nightly job that always passes trains people to ignore it; it also boots the full
|
||||
docker-dev fleet for nothing. **Recommendation:** either land the minimal E2E round-trip
|
||||
project (the `scripts/e2e/test-*.ps1` harnesses show exactly what it should assert) or
|
||||
disable the schedule until it exists.
|
||||
|
||||
#### S-4 (Medium) — fixed-sleep timing tests are the dominant wait style
|
||||
`grep Task.Delay|Thread.Sleep` across `tests/` (excluding obj) shows heavy fixed-delay
|
||||
usage: Driver.Galaxy.Tests (30), Driver.Modbus.Tests (22), Driver.AbCip.Tests (20),
|
||||
Host.IntegrationTests (14), and in this slice **Client.CLI.Tests** —
|
||||
`SubscribeCommandTests.cs:30,55,78,103`, `AlarmsCommandTests.cs:26,51,76,104,127,149`
|
||||
(`await Task.Delay(100)` to let a background command loop start before cancelling),
|
||||
`EventHandlerLifecycleTests.cs:54` (150 ms). These pass locally and flake under CI load —
|
||||
which is currently masked because CI never runs them (S-1). **Recommendation:** replace
|
||||
start-up sleeps with a readiness signal from the fake service (e.g. a
|
||||
`TaskCompletionSource` completed on first `SubscribeAsync`), which `FakeOpcUaClientService`
|
||||
can expose cheaply.
|
||||
|
||||
#### S-5 (Medium) — Client.Shared subscription bookkeeping has lock-discipline gaps
|
||||
`OpcUaClientService.cs` documents that `_subscriptionLock` guards the subscription state
|
||||
(lines 18-22), but:
|
||||
- `SubscribeAsync` checks/creates the shared `_dataSubscription` **outside** the lock
|
||||
(line 262) — two concurrent first-subscribers can both see null and create two SDK
|
||||
subscriptions, leaking one.
|
||||
- `RunFailoverAsync` nulls `_dataSubscription` / `_alarmSubscription` **without** the lock
|
||||
(lines 695-696), while `DisconnectAsync`'s comment (lines 138-140) assumes the failover
|
||||
path nulls them under the lock.
|
||||
- `UnsubscribeAlarmsAsync` (line 335) does not take `_alarmSubscribeSemaphore`, so an
|
||||
unsubscribe racing a `SubscribeAlarmsAsync` can delete the adapter the subscriber just
|
||||
created and leave `_alarmSubscription` non-null-but-deleted.
|
||||
- Keep-alive failover is fire-and-forget (`_ = HandleKeepAliveFailureAsync()`, lines
|
||||
115, 718) — correct re-entrancy guard via `Interlocked.CompareExchange` (line 659), but
|
||||
an exception escaping `TransitionState`'s event invocation is unobserved.
|
||||
|
||||
For the CLI's session-per-command usage these races are near-unhittable; for Client.UI
|
||||
(long-lived service, UI-thread + keep-alive-thread concurrency) they are real.
|
||||
**Recommendation:** move the `_dataSubscription` null-check/create inside a small async
|
||||
gate (mirror `_alarmSubscribeSemaphore`), take the lock in `RunFailoverAsync`, and route
|
||||
`UnsubscribeAlarmsAsync` through the semaphore.
|
||||
|
||||
#### S-6 (Medium) — global `Log.Logger` swap per CLI command
|
||||
`CommandBase.ConfigureLogging()` (`CommandBase.cs:120-134`) does `Log.CloseAndFlush()` then
|
||||
replaces the static `Log.Logger` on every command execution. In-process this is a race for
|
||||
any parallel test collections that execute commands concurrently (xunit.v3 parallelises
|
||||
collections by default), and it makes the CLI hostile to embedding. The
|
||||
`LoggerLifecycleTests` suite exists precisely to police this. **Recommendation:** give
|
||||
each command an instance `ILogger` (Serilog `LoggerConfiguration.CreateLogger()` held per
|
||||
execution) instead of mutating the global.
|
||||
|
||||
#### S-7 (Low) — no `global.json`, but the build depends on an exact SDK band
|
||||
`v2-ci.yml:10-12` claims "The .NET 10 SDK is pinned via global.json at the repo root" —
|
||||
**no `global.json` exists**. Meanwhile `Directory.Packages.props:44-49` pins
|
||||
`Microsoft.CodeAnalysis.CSharp` to 5.0.0 explicitly because "SDK 10.0.105 ships compiler
|
||||
5.0.0.0… until the SDK rolls to 10.0.110+". An SDK roll on a runner or dev box silently
|
||||
changes the compiler this pin was matched to. **Recommendation:** add `global.json` with
|
||||
`rollForward: latestFeature` and revisit the Roslyn pin note when it lands.
|
||||
|
||||
#### S-8 (Low) — first-caller interval wins for the shared data subscription
|
||||
`SubscribeAsync` creates one `_dataSubscription` with the first caller's `intervalMs`
|
||||
(`OpcUaClientService.cs:262`); later subscriptions with different intervals join the same
|
||||
publish interval silently (the monitored-item sampling interval is honoured, the publish
|
||||
cadence is not). Fine for the CLI; surprising for UI users mixing 100 ms and 10 s items.
|
||||
Document or create per-interval subscriptions.
|
||||
|
||||
### 2. PERFORMANCE
|
||||
|
||||
#### P-1 (Medium) — CI does 7 independent restore+build passes per push with no caching
|
||||
Every matrix leg in `v2-ci.yml` checks out and implicitly restores/builds from scratch
|
||||
(`dotnet test` without `--no-build`), and the dedicated `build` job's outputs are thrown
|
||||
away. No `actions/cache` for the NuGet package folder. For a solution this size (70+
|
||||
projects) that is the bulk of CI wall-clock. **Recommendation:** cache
|
||||
`~/.nuget/packages` keyed on `Directory.Packages.props`, and either share the build via
|
||||
artifacts or accept rebuild but add `--no-restore` after an explicit cached restore.
|
||||
|
||||
#### P-2 (Low) — recursive browse is N+1 over `HasChildrenAsync`
|
||||
`OpcUaClientService.BrowseAsync` issues an extra `HasChildrenAsync` round-trip per Object
|
||||
child (`OpcUaClientService.cs:230-231`), and `SubscribeCommand.CollectVariablesAsync`
|
||||
walks the tree serially (`SubscribeCommand.cs:256-281`). On the fleet address space
|
||||
(thousands of equipment folders) `subscribe -r` start-up is dominated by this. Acceptable
|
||||
for a diagnostic tool; batch the browse (`BrowseNext` on multiple nodes / read
|
||||
`References` in bulk) if it becomes a soak-test bottleneck.
|
||||
|
||||
#### P-3 (Low, positive) — integration-fixture cost is well-engineered
|
||||
The probe-once collection-fixture pattern (`ModbusSimulatorFixture` remarks: "checking
|
||||
every test would waste several seconds against a firewalled endpoint") is consistently
|
||||
applied across all 10 IntegrationTests projects, and fixtures do not hold sockets open.
|
||||
No per-test container spin-up anywhere. This is the right shape; the problem is what CI
|
||||
does with it (S-2), not the fixtures themselves.
|
||||
|
||||
#### P-4 (Low) — CLI subscription output path is allocation-sane
|
||||
`SubscribeCommand` serialises SDK callbacks through an unbounded single-reader `Channel`
|
||||
(`SubscribeCommand.cs:118-119`) rather than locking the console writer — correct and
|
||||
cheap. The unbounded channel could balloon if the console blocks while thousands of
|
||||
monitored items update; a bounded channel with `DropOldest` would cap it. Cosmetic at
|
||||
current scale.
|
||||
|
||||
### 3. CONVENTIONS
|
||||
|
||||
#### C-1 (High) — the custom analyzer is wired into **zero** projects; OTOPCUA0001 enforces nothing
|
||||
A repo-wide grep of every `.csproj`, `.props`, and `.targets` finds only two references to
|
||||
`ZB.MOM.WW.OtOpcUa.Analyzers`: its own csproj and its test project's `ProjectReference`.
|
||||
No consuming project references it with `OutputItemType="Analyzer"`, and
|
||||
`Directory.Build.props` does not inject it. The carefully built rule — "every
|
||||
IReadable/IWritable/… call must route through CapabilityInvoker" — is enforced **only by
|
||||
its 31 unit tests asserting the analyzer itself works**, not against the actual Core/
|
||||
Server/Driver code it was written to police. This is the same failure mode the project
|
||||
has already been bitten by twice at runtime (the dormant `GatewayTagProvisioner`, the
|
||||
non-forwarding `DeferredAddressSpaceSink`): a capability built and tested but never
|
||||
plugged in. **Recommendation:** add to `Directory.Build.props` (conditioned on
|
||||
`'$(MSBuildProjectName)' != 'ZB.MOM.WW.OtOpcUa.Analyzers'`):
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="$(MSBuildThisFileDirectory)src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers/ZB.MOM.WW.OtOpcUa.Analyzers.csproj"
|
||||
OutputItemType="Analyzer" ReferenceOutputAssembly="false" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
```
|
||||
then triage the first wave of OTOPCUA0001 warnings (add the documented test-project
|
||||
`NoWarn` where intended).
|
||||
|
||||
#### C-2 (Medium) — `TreatWarningsAsErrors` opt-in never reached the Client/Tooling slice (or most tests)
|
||||
`Directory.Build.props:2-11` explains TWE is per-project pending legacy cleanup, and every
|
||||
Core, Server, Driver, and Driver-CLI **src** project has opted in — but
|
||||
`Client.CLI.csproj`, `Client.Shared.csproj`, `Client.UI.csproj`, and
|
||||
`Analyzers.csproj` have not, and only 9 of 47 test projects have. The comment's stated
|
||||
blocker ("pre-v2 test projects… xUnit1051") does not apply to the client stack, which is
|
||||
v2-era code. **Recommendation:** add TWE to the four remaining src projects now (they
|
||||
build warning-clean or nearly so), and burn down the test-project debt per module.
|
||||
|
||||
#### C-3 (Low) — central package management discipline is exemplary; the audit carve-out is still valid
|
||||
Verified current state: CPM enabled, every version pinned, `packageSourceMapping` prevents
|
||||
dependency-confusion for the private `ZB.MOM.WW.*` namespaces (`NuGet.config:9-29`), and
|
||||
the `NuGetAuditSuppress` for GHSA-2m69-gcr7-jv3q (`Directory.Build.props:19-32`) is still
|
||||
present, still scoped to a single advisory, and still accurate as written (transitive
|
||||
SQLitePCLRaw native bundle, no patched release; documented removal condition). The two
|
||||
transitive CVE pins (OpenTelemetry 1.15.3 at `Directory.Packages.props:80-87`,
|
||||
Tmds.DBus.Protocol 0.21.3 at lines 104-107 with the matching direct reference in
|
||||
`Client.UI.csproj`) follow the memory-documented "surgical direct reference" strategy.
|
||||
No action; keep the removal reminders alive.
|
||||
|
||||
#### C-4 (Low) — test naming/layout is consistent; one categorisation deviation
|
||||
`*.Tests` vs `*.IntegrationTests` naming is uniform and mirrors `src/` exactly. The single
|
||||
deviation: live-gated tests live *inside* a unit-suite project
|
||||
(`Driver.Historian.Gateway.Tests/Live/GatewayLiveIntegrationTests.cs`,
|
||||
`Category=LiveIntegration`) rather than a `*.IntegrationTests` sibling — harmless because
|
||||
they skip-gate on env vars, but a `dotnet test tests/Drivers/...Gateway.Tests` run now has
|
||||
a hidden live-test dependency surface. Framework split: 44 projects on xunit.v3, 3 legacy
|
||||
on xunit 2.x (`AdminUI.Tests`, `ControlPlane.Tests`, `Runtime.Tests`) — finish the
|
||||
migration and drop the `xunit` 2.9.2 pin from `Directory.Packages.props:108`.
|
||||
|
||||
#### C-5 (Low) — no mechanical code-style enforcement exists; StyleGuide.md is a *docs* style guide
|
||||
There is **no `.editorconfig`** in the repo, no `dotnet format` CI step, and
|
||||
`StyleGuide.md` is exclusively a documentation-writing guide — whose opening line still
|
||||
says "for all **ScadaBridge** documentation" (`StyleGuide.md:3`), a copy-paste from the
|
||||
sister repo. Code style is therefore enforced only by review culture. **Recommendation:**
|
||||
add a root `.editorconfig` (the implicit conventions are already consistent — file-scoped
|
||||
namespaces, 4-space, `_camelCase` fields) and fix the StyleGuide title/product name.
|
||||
|
||||
#### C-6 (Low) — csproj boilerplate duplicates Directory.Build.props
|
||||
Nearly every csproj restates `TargetFramework`/`Nullable`/`ImplicitUsings` that
|
||||
`Directory.Build.props:12-17` already supplies (e.g. `Client.CLI.csproj:5-8`). Harmless
|
||||
but it means an SDK bump requires touching 70+ files instead of one. Strip on next sweep.
|
||||
|
||||
### 4. UNDERDEVELOPED AREAS
|
||||
|
||||
#### U-1 — Client.UI maturity: real product, adequately tested, current docs
|
||||
Verdict: **not vestigial**. ~2.9 k LOC C#, 551 lines AXAML, MVVM with dispatcher and
|
||||
settings seams, alarm ack/confirm/shelve dialogs, history view with custom range picker,
|
||||
126 headless tests, and `docs/Client.UI.md` matches the code (verified stack table and
|
||||
window-layout claims). It is the least exercised layer in anger (no CI — S-1 — and no
|
||||
E2E), and headless tests can't catch AXAML binding regressions (the AdminUI "no bUnit —
|
||||
live-verify" lesson applies equally here). Rating it "maturing", not "underdeveloped".
|
||||
|
||||
#### U-2 (Medium) — source → test coverage matrix: three structural gaps
|
||||
Matrix over the 41 solution src projects (test projects verified against the slnx):
|
||||
|
||||
| Source project | Unit tests | Integration | Gap |
|
||||
|---|---|---|---|
|
||||
| Client.Shared | ✅ Client.Shared.Tests (158) | — | none |
|
||||
| Client.CLI | ✅ Client.CLI.Tests (104) | — | none |
|
||||
| Client.UI | ✅ Client.UI.Tests (126, headless) | — | none |
|
||||
| Tooling/Analyzers | ✅ Analyzers.Tests (31) | — | **not wired into builds (C-1)** |
|
||||
| Core, Commons, Cluster, Configuration, Core.Scripting, VirtualTags, ScriptedAlarms, AlarmHistorian, Core.Abstractions | ✅ each | — | none |
|
||||
| **Core.Scripting.Abstractions** | ❌ | — | no dedicated tests (thin interfaces; covered incidentally by Core.Scripting.Tests) |
|
||||
| AdminUI, ControlPlane, Runtime, Security, OpcUaServer | ✅ each | OpcUaServer.IntegrationTests | none |
|
||||
| **Host** | ❌ **no Host.Tests** | Host.IntegrationTests only | Host's DI/wiring logic (the layer where both "dormant wiring" bugs lived) has no unit tier |
|
||||
| All 8 protocol/gateway drivers + Modbus.Addressing + 2 Browsers + Historian.Gateway | ✅ each | 7 × IntegrationTests + LiveIntegration | none |
|
||||
| **8 × `*.Contracts` projects** (Galaxy, Modbus, S7, AbCip, AbLegacy, TwinCAT, FOCAS, OpcUaClient) | ❌ | — | DTO-only; consuming-suite coverage; OpcUaClient.Contracts NamespaceMap gap already known-deferred |
|
||||
| 7 × Driver CLIs + Cli.Common | ✅ each | — | none |
|
||||
|
||||
The one gap worth acting on is **Host**: it is where registration/forwarding mistakes
|
||||
land, and its only automated coverage requires the 2-node harness. A `Host.Tests` project
|
||||
asserting DI composition (e.g. "when `ServerHistorian:Enabled`, `IHistorianProvisioning`
|
||||
resolves to `GatewayTagProvisioner` and the applier receives it") would have caught the
|
||||
PR #423 dormancy at unit speed.
|
||||
|
||||
#### U-3 (Medium) — docs drift: `docs/Client.CLI.md` documents 8 of 14 commands
|
||||
Spot-checks performed:
|
||||
1. `docs/Client.CLI.md` command sections: connect, read, write, browse, subscribe,
|
||||
historyread, alarms, redundancy — **`ack`, `confirm`, `shelve`, `enable`, `disable`
|
||||
have zero mentions** (grep), yet the commands ship
|
||||
(`Commands/AcknowledgeCommand.cs`, `ConfirmCommand.cs`, `ShelveCommand.cs`,
|
||||
`EnableCommand.cs`, `DisableCommand.cs`) and CLAUDE.md explicitly says "Client.CLI
|
||||
supports `ack`, `confirm`, `shelve` commands. See `docs/Client.CLI.md` for full
|
||||
documentation." Five commands undocumented.
|
||||
2. The doc's "104 unit tests" claim (`docs/Client.CLI.md`, Testing section) — **accurate**:
|
||||
exactly 104 `[Fact]`/`[Theory]` in Client.CLI.Tests today.
|
||||
3. `docs/Client.UI.md` stack/layout claims — **accurate** vs csproj and Views/.
|
||||
4. `v2-ci.yml:10-12` global.json claim — **false** (S-7).
|
||||
Recommendation: add the five missing command sections; they are the operator-facing alarm
|
||||
workflow.
|
||||
|
||||
#### U-4 (Medium) — repo-root planning-file sprawl contradicts its own rules
|
||||
Five point-in-time planning/state files are committed at the root: `looseends.md` (state
|
||||
as of 2026-05-18), `pending.md` (2026-06-16), `stillpending.md`, `current.md`,
|
||||
`HISTORIAN-GATEWAY-INTEGRATION-ISSUES.md`. `pending.md` itself declares "HARD RULE: never
|
||||
`git add .`; never stage `pending.md` / `current.md` / …" — **yet `pending.md` and
|
||||
`current.md` are tracked** (they appear in `git ls-files`; last commits `cd20c3c0`,
|
||||
`384dbd7d`). The memory index says the stillpending backlog is ~95% shipped, so most of
|
||||
this content is stale snapshots that now contradict `docs/` and CLAUDE.md.
|
||||
**Recommendation:** either move them under `docs/plans/` with dated names (the existing
|
||||
convention) or delete + gitignore them; make the file's own rule true.
|
||||
|
||||
#### U-5 (Medium) — orphaned proprietary AVEVA DLLs tracked in `lib/`
|
||||
`lib/` contains 7 **committed** vendor binaries (`aahClient.dll`, `aahClientCommon.dll`,
|
||||
`aahClientManaged.dll`, `ArchestrA.MXAccess.dll`, `ArchestrA.CloudHistorian.Contract.dll`,
|
||||
`Historian.CBE.dll`, `Historian.DPAPI.dll`). No csproj anywhere references them (zero
|
||||
`HintPath` hits repo-wide) — they are leftovers from the retired Wonderware sidecar /
|
||||
in-process MXAccess era. Committed proprietary SDK DLLs are a redistribution/licence risk
|
||||
in any clone of this repo and dead weight in history. **Recommendation:** `git rm -r lib/`
|
||||
(the bitness/COM story now lives entirely in the mxaccessgw repo, per CLAUDE.md).
|
||||
|
||||
#### U-6 (Low) — secrets hygiene: `sql_login.txt` is NOT committed (gitignored), but is still a plaintext credential at the root
|
||||
Verified: `.gitignore:47` lists it, `git check-ignore` confirms, and
|
||||
`git log -- sql_login.txt` is empty — the file has never been committed. The remaining
|
||||
risk is purely local (plaintext `wwadmin` password for `wonder-sql-vd03` sitting in a
|
||||
Desktop directory that agents and sync tools read). **Recommendation:** move to
|
||||
`dotnet user-secrets` / an env file outside the repo; at minimum keep it out of any
|
||||
export bundle (`export-clean-copy.bat` at the root should be checked to exclude it).
|
||||
Also note the retired `Driver.Historian.Wonderware*` project dirs still exist on disk as
|
||||
ignored `bin/obj` husks — local-only debris, safe to delete.
|
||||
|
||||
#### U-7 (Low) — missing CI stages (inventory)
|
||||
Not gated anywhere today: analyzer tests + analyzer enforcement (C-1), all client/driver/
|
||||
core unit suites (S-1), `dotnet format`/style (C-5), NuGet vulnerability audit as an
|
||||
explicit failing step (currently only implicit in restore warnings, which are not TWE'd
|
||||
in most projects), the `scripts/check-code-reviews-readme.ps1` consistency check, and any
|
||||
docs link/freshness check. The `ci/ab-server.lock.json` fixture pin references a
|
||||
"GitHub Actions step" in `docs/v2/test-data-sources.md` that does not exist in either
|
||||
workflow — the AB fixture download step was planned but never landed.
|
||||
|
||||
#### U-8 (Low) — CLI security posture is dev-tool-grade by construction
|
||||
`CommandBase.CreateConnectionSettings()` hardcodes `AutoAcceptCertificates = true`
|
||||
(`CommandBase.cs:91`), so `--security signandencrypt` encrypts but never authenticates the
|
||||
server (any cert accepted → MITM-able), and `-P` takes the password as a process-visible
|
||||
argv. Acceptable for a diagnostic tool, but the doc should say so, and a
|
||||
`--strict-certs` opt-in would be cheap. Also `Client.Shared`'s alarm fallback path bakes
|
||||
Galaxy-specific attribute names (`.InAlarm`, `.Acked`, `.TimeAlarmOn`, `.DescAttrName`)
|
||||
into the generic client library (`OpcUaClientService.cs:851-871`) — a documented but
|
||||
layering-violating convenience.
|
||||
|
||||
---
|
||||
|
||||
## Maturity Ratings (1 = ad hoc, 5 = exemplary)
|
||||
|
||||
| Dimension | Rating | Justification |
|
||||
|---|---|---|
|
||||
| Stability | **2.5** | The client code and fixture patterns are careful, but CI runs ~15% of the suite, the integration tier silently skips to green on hosted runners, and the nightly E2E is a documented no-op — the safety net exists mostly on developer machines. |
|
||||
| Performance | **3.5** | Build/test structure is lean (CPM, probe-once fixtures, channel-serialised CLI output); loses points for 7× uncached restore/builds in CI and the N+1 recursive browse. |
|
||||
| Conventions | **3** | Package management and test layout are exemplary and self-documenting, but the flagship convention-enforcement tool (the analyzer) is wired into nothing, TWE never reached the client slice, and there is no .editorconfig — conventions hold by culture, not mechanism. |
|
||||
| Underdeveloped areas | **3** | Client.UI is genuinely mature and the coverage matrix is nearly complete (Host is the one real hole); dragged down by 5-command docs drift, committed stale planning files that violate their own rules, and orphaned proprietary DLLs in `lib/`. |
|
||||
|
||||
---
|
||||
|
||||
## Top recommendations (ordered)
|
||||
|
||||
1. Wire `ZB.MOM.WW.OtOpcUa.Analyzers` into every project via `Directory.Build.props` (C-1).
|
||||
2. Widen `v2-ci.yml` to the whole solution and make skipped integration tests visible/failing in CI (S-1, S-2).
|
||||
3. Delete tracked `lib/` vendor DLLs (U-5) and resolve the root planning-file contradiction (U-4).
|
||||
4. Add the five missing command sections to `docs/Client.CLI.md` (U-3).
|
||||
5. Add `global.json` (S-7), `Host.Tests` (U-2), and TWE to the four client/tooling csprojs (C-2).
|
||||
6. Fix the three `OpcUaClientService` lock-discipline gaps before Client.UI grows long-lived multi-thread usage (S-5).
|
||||
Reference in New Issue
Block a user