merge: MTConnect review findings resolved (Driver.MTConnect-004 write-back authoring, -005/-006/-007)

# Conflicts:
#	code-reviews/Driver.MTConnect/findings.md
This commit is contained in:
Joseph Doherty
2026-08-10 08:11:03 -04:00
11 changed files with 538 additions and 55 deletions
+66 -10
View File
@@ -7,7 +7,7 @@
| Review date | 2026-08-10 |
| Commit reviewed | `b990adae` |
| Status | Complete |
| Open findings | 4 |
| Open findings | 0 |
## Checklist coverage
@@ -168,7 +168,7 @@ defect left inside these two modules are corrected under `Driver.MTConnect-008`
| Severity | Medium |
| Category | Design-document adherence |
| Location | `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Forms/MTConnectDriverForm.razor:1` · `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectDriverOptions.cs:146` |
| Status | Open |
| Status | Resolved |
**Description:** `IWritable` shipped for this driver on 2026-08-05 (`b29623cf`) behind
`Write.Enabled`, and `docs/drivers/MTConnect.md` §"Two independent gates" tells the operator to set
@@ -192,8 +192,32 @@ edit through the form. The capability is unreachable rather than fragile.
comment. Until then the driver-side gate is correct and fails closed, which is why this is Medium
rather than High.
**Resolution:** _(open — the fix belongs in `ZB.MOM.WW.OtOpcUa.AdminUI`, outside this review's
modules.)_
**Resolution (2026-08-10):** Fixed `298ad04f`. `MTConnectDriverForm` gained a **Write-back**
section — a switch plus form text stating that a write is observation injection, that the Agent's own
`AllowPut` is the independent second gate, and that arming it does not by itself make any node
writable over OPC UA (`Tag.AccessLevel` still composes `CurrentWrite`; CONDITION items never are).
`Write` was added to the form's mirrored `MTConnectConfigDto` and to `MTConnectFormModel`, the false
"MTConnect is read-only" header comment is gone, and `docs/drivers/MTConnect.md` §"Two independent
gates" now names the section instead of implying a config-DB edit.
⚠️ **The block is written in BOTH states, unlike the timing knobs.** Those are omitted when unusable
so `ParseOptions` substitutes the driver's own default; an omitted `write` block would instead leave a
previously-authored `"enabled": true` standing — the switch would appear to turn writing off while the
deployed driver went on accepting writes. Pinned by
`Turning_write_back_off_clears_a_previously_authored_true`, the falsification control for that
decision. The finding's own mitigation (unknown top-level keys survive an edit) is likewise not
sufficient on its own: `FromJson` had to LEARN the block, or the modal would show the switch off over
a config that has writing on — `Write_back_round_trips_and_a_hand_authored_block_loads_into_the_form`.
This project has no bUnit, so the seam is unit-tested from both ends rather than through the page:
`MTConnectDriverFormModelTests` asserts the exact bytes `BuildConfigJson` emits, and
`MTConnectFactoryTests.The_document_the_AdminUI_form_emits_arms_write_back` parses that same document
through `MTConnectDriver.ParseOptions` and asserts `MTConnectDriverOptions.Write.Enabled` is true
(then false for the switched-off variant). The literal is duplicated because a driver test project
cannot reference the AdminUI; `The_write_enabled_document_is_the_literal_the_driver_test_parses` pins
the two copies together so a rename fails on the authoring side, naming the literal to update.
**Residual: live page verification is still a rig step** — the razor markup itself (the switch's
binding and `EmitAsync` wiring) is not covered by any test here.
### Driver.MTConnect-005
@@ -202,7 +226,7 @@ modules.)_
| Severity | Low |
| Category | Concurrency & thread safety |
| Location | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs:2655` · `:689` · `:797` · `:1425` |
| Status | Open |
| Status | Resolved |
**Description:** `_options` is the one piece of shared driver state published by a **plain** field
write (`_options = options;` at the `StartCoreAsync` commit point) and read lock-free from other
@@ -223,7 +247,17 @@ three siblings, or fold it into the `AgentSession` / `TagBinding` snapshot so th
consistent unit" rule this file already argues for covers it too. Record-only for now: the change
touches ~10 read sites and the exposure does not justify unreviewed churn.
**Resolution:** _(open)_
**Resolution (2026-08-10):** Fixed `2d7dffcc`. Both writes are `Volatile.Write` (the constructor's as
well as the commit point's) and all thirteen reads are `Volatile.Read`; the field gained the remark
its three siblings already carried. Two sites took a local snapshot rather than repeating the read —
the `_streamUnsupported` refusal, which named the Agent twice, and `GetOrFetchProbeModelAsync`.
**A source scan is the pin, deliberately.** The defect is a missing fence, and no test can force the
interleaving that exposes it on x86/arm64 in bounded time; what actually regresses is a NEW read site
written without the fence. `MTConnectOptionsPublicationTests` therefore fails on any unguarded touch
of `_options` in `MTConnectDriver.cs` — the same shape as `LibplctagGatewayConsumptionGuardTests`, and
red before the change with all 15 sites listed by line. It carries a floor count so the scan cannot go
blind if the field is renamed.
### Driver.MTConnect-006
@@ -232,7 +266,7 @@ touches ~10 read sites and the exposure does not justify unreviewed churn.
| Severity | Low |
| Category | Correctness & logic bugs |
| Location | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs:914` |
| Status | Open |
| Status | Resolved |
**Description:** `TryFormatValue` refuses `null` and `Array`, then renders everything else with
`Convert.ToString(value, InvariantCulture)`. Its own remark gives the right reason for the array
@@ -250,7 +284,20 @@ MTConnect tag is a scalar Boolean/numeric/String — hence Low.
that happens to have it: `if (value is null or Array or not IConvertible) return false;`. `string`,
`bool`, every numeric and `DateTime` are `IConvertible`, so the accepted set is unchanged.
**Resolution:** _(open)_
**Resolution (2026-08-10):** Fixed `2d7dffcc` exactly as recommended (`Array` kept in the pattern as
documentation of the shape this was first noticed on, though it is subsumed). Red first:
`A_non_convertible_composite_value_is_refused_as_a_type_mismatch` wrote a `List<double>` and got
`Good` back, having injected the list's own type name into the Agent as the observation value.
The falsification control is the load-bearing half — `An_IConvertible_scalar_still_writes` plus
`A_DateTime_still_writes_as_its_invariant_string` fail any over-correction expressed as "is this a
primitive I recognise", which would start refusing real writes.
⚠️ **Worth knowing: the fix stranded the vehicle of an existing test.**
`Cancellation_mid_batch_leaves_the_slots_already_written_with_their_real_outcomes` injected its
cancellation from a value's own `ToString`, and such a value is not `IConvertible` — so it was refused
before it was ever rendered and the cancellation never happened. The injection point moved to the
Agent call (`CannedAgentClient.AfterWrite`), which is the seam that test is asserting around anyway;
its determinism property is unchanged.
### Driver.MTConnect-007
@@ -259,7 +306,7 @@ that happens to have it: `if (value is null or Array or not IConvertible) return
| Severity | Low |
| Category | Performance & resource management |
| Location | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectAgentClient.cs:85` |
| Status | Open |
| Status | Resolved |
**Description:** `MTConnectAgentClient.Dispose()` is a plain check-then-set on a non-volatile
`_disposed` field, so two concurrent disposals can both pass the guard. The second then calls
@@ -272,7 +319,16 @@ Low rather than Medium.
**Recommendation:** Guard with `Interlocked.Exchange(ref _disposed, 1) != 0` over an `int` field, or
state on `Dispose` that it is not thread-safe and is called only under the driver's lifecycle lock.
**Resolution:** _(open)_
**Resolution (2026-08-10):** Fixed `2d7dffcc` — `_disposed` is an `int` claimed with
`Interlocked.Exchange`, the four request seams read it with `Volatile.Read`, and `Dispose` now states
that it is idempotent under a race. Pinned by `Concurrent_disposals_are_idempotent`.
**Its budget is part of the fix.** The window is a handful of instructions and the loser only throws
if it reaches `Cancel()` after the winner has finished disposing two `HttpClient`s, so a small
race proves nothing: four racers over 200 rounds passed against the DEFECTIVE code. Eight racers on a
barrier over 3000 rounds failed at round 690, which is the red this resolution rests on. Once the
claim is atomic no schedule can fail it, so the test cannot flake in either direction — but do not
shrink the budget.
### Driver.MTConnect-008