test(mtconnect): close live-gate leg 5 — deploy -> OPC UA read/subscribe PASSED
v2-ci / build (pull_request) Successful in 6m48s
v2-ci / unit-tests (pull_request) Failing after 18m6s

Re-ran the gate on the rebased branch against a freshly rebuilt
otopcua-host:mtconnect image, so it exercises the merged code.

The 2026-07-24 blocker (antiforgery cookie collision between the running
otopcua-dev rig on :9200 and the isolated stack on :9220) is sidestepped
rather than worked around: the headless deploy API
(POST /api/deployments + X-Api-Key) is AllowAnonymous().DisableAntiforgery()
by design, so cookie scoping is structurally irrelevant to it. No browser
needed — the recommended recipe for any future isolated-stack gate.

Both deploys sealed (DeploymentStatus.Sealed, FailureReason NULL) with both
MAIN nodes acking. Read + subscribe verified over opc.tcp://localhost:4860
across all three type paths: Float64 SAMPLE (sinusoid), Int64 EVENT
(monotonic), String EVENT (READY -> INTERRUPTED). This closes the last
unproven link — driver seam was already covered by the live integration
suite; leg 5 adds DeploymentArtifact -> address space -> OPC UA publish.

Two findings recorded in the plan:

- A bad authored dataType degrades to exactly ONE skipped tag, loudly
  (per-tag warning naming the expected keys, BadNodeIdUnknown, "every other
  tag is unaffected") while the rest keep streaming — the intended
  fail-isolated behaviour, demonstrated rather than argued. Correcting it
  took effect with no restart, showing MTConnect is not subject to the
  separately-tracked config-edits-discarded defect.

- fixture_asset_changed reading 0x80000000 is NOT an MTConnect defect. The
  driver maps the agent's UNAVAILABLE to BadNoCommunication correctly; the
  shared publish path collapses every status to a 3-state OpcUaQuality enum
  (DriverInstanceActor.QualityFromStatus, statusCode >> 30) and re-expands
  Bad to StatusCodes.Bad (OtOpcUaNodeManager.StatusFromQuality). Deliberate
  per the enum's own doc comment, but the client-visible consequence appears
  undocumented: no driver's Bad/Uncertain sub-code ever reaches a client.
  Tracked separately; out of scope here.

Also records the rebase onto master 123ddc3f and confirms StatusCodeParityTests
now covers 9 MTConnect constants, all passing.
This commit is contained in:
Joseph Doherty
2026-07-27 13:23:13 -04:00
parent 9d430bee62
commit 813e445936
+104 -14
View File
@@ -894,7 +894,7 @@ git commit -m "test(mtconnect): live /run gate on docker-dev — browse/read/sub
--- ---
### LIVE-GATE RESULT (2026-07-24) — 4 of 5 legs PASSED; the deploy leg is BLOCKED on a rig artifact, not a defect ### LIVE-GATE RESULT (2026-07-24, completed 2026-07-27) — ALL 5 LEGS PASSED
**Rig.** The shared `otopcua-dev` rig was NOT used: three sibling driver worktrees **Rig.** The shared `otopcua-dev` rig was NOT used: three sibling driver worktrees
(`modbus-rtu`, `mqtt-sparkplug`, `sql-poll-driver`) share its `otopcua-host:dev` image, and it had (`modbus-rtu`, `mqtt-sparkplug`, `sql-poll-driver`) share its `otopcua-host:dev` image, and it had
@@ -917,9 +917,96 @@ serves the seeded `OtFixtureCnc` model with live-moving values. Note it answers
| 2. Typed config modal renders | ✅ | "Configure driver · MTConnect" with all four sections (Agent / Polling / Connectivity probe / Reconnect), **not** the "no typed config form" banner. **This is the mutation that survived all 749 AdminUI tests** — the `switch` in `DriverConfigModal.razor` compiles to `BuildRenderTree` and is unreachable by reflection. | | 2. Typed config modal renders | ✅ | "Configure driver · MTConnect" with all four sections (Agent / Polling / Connectivity probe / Reconnect), **not** the "no typed config form" banner. **This is the mutation that survived all 749 AdminUI tests** — the `switch` in `DriverConfigModal.razor` compiles to `BuildRenderTree` and is unreachable by reflection. |
| 3. Config persists + round-trips | ✅ | Stored `{"agentUri":"http://10.100.0.35:5000","requestTimeoutMs":5000,…,"probe":{"enabled":true,…},"reconnect":{…}}`; reopening the modal displays the saved URI (Razor binding verified — no bUnit in this repo). | | 3. Config persists + round-trips | ✅ | Stored `{"agentUri":"http://10.100.0.35:5000","requestTimeoutMs":5000,…,"probe":{"enabled":true,…},"reconnect":{…}}`; reopening the modal displays the saved URI (Razor binding verified — no bUnit in this repo). |
| 4. Browse picker → commit | ✅ | "Connect & browse" dialled the live Agent; tree rendered `Agent` + `OtFixtureCnc` with `Favail` (name ≠ id) beside id-named leaves, proving `browseName = Name ?? Id`. Committed TagConfig is **`{"fullName":"fixture_asset_changed","dataType":"String"}`** — the `fullName`-not-`address` fix, live. Wave-0 #468 did **not** block this. | | 4. Browse picker → commit | ✅ | "Connect & browse" dialled the live Agent; tree rendered `Agent` + `OtFixtureCnc` with `Favail` (name ≠ id) beside id-named leaves, proving `browseName = Name ?? Id`. Committed TagConfig is **`{"fullName":"fixture_asset_changed","dataType":"String"}`** — the `fullName`-not-`address` fix, live. Wave-0 #468 did **not** block this. |
| 5. Deploy → OPC UA read/subscribe | **BLOCKED — rig artifact** | see below | | 5. Deploy → OPC UA read/subscribe | **PASSED (2026-07-27, post-rebase)** | see "Leg 5" below |
**Why leg 5 is blocked, and why it is not a product defect.** Browser cookies are scoped to a ---
#### Leg 5 — PASSED 2026-07-27, on the rebased branch
Re-run after the rebase onto master (`123ddc3f`), against a **freshly rebuilt** `otopcua-host:mtconnect`
image, so this gate exercises the merged code and not the pre-rebase tree.
**The cookie blocker was sidestepped, not worked around.** Rather than moving hosts or stopping the
`otopcua-dev` rig, the deploy went through the **headless deploy API**
`POST /api/deployments` with `X-Api-Key` (`DeployApiEndpoints`, `Security:DeployApiKey`). It is
`AllowAnonymous().DisableAntiforgery()` by design ("machine endpoint, not a browser form post"), so
the antiforgery cookie collision is structurally irrelevant to it. This is the better recipe for any
future isolated-stack gate — it needs no browser at all:
```bash
curl -s -X POST http://localhost:9220/api/deployments \
-H "X-Api-Key: docker-dev-deploy-key" -H "Content-Type: application/json" \
-d '{"CreatedBy":"mtconnect-live-gate"}'
# {"outcome":"Accepted","deploymentId":"…","revisionHash":"…"} HTTP 202
```
Both deployments sealed `Status = 2` (`DeploymentStatus.Sealed`) with `FailureReason = NULL` — i.e.
**both** MAIN nodes acked. No `MaintenanceMode` hatch was needed: the isolated stack's topology is
MAIN-only with both central nodes enabled and running.
**Tag set.** Leg 4's AdminUI-committed tag (`fixture_asset_changed`) is an `AssetChanged` EVENT that
never moves, so three live-changing tags were added **directly via SQL** (deliberately bypassing the
typed editor — see the finding below) to exercise all three type paths.
**Results — read (`Client.CLI read`, `opc.tcp://localhost:4860`):**
| NodeId (`ns=2`, Raw realm) | Value | Status |
|---|---|---|
| `s=mtc-1/vmc/fixture_partcount` | `48099` (Int64) | `0x00000000` Good |
| `s=mtc-1/vmc/fixture_x_pos` | `96.4416` (Float64) | `0x00000000` Good |
| `s=mtc-1/vmc/fixture_execution` | `ACTIVE` (String) | `0x00000000` Good |
| `s=mtc-1/vmc/fixture_asset_changed` | *(null)* | `0x80000000` — see finding 6 |
The RawPath is `mtc-1/vmc/<dataItemId>` (driver/device/tag; no folder prefix), confirmed by
`browse -r`, which rendered all four as `[Variable]` under the `vmc` device object.
**Results — subscribe (`Client.CLI subscribe -r` over the whole device, 500 ms):** all four monitored;
live updates flowed continuously from the `/sample` long-poll pump through to OPC UA — `fixture_x_pos`
tracing its sinusoid (`103.66 → 123.18 → 141.90 → 155.27 → 159.99 → 154.93 → 141.32 → 122.48 → 103.03
→ 87.75 → 80.35`), `fixture_partcount` incrementing monotonically (`48128 → 48129 → 48130`), and
`fixture_execution` transitioning `READY → INTERRUPTED`. **This closes the last unproven link**: the
driver seam was already covered by the live integration suite, and leg 5 adds `DeploymentArtifact`
address-space materialisation → OPC UA publish on top of it.
**Additional findings from leg 5:**
5. **A bad `dataType` degrades to exactly one skipped tag, loudly — verified by accident.** The
hand-written SQL used `"dataType":"Double"`; the vocabulary is `DriverDataType`, whose member is
**`Float64`**. The driver logged, per tag:
> `could not map the raw tag mtc-1/vmc/fixture_x_pos to an Agent DataItem; it names no readable id
> (expected 'fullName', 'dataItemId' or 'address') or declares an unrecognised driverDataType. The
> tag is skipped and reports BadNodeIdUnknown; every other tag is unaffected.`
…and the other three tags kept streaming. That is the intended fail-isolated behaviour,
demonstrated live rather than argued. Correcting the row to `Float64` and redeploying turned the
tag Good with **no container restart** — which incidentally shows **MTConnect is not subject to the
separately-tracked config-edits-silently-discarded defect** that affects Modbus/FOCAS/OpcUaClient.
Note also that the typed AdminUI editor would have prevented the mistake outright (it offers a
`DriverDataType` dropdown); the error was an artifact of authoring straight into SQL.
6. **`fixture_asset_changed` reading `0x80000000` is NOT an MTConnect defect — it is a pre-existing,
deliberate fidelity gap on the shared publish path.** The agent does return the tag in `/current`
as `<AssetChanged …>UNAVAILABLE</AssetChanged>`, and the driver maps it correctly to
`BadNoCommunication` (`0x80310000`, `MTConnectObservationIndex.cs:255`). The node nevertheless
reports generic `Bad` (`0x80000000`) — while carrying the agent's exact source timestamp, which
proves the publish landed and only the status differs. Cause, verified by reading the source:
`DriverInstanceActor.QualityFromStatus` (`DriverInstanceActor.cs:1032-1041`) projects the driver's
`uint` onto the 3-state `OpcUaQuality` enum using only the top two severity bits (`statusCode >> 30`),
and `OtOpcUaNodeManager.StatusFromQuality` (`OtOpcUaNodeManager.cs:3318-3322`) re-expands `Bad` to
`StatusCodes.Bad`. It is intentional — `OpcUaQuality`'s own doc comment says *"Real SDK has
finer-grained codes; the engine actors only need this 3-state classification."*
The consequence appears undocumented and is client-visible: **no driver's Bad/Uncertain sub-code
ever reaches an OPC UA client.** Issue #497's 16 corrected constants and the new
`StatusCodeParityTests` guard keep the constants internally right, but a client cannot tell
`BadNoCommunication` from `BadTypeMismatch` from `BadNotSupported`. (The one survivor is
`BadWaitingForInitialData`, written directly at the node at `OtOpcUaNodeManager.cs:1806/1901`,
bypassing the projection.) Tracked separately; out of scope for this plan.
---
**Historical — why leg 5 was blocked on 2026-07-24 (superseded by the API-key recipe above).**
Browser cookies are scoped to a
**host, not a port**, so the AdminUI antiforgery cookie issued by the still-running `otopcua-dev` **host, not a port**, so the AdminUI antiforgery cookie issued by the still-running `otopcua-dev`
rig on `localhost:9200` is sent to the isolated stack on `localhost:9220`, whose data-protection rig on `localhost:9200` is sent to the isolated stack on `localhost:9220`, whose data-protection
keys differ: keys differ:
@@ -933,12 +1020,9 @@ The `Deploy current configuration` POST is rejected before reaching any MTConnec
(the antiforgery cookie is `HttpOnly`). This is a two-stacks-on-one-host collision, entirely outside (the antiforgery cookie is `HttpOnly`). This is a two-stacks-on-one-host collision, entirely outside
the driver. the driver.
**To finish leg 5**, do any one of: (a) use a distinct host so cookies don't collide (a hosts entry, The three browser-side remedies originally listed (distinct host / incognito profile / stop the
or bind `127.0.0.2`), (b) drive `:9220` from an incognito/separate browser profile, or (c) stop the `otopcua-dev` rig) all remain valid, but none was needed — the headless deploy API above avoids the
`otopcua-dev` rig first. Then Deploy and: browser entirely and is the recommended recipe.
`dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- read -u opc.tcp://localhost:4860 -n "ns=2;s=docker-dev/mtc-1/vmc/fixture_asset_changed"`.
Note the RawPath keying is already proven at the driver seam by the live integration test above;
what leg 5 adds is the `DeploymentArtifact` → address-space → OPC UA materialisation on top.
**Incidental findings from the rig work:** **Incidental findings from the rig work:**
1. ~~`ClusterNode` has no `MaintenanceMode` column, so CLAUDE.md is ahead of the migration.~~ 1. ~~`ClusterNode` has no `MaintenanceMode` column, so CLAUDE.md is ahead of the migration.~~
@@ -946,10 +1030,15 @@ what leg 5 adds is the `DeploymentArtifact` → address-space → OPC UA materia
`20260722125506_AddClusterNodeMaintenanceMode` exists on master, along with `AkkaPort`/`GrpcPort`. `20260722125506_AddClusterNodeMaintenanceMode` exists on master, along with `AkkaPort`/`GrpcPort`.
**This branch is 39 commits behind master** (base `963eec1b`, master `28c28667`) and simply **This branch is 39 commits behind master** (base `963eec1b`, master `28c28667`) and simply
predates them, so the isolated stack's schema lacked the column and I reduced the seeded topology predates them, so the isolated stack's schema lacked the column and I reduced the seeded topology
by deleting the SITE-A/SITE-B rows instead. CLAUDE.md is accurate. **Rebase onto master before by deleting the SITE-A/SITE-B rows instead. CLAUDE.md is accurate. **DONE 2026-07-27 — rebased**
merging** — and once rebased, `MaintenanceMode = 1` on the site nodes is the correct, documented onto master `123ddc3f` (67 commits by then, not 39). Conflicts were all of one shape — this driver
hatch for a partial-topology live gate (it is what the SQL-poll driver's gate used), in place of and the newly-merged SQL-poll driver each adding their entry to the same list — resolved by keeping
the row deletion I resorted to. **both** in: `ZB.MOM.WW.OtOpcUa.slnx`, `DriverTypeNames.cs`, `DriverFactoryBootstrap.cs`,
`Core.Abstractions.Tests.csproj`, `TagConfigEditorMap.cs`, `TagConfigValidator.cs`,
`DriverConfigModal.razor`, `RawDriverTypeDialog.razor`. Post-rebase: solution build 0 errors,
MTConnect 491/491, Core.Abstractions 256/256, AdminUI 800/800. For a future partial-topology gate,
`MaintenanceMode = 1` is now available and is the correct hatch (it is what the SQL-poll driver's
gate used), in place of the row deletion I resorted to.
2. `sqlcmd` against this schema needs `-I` (`SET QUOTED_IDENTIFIER ON`); without it every DML 2. `sqlcmd` against this schema needs `-I` (`SET QUOTED_IDENTIFIER ON`); without it every DML
statement fails with `Msg 1934` because of the filtered/computed indexes. statement fails with `Msg 1934` because of the filtered/computed indexes.
3. **Status-code parity is pre-verified against master's new guard.** Master gained 3. **Status-code parity is pre-verified against master's new guard.** Master gained
@@ -961,7 +1050,8 @@ what leg 5 adds is the `DeploymentArtifact` → address-space → OPC UA materia
`BadNodeIdUnknown 0x80340000`, `BadNotConnected 0x808A0000`, `BadNotSupported 0x803D0000`, `BadNodeIdUnknown 0x80340000`, `BadNotConnected 0x808A0000`, `BadNotSupported 0x803D0000`,
`BadOutOfRange 0x803C0000`, `BadTypeMismatch 0x80740000`, `BadWaitingForInitialData 0x80320000` `BadOutOfRange 0x803C0000`, `BadTypeMismatch 0x80740000`, `BadWaitingForInitialData 0x80320000`
**0 mismatches**. Note the guard only sees *named* constants; an inline status literal at a call **0 mismatches**. Note the guard only sees *named* constants; an inline status literal at a call
site is invisible to it, so keep hoisting them. site is invisible to it, so keep hoisting them. **Confirmed after the rebase**: the guard reports
9 MTConnect constants in scope (the 8 above plus `Good`), all passing — the pre-verification held.
4. **The separately-tracked `BadTypeMismatch` defect in FOCAS/TwinCAT/AbLegacy/AbCip is already fixed 4. **The separately-tracked `BadTypeMismatch` defect in FOCAS/TwinCAT/AbLegacy/AbCip is already fixed
on master** (issue #497 grew it to 16 wrong constants across 6 drivers). This driver independently on master** (issue #497 grew it to 16 wrong constants across 6 drivers). This driver independently
chose the correct `0x80740000` and pinned it with a mutation test, so the two agree; no action. chose the correct `0x80740000` and pinned it with a mutation test, so the two agree; no action.