Every deliverable was already on master with the plan's verbatim commit messages (T1 24796f2c, T2 4fca4e1a, T3 1b7f995a, T4 8ce57e47, T5 ec9599e2, T6 5f48f81d). Re-verified: DeploymentArtifactTests 29/29 + MultiClusterScopingTests 1/1 green; central-1 live-serves its slice on the running hub-and-spoke rig. No code written.
Two bugs caught by live verification against the mxaccessgw at 10.100.0.48:5120:
- MaxAttempts=1 produced an invalid Polly RetryStrategyOptions -> the probe failed
on every real gateway. Removed the Retry override (matches GalaxyDriver); fail-fast
is already guaranteed by the TCP preflight + the per-call deadline.
- A rejected key surfaces as a typed MxGatewayAuthenticationException, not a raw
RpcException, so 'auth-rejection = reachable' was bypassed. Catch the typed auth/
authorization exceptions -> Ok=true.
Adds DriverProbeHandshakeE2eTests: direct-probe, skip-gated cross-protocol green/red
discrimination (Modbus, OpcUaClient, Galaxy + a local real OPC UA server).
Create docs/drivers/TestConnectProbes.md: full reference for the Phase 5
protocol-handshake probes — result contract, per-driver handshake table,
TwinCAT/FOCAS/Galaxy degrade semantics, live-verify scope, and the
Historian.Wonderware already-done note. Annotate the Phase 7 step in
docs/plans/2026-05-28-adminui-driver-pages-design.md with a shipped note
pointing at the phase-5 design doc and TestConnectProbes.md.
The plan + task list for the write-outcome self-correction work (B1, already
shipped via master 1d797c1c). Its design-doc counterpart is already committed;
this adds the matching plan artifacts, consistent with the other docs/plans/.
C1 (critical): a boundary tie cluster larger than NumValuesPerNode could
silently truncate a resumed read to GoodNoData, permanently dropping the
un-emitted ties — the (timestamp, skip) cursor cannot advance past a single
timestamp the fixed-(start,end,cap) backend keeps re-returning. Now detected
and failed LOUDLY per node with BadHistoryOperationUnsupported + a log naming
the tag/timestamp/cap; documented in Historian.md with the larger-cap remedy.
Regression test Raw_tie_cluster_larger_than_page_fails_loudly_not_silently.
I3: build HistoryData before Save() so a projection failure can never orphan a
stored continuation cursor.
N1 (YAGNI): drop the never-produced HistoryReadKind enum + Processed-only
Aggregate/IntervalTicks fields from HistoryContinuationState — only Raw pages.
N3: ComputeResumeCursor guards its documented non-empty precondition.
I1: document InMemoryHistoryContinuationStore's eventual-consistency (test double).
Build clean, 182/182 OpcUaServer tests pass.
The Wonderware historian backend is single-shot — it returns up to
NumValuesPerNode samples with a null continuation point — so paging is
synthesised server-side, time-based, for the only count-capped arm (Raw):
- A full page (count == NumValuesPerNode, NumValuesPerNode > 0) emits an
opaque 16-byte continuation point and stores a resume cursor; a short page
(or NumValuesPerNode == 0 "all values") emits none.
- A resume read takes the stored cursor, reads the next page from the boundary
forward, and emits a fresh CP only if that page is also full.
- The resume cursor is tie-safe (HistoryPaging.ComputeResumeCursor /
TrimBoundaryDuplicates): the next page resumes from the boundary timestamp
INCLUSIVE and drops the head ties already returned, so samples sharing the
boundary SourceTimestamp are neither duplicated nor skipped.
Continuation points are bound to the OPC UA session via the SDK's
ISession.SaveHistoryContinuationPoint / RestoreHistoryContinuationPoint store
(SessionHistoryContinuationStore) — capped by ServerConfiguration.
MaxHistoryContinuationPoints (default 100, oldest-evicted) and disposed on
session close. releaseContinuationPoints is honoured via an override of
HistoryReleaseContinuationPoints (the base dispatcher routes release-only reads
there, never to the per-details arms). An unknown / evicted / released point
resumes to BadContinuationPointInvalid.
Processed and AtTime stay single-shot: neither details type carries a client
count cap, so the single-shot backend returns the complete result in one read
and there is no "full page" signal to page on (spec-conformant). Modified-value
history remains out of scope.
The pure paging decisions + CP store contract are unit-tested via HistoryPaging
+ InMemoryHistoryContinuationStore; the full multi-page round trip is driven
end-to-end through the node manager with an in-memory store + a series-backed
fake historian (the in-process harness is session-less).
The gate reads the literal role string OpcUaDataPlaneRoles.AlarmAck = "AlarmAck"
(OtOpcUaNodeManager.cs:643), but the Role-grant-source section told operators to map
their alarm-ack group to "AlarmAcknowledge" (the PermissionFlags ACL bit, a different
vocabulary) — which silently never satisfies the ack gate. Fix the three role-string
occurrences + add a code-true note; generalize the scripted-alarm note to native alarms.
Add AllDeadLetters probe to Native_alarm_during_reconnect_is_dropped_not_forwarded so the
test genuinely guards the Reconnecting state's Receive<NativeAlarmRaised> drop handler —
removing that handler would now cause a dead-letter and fail the assertion (false-negative
gap closed). Reword the ScriptedAlarms.md severity-mapping note: "snaps on the first
transition" → "every transition maps … overriding the authored seed from the first
transition onward", clarifying that MapSeverity runs on every event, not just the first.