9fadead6a6
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.
412 lines
28 KiB
Markdown
412 lines
28 KiB
Markdown
# 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).
|