Implement EnsureTagAsync (live-verified) + DeleteTagAsync (DelT semantics partial)

New SDK surface:
  HistorianClient.EnsureTagAsync(HistorianTagDefinition)
  HistorianClient.DeleteTagAsync(string tagName)

Plumbing:
  src/AVEVA.Historian.Client/Models/HistorianTagDefinition.cs
    Public input model — TagName/Description/EngineeringUnit/DataType/MinEU/MaxEU.
    Currently only HistorianDataType.Float is live-verified.

  src/AVEVA.Historian.Client/Wcf/HistorianTagWriteProtocol.cs
    SerializeAnalogCTagMetadata produces 146-byte payload byte-for-byte
    identical to the captured native EnsT2(Float) request.
    SerializeDeleteTagNames produces ushort 0x6751 + ushort 1 + uint count
    + per-tag (uint charCount + UTF-16 chars).

  src/AVEVA.Historian.Client/Wcf/HistorianWcfTagWriteOrchestrator.cs
    Both EnsT2 and DelT run the full Stat-priming chain captured for the
    analog flow (UpdC3 + Stat.GetV ×3 + Stat.GETHI ×2 + 7× GetSystemParameter
    + Trx.GetV + Retr.GetV).

  src/AVEVA.Historian.Client/Wcf/HistorianWcfTagClient.cs
    MapDataType extended to accept tag-origin marker 0xC7 (SDK-created tags).

Tests:
  5 golden-byte tests (HistorianTagWriteProtocolTests):
    SerializeAnalogCTagMetadata byte-for-byte match against captured 146-byte fixture
    SerializeAnalogCTagMetadata produces different bytes for different inputs
    SerializeDeleteTagNames single-tag matches captured shape
    SerializeDeleteTagNames multi-tag appends each
    SerializeDeleteTagNames empty list throws

  1 live integration test (gated by HISTORIAN_WRITE_SANDBOX_TAG):
    EnsureTagAsync_AndDeleteTagAsync_RoundTrip_AgainstLocalHistorian
    EnsureTagAsync creates the sandbox tag, GetTagMetadataAsync reads it
    back. 130/130 tests pass.

Harness improvements:
  --write-delete-after now runs DelT independently of AddStreamedValue
  outcome.
  HistorianTagStatusList constructed correctly for DeleteTags reflection
  call (previous StringCollection attempt failed with TypeMismatch).

Known DelT gap: SDK's DeleteTagAsync returns true but server-side
cascading deletion does not always complete (the row remains in
Runtime.dbo.Tag). The captured native flow's DelT removes the tag
cleanly (verified via harness --write-delete-after), so something
around the WCF DelT call is missing from our orchestrator. Documented
as known issue with SMC-based cleanup as workaround.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
dohertj2
2026-05-04 08:33:21 -04:00
parent b3d22befd0
commit cfc8d44e3a
9 changed files with 664 additions and 19 deletions
@@ -141,6 +141,41 @@ Phase 2 effective deliverables:
- ⏸ Revision write path — separate capture needed against a historized
tag
## Phase 3 partial (2026-05-04) — EnsureTagAsync live, DeleteTagAsync partial
`HistorianTagWriteProtocol` + `HistorianWcfTagWriteOrchestrator` +
`HistorianClient.EnsureTagAsync`/`DeleteTagAsync` landed:
- `HistorianTagDefinition` public model (TagName/Description/EngineeringUnit/
DataType/MinEU/MaxEU; only `Float` data type currently supported live).
- `HistorianTagWriteProtocol.SerializeAnalogCTagMetadata` — produces 146-byte
payload byte-for-byte identical to the captured native EnsT2(Float) request.
- `HistorianTagWriteProtocol.SerializeDeleteTagNames` — `[ushort 0x6751,
ushort 1, uint count, per-tag (uint charCount + UTF-16 chars)]`.
- `HistorianWcfTagWriteOrchestrator` — both EnsT2 and DelT run the full
Stat-priming chain captured for the analog flow (UpdC3 + Stat.GetV ×3 +
Stat.GETHI ×2 + 7× GetSystemParameter + Trx.GetV + Retr.GetV).
- New tag-origin marker `0xC7` added to `MapDataType` (SDK-created tags have
byte 1 = 0xC7, distinct from 0xCF system / 0xC3 MDAS-routed).
Golden-byte tests (5): EnsT2(Float) byte-for-byte match against the captured
146-byte fixture; DelT(single tag) byte-for-byte; DelT(multi-tag); empty list
throws; different-inputs-produce-different-bytes.
Live integration test
(`EnsureTagAsync_AndDeleteTagAsync_RoundTrip_AgainstLocalHistorian`,
gated by `HISTORIAN_WRITE_SANDBOX_TAG=RetestSdkWriteSandbox`): EnsureTagAsync
followed by GetTagMetadataAsync confirms the sandbox tag is created in
the Runtime DB. Test passes 130/130 in the full suite.
**Known DelT gap.** SDK's DeleteTagAsync currently returns true but the
server-side cascading deletion does not always complete — the row remains
in `Runtime.dbo.Tag` even after the call returns. The captured native flow's
DelT removes the tag cleanly (verified via the harness with
`--write-delete-after`), so something the native code does between or
around the WCF DelT call is missing from our orchestrator. The harness
cleanup path remains the documented workaround for sandbox housekeeping.
## Phase 2 remaining work (revised — narrower scope)
1. Decode the 146-byte EnsT2(Float) CTagMetadata against the IL of