fix(opcua): DeleteRawModified with an unspecified bound is BadInvalidArgument; HistoryUpdate node-manager tests

- A DateTime.MinValue start or end on DeleteRawModified is refused
  BadInvalidArgument before the backend.
- Tests: anonymous without the HistoryUpdate role denied (with it allowed),
  UpdateTagAttributes enabling historization sets HistoryWrite, a throwing
  updater is BadInternalError, the MinValue bounds case; corrected the
  test helper's comment on how the SDK base seeds unadmitted slots.
- GatewayHistorianValueWriter remarks describe the recorder's real status
  capture (and that it does not persist on the gateway).

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-09 14:27:33 -04:00
parent c6d4134795
commit f8ad74dc19
3 changed files with 129 additions and 8 deletions
@@ -28,12 +28,18 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Recorder;
/// <b>Quality is a documented-dead input</b> on the <c>WriteLiveValues</c> SQL path
/// (HistorianGateway 0.2.0 Contracts C-002): <c>HistorianLiveValue.Quality</c> /
/// <c>QualityDetail</c> are server-managed columns — any value sent is accepted on the wire and
/// then silently discarded. We populate <c>Quality</c> anyway (see the mapping below): proto3
/// transmits <c>0</c> when a field is left unset, and <c>0</c> is "Bad" in OPC-DA terms, so
/// <em>not</em> setting it would send a <em>wrong</em> quality to any future gateway version that
/// starts honoring the field — whereas the recorder's <c>192</c> ("Good") is the correct value.
/// <b>Do NOT extend the recorder to capture real node quality expecting it to persist</b> — it
/// will not, until the gateway honors the field.
/// then silently discarded. We populate <c>Quality</c> anyway: proto3 transmits <c>0</c> when a
/// field is left unset, and <c>0</c> is "Bad" in OPC-DA terms, so <em>not</em> setting it would send a
/// <em>wrong</em> quality to any future gateway version that starts honoring the field. The recorder
/// now carries each sample's real OPC UA status, which <see cref="Mapping.GatewayQualityMapper.ToDaQuality"/>
/// maps to the DA byte (Good ⇒ <c>192</c>, as before). <b>That status does not persist on this
/// backend</b>: the gateway discards the field, so AVEVA still stores every sample Good — use the
/// SqlServer backend where the stored status must be the real one.
/// </para>
/// <para>
/// <b>Numeric only.</b> Values whose kind has no double form (String, DateTime, Empty) are skipped,
/// never thrown; a batch with none left is acked without calling the gateway. The recorder already drops
/// those kinds at capture for this backend, so the skip is a backstop.
/// </para>
/// </remarks>
public sealed class GatewayHistorianValueWriter : IHistorianValueWriter, IAsyncDisposable