Resolve write-path silent fails + expand EnsureTagAsync, RetrievalMode coverage

DelT and EnsT2 had two distinct silent-fail blockers; both now resolved live
end-to-end. Read path's RetrievalMode mapping was missing 11 of 15 enum values
(plus a latent Cyclic→4 bug). Investigation tooling kept as env-gated helpers.

DelT silent fail: Open2 was using NativeIntegratedReadOnlyConnectionMode (0x402);
server returned err 132 OperationNotEnabled silently. Added
NativeIntegratedWriteEnabledConnectionMode (0x401) per
HistorianAccessUtil.SetConnectionMode bit map (Process=1 | IntegratedSecurity=0x400).
Write orchestrator now opens with write-enabled mode.

EnsT2 silent fail: byte-by-byte comparison via inspector revealed two bugs in
SerializeAnalogCTagMetadata. The original "146-byte byte-for-byte match" was
misaligned — it omitted the leading 0x4E marker byte and treated WCF's `01 01 01`
EndElement closing markers as if they were part of the InBuff payload. Real native
InBuff is 144 bytes with 0x4E lead and 2-byte `FE 00` trailer. Golden test bytes
corrected.

EnsureTagAsync expansion: probed every analog data type via
instrument-wcf-writemessage; byte 11 of CTagMetadata is the data-type
discriminator (Float=0x01, Double=0x21, UInt2=0x09, UInt4=0x11, Int2=0x29,
Int4=0x31). String/Int1/Int8/UInt8 fail at native AddTag — out of scope for
this op. Range encoding decoded: defaults emit compact `1A 03`; non-default
emit `1F 00` + 4 doubles in order MinEU/MaxEU/MinRaw/MaxRaw. MinRaw/MaxRaw
sent on the wire but server mirrors them to MinEU/MaxEU when ApplyScaling=false
(verified against native — server quirk, not SDK bug).

RetrievalMode mapping: probed all 15 enum values; QueryType is just the native
enum ordinal. Replaced the broken switch with `(uint)mode`. Existing SDK
mapped Cyclic→4 (BestFit's value); Cyclic is actually 0.

CLAUDE.md updated: stale "Active Protocol Blocker" rewritten as resolved-status
block; SDK surface now reflects the read-blocker resolution and the new write
ops; "Remaining gaps" punch list refreshed.

Tools added (both env-gated, no runtime overhead unless flipped on):
- HistorianWcfMessageCaptureBehavior — captures all WCF body bytes when
  AVEVA_HISTORIAN_SDK_WIRE_CAPTURE is set; used for byte-level diff vs native.
- HistorianWcfHistAddressingBehavior — explicitly sets wsa:To header on the
  Hist channel for parity with native bytes (kept though not load-bearing).
- WriteDiag in TagWriteOrchestrator — env-gated EnsT2/DelT response logging
  (AVEVA_HISTORIAN_DELT_DIAG).

NativeTraceHarness CLI: added --write-min-eu/--write-max-eu/--write-min-raw/
--write-max-raw for capturing non-default-range EnsT2 payloads.

Tests: 130 → 161 passing (+31). Includes 16-mode RetrievalMode mapping table,
4 per-data-type EnsT2 golden tests, NonDefaultRanges golden test, 6 live
round-trip integration tests covering Float/Double/Int2/Int4/UInt4/FloatRanges,
3 live tests for previously-unmapped RetrievalMode values.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-04 14:52:13 -04:00
parent 200493c990
commit 7a3cd9b76e
12 changed files with 591 additions and 79 deletions
@@ -228,6 +228,10 @@ internal static class Program
}
string writeDataTypeName = GetArg(args, "--write-data-type") ?? "Float";
double writeValue = double.TryParse(GetArg(args, "--write-value"), out double parsedValue) ? parsedValue : 42.5;
double writeMinEu = double.TryParse(GetArg(args, "--write-min-eu"), out double parsedMinEu) ? parsedMinEu : 0.0;
double writeMaxEu = double.TryParse(GetArg(args, "--write-max-eu"), out double parsedMaxEu) ? parsedMaxEu : 100.0;
double writeMinRaw = double.TryParse(GetArg(args, "--write-min-raw"), out double parsedMinRaw) ? parsedMinRaw : 0.0;
double writeMaxRaw = double.TryParse(GetArg(args, "--write-max-raw"), out double parsedMaxRaw) ? parsedMaxRaw : 100.0;
// --write-skip-add-tag lets the value-only second pass run without re-creating
// the sandbox. The connection's tag cache is bound at OpenConnection time, so the
// server-cache refresh after a fresh AddTag requires a NEW process / connection.
@@ -251,10 +255,10 @@ internal static class Program
SetProperty(tag, "EngineeringUnit", "test");
SetProperty(tag, "TagDataType", Enum.Parse(tagDataTypeEnum, writeDataTypeName, ignoreCase: true));
SetProperty(tag, "TagStorageType", Enum.Parse(tagStorageTypeEnum, "Cyclic", ignoreCase: true));
SetProperty(tag, "MinEU", 0.0);
SetProperty(tag, "MaxEU", 100.0);
SetProperty(tag, "MinRaw", 0.0);
SetProperty(tag, "MaxRaw", 100.0);
SetProperty(tag, "MinEU", writeMinEu);
SetProperty(tag, "MaxEU", writeMaxEu);
SetProperty(tag, "MinRaw", writeMinRaw);
SetProperty(tag, "MaxRaw", writeMaxRaw);
SetProperty(tag, "StorageRate", 1000u);
SetProperty(tag, "ApplyScaling", false);