# 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/`, `` kept as `ZB.MOM.WW.OtOpcUa.Tests` so the v1 Host's `InternalsVisibleTo` still matches, `false` so `dotnet test slnx` excludes it. 2. **Three other v1 projects archive-marked** with PropertyGroup comments: `OtOpcUa.Host`, `Historian.Aveva`, `IntegrationTests`. `IntegrationTests` also gets `false`. 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