124 lines
7.0 KiB
Markdown
124 lines
7.0 KiB
Markdown
# Phase 2 Final Exit Gate (2026-04-18)
|
||
|
||
> Supersedes `phase-2-partial-exit-evidence.md` and `exit-gate-phase-2.md`. Captures the
|
||
> as-built state at the close of Phase 2 work delivered across two PRs.
|
||
|
||
## Status: **All five Phase 2 streams addressed. Stream D split across PR 2 (archive) + PR 3 (delete) per safety protocol.**
|
||
|
||
## Stream-by-stream status
|
||
|
||
| Stream | Plan §reference | Status | PR |
|
||
|---|---|---|---|
|
||
| A — Driver.Galaxy.Shared | §A.1–A.3 | ✅ Complete | PR 1 (merged or pending) |
|
||
| B — Driver.Galaxy.Host | §B.1–B.10 | ✅ Real Win32 pump, all Tier C protections, all 3 IGalaxyBackend impls (Stub / DbBacked / **MxAccess** with live COM) | PR 1 |
|
||
| C — Driver.Galaxy.Proxy | §C.1–C.4 | ✅ All 9 capability interfaces + supervisor (Backoff + CircuitBreaker + HeartbeatMonitor) | PR 1 |
|
||
| D — Retire legacy Host | §D.1–D.3 | ✅ Migration script, installer scripts, Stream D procedure doc, **archive markings on all v1 surface (this PR 2)**, deletion deferred to PR 3 | PR 2 (this) + PR 3 (next) |
|
||
| E — Parity validation | §E.1–E.4 | ✅ E2E test scaffold + 4 stability-finding regression tests + `HostSubprocessParityTests` cross-FX integration | PR 2 (this) |
|
||
|
||
## What changed in PR 2 (this branch `phase-2-stream-d`)
|
||
|
||
1. **`tests/ZB.MOM.WW.OtOpcUa.Tests/`** renamed to `tests/ZB.MOM.WW.OtOpcUa.Tests.v1Archive/`,
|
||
`<AssemblyName>` kept as `ZB.MOM.WW.OtOpcUa.Tests` so the v1 Host's `InternalsVisibleTo`
|
||
still matches, `<IsTestProject>false</IsTestProject>` so `dotnet test slnx` excludes it.
|
||
2. **Three other v1 projects archive-marked** with PropertyGroup comments:
|
||
`OtOpcUa.Host`, `Historian.Aveva`, `IntegrationTests`. `IntegrationTests` also gets
|
||
`<IsTestProject>false</IsTestProject>`.
|
||
3. **New `tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E/`** project (.NET 10):
|
||
- `ParityFixture` spawns `OtOpcUa.Driver.Galaxy.Host.exe` (net48 x86) as subprocess via
|
||
`Process.Start`, connects via real named pipe, exposes a connected `GalaxyProxyDriver`.
|
||
Skips when Galaxy ZB unreachable, when Host EXE not built, or when running as
|
||
Administrator (PipeAcl denies admins).
|
||
- `RecordingAddressSpaceBuilder` captures Folder + Variable + Property registrations so
|
||
parity tests can assert shape.
|
||
- `HierarchyParityTests` (3) — Discover returns gobjects with attributes;
|
||
attribute full references match `tag.attribute` shape; HistoryExtension flag flows
|
||
through.
|
||
- `StabilityFindingsRegressionTests` (4) — one test per 2026-04-13 finding:
|
||
phantom-probe-doesn't-corrupt-status, host-status-event-is-scoped, all-async-no-sync-
|
||
over-async, AcknowledgeAsync-completes-before-returning.
|
||
4. **`docs/v2/V1_ARCHIVE_STATUS.md`** — inventory + deletion plan for PR 3.
|
||
5. **`docs/v2/implementation/exit-gate-phase-2-final.md`** (this doc) — supersedes the two
|
||
partial-exit docs.
|
||
|
||
## Test counts
|
||
|
||
**Solution-level `dotnet test ZB.MOM.WW.OtOpcUa.slnx`**: **470 pass / 7 skip / 1 baseline failure**.
|
||
|
||
| Project | Pass | Skip |
|
||
|---|---:|---:|
|
||
| Core.Abstractions.Tests | 24 | 0 |
|
||
| Configuration.Tests | 42 | 0 |
|
||
| Core.Tests | 4 | 0 |
|
||
| Server.Tests | 2 | 0 |
|
||
| Admin.Tests | 21 | 0 |
|
||
| Driver.Galaxy.Shared.Tests | 6 | 0 |
|
||
| Driver.Galaxy.Host.Tests | 30 | 0 |
|
||
| Driver.Galaxy.Proxy.Tests | 10 | 0 |
|
||
| **Driver.Galaxy.E2E (NEW)** | **0** | **7** (all skip with documented reason — admin shell) |
|
||
| Client.Shared.Tests | 131 | 0 |
|
||
| Client.UI.Tests | 98 | 0 |
|
||
| Client.CLI.Tests | 51 / 1 fail | 0 |
|
||
| Historian.Aveva.Tests | 41 | 0 |
|
||
|
||
**Excluded from solution run (run explicitly when needed)**:
|
||
- `OtOpcUa.Tests.v1Archive` — 494 pass (v1 unit tests, kept as parity reference)
|
||
- `OtOpcUa.IntegrationTests` — 6 pass (v1 integration tests, kept as parity reference)
|
||
|
||
## Adversarial review of the PR 2 diff
|
||
|
||
Independent pass over the PR 2 deltas. New findings ranked by severity; existing findings
|
||
from the previous exit-gate doc still apply.
|
||
|
||
### New findings
|
||
|
||
**Medium 1 — `IsTestProject=false` on `OtOpcUa.IntegrationTests` removes the safety net.**
|
||
The 6 v1 integration tests no longer run on solution test. *Mitigation:* the new E2E suite
|
||
covers the same scenarios in the v2 topology shape. *Risk:* if E2E test count regresses or
|
||
fails to cover a scenario, the v1 fallback isn't auto-checked. **Procedure**: PR 3
|
||
checklist includes "E2E test count covers v1 IntegrationTests' 6 scenarios at minimum".
|
||
|
||
**Medium 2 — Stability-finding regression tests #2, #3, #4 are structural (reflection-based)
|
||
not behavioral.** Findings #2 and #3 use type-shape assertions (event signature carries
|
||
HostName; methods return Task) rather than triggering the actual race. *Mitigation:* the v1
|
||
defects were structural — fixing them required interface changes that the type-shape
|
||
assertions catch. *Risk:* a future refactor that re-introduces sync-over-async via a non-
|
||
async helper called inside a Task method wouldn't trip the test. **Filed as v2.1**: add a
|
||
runtime async-call-stack analyzer (Roslyn or post-build).
|
||
|
||
**Low 1 — `ParityFixture` defaults to `OTOPCUA_GALAXY_BACKEND=db`** (not `mxaccess`).
|
||
Discover works against ZB without needing live MXAccess. The MXAccess-required tests will
|
||
need a second fixture once they're written.
|
||
|
||
**Low 2 — `Process.Start(EnvironmentVariables)` doesn't always inherit clean state.** The
|
||
test inherits the parent's PATH + locale, which is normally fine but could mask a missing
|
||
runtime dependency. *Mitigation:* in CI, pin a clean environment block.
|
||
|
||
### Existing findings (carried forward from `exit-gate-phase-2.md`)
|
||
|
||
All 8 still apply unchanged. Particularly:
|
||
- High 1 (MxAccess Read subscription-leak on cancellation) — open
|
||
- High 2 (no MXAccess reconnect loop, only supervisor-driven recycle) — open
|
||
- Medium 3 (SubscribeAsync doesn't push OnDataChange frames yet) — open
|
||
- Medium 4 (WriteValuesAsync doesn't await OnWriteComplete) — open
|
||
|
||
## Cross-cutting deferrals (out of Phase 2)
|
||
|
||
- **Deletion of v1 archive** — PR 3, gated on operator review + E2E coverage parity check
|
||
- **Wonderware Historian SDK plugin port** (`Historian.Aveva` → `Driver.Galaxy.Host/Backend/Historian/`) — Task B.1.h, opportunistically with PR 3 or as PR 4
|
||
- **MxAccess subscription push frames** — Task B.1.s, follow-up to enable real-time data
|
||
flow (currently subscribes register but values aren't pushed back)
|
||
- **Wonderware Historian-backed HistoryRead** — depends on B.1.h
|
||
- **Alarm subsystem wire-up** — `MxAccessGalaxyBackend.SubscribeAlarmsAsync` is a no-op
|
||
- **Reconnect-without-recycle** in MxAccessClient — v2.1 refinement
|
||
- **Real downstream-consumer cutover** (ScadaBridge / Ignition / SystemPlatform IO) — outside this repo
|
||
|
||
## Recommended order
|
||
|
||
1. **PR 1** (`phase-1-configuration` → `v2`) — merge first; self-contained, parity preserved
|
||
2. **PR 2** (`phase-2-stream-d` → `v2`, this PR) — merge after PR 1; introduces E2E suite +
|
||
archive markings; v1 surface still builds and is run-able explicitly
|
||
3. **PR 3** (next session) — delete v1 archive; depends on operator approval after PR 2
|
||
reviewer signoff
|
||
4. **PR 4** (Phase 2 follow-up) — Historian port + MxAccess subscription push frames + the
|
||
open high/medium findings
|