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
@@ -366,25 +366,34 @@ internal static class Program
ErrorDescription = GetPropertyText(addValueError, "ErrorDescription"),
});
// Optionally delete the tag for clean rollback.
if (HasFlag(args, "--write-delete-after"))
}
// DeleteTags runs independently of AddStreamedValue success (write-RE
// sandbox cleanup); guarded by --write-delete-after to keep the default
// run non-destructive.
if (HasFlag(args, "--write-delete-after"))
{
Type tagStatusType = GetType(assembly, "ArchestrA.HistorianTagStatus");
Type tagStatusListType = GetType(assembly, "ArchestrA.HistorianTagStatusList");
object tagsToDelete = Activator.CreateInstance(tagStatusListType)!;
object tagStatus = Activator.CreateInstance(tagStatusType)!;
SetProperty(tagStatus, "TagName", sandboxTag);
MethodInfo addItem = tagStatusListType.GetMethod("Add", new[] { tagStatusType })
?? throw new MissingMethodException("HistorianTagStatusList.Add");
addItem.Invoke(tagsToDelete, [tagStatus]);
object deleteError = Activator.CreateInstance(errorType)!;
MethodInfo deleteMethod = accessType.GetMethods().First(m =>
m.Name == "DeleteTags" && m.GetParameters().Length == 2);
object?[] deleteArgs = [tagsToDelete, deleteError];
bool deleteSuccess = (bool)deleteMethod.Invoke(access, deleteArgs)!;
deleteError = deleteArgs[1]!;
rows.Add(new
{
object deleteError = Activator.CreateInstance(errorType)!;
MethodInfo deleteMethod = accessType.GetMethods().FirstOrDefault(m =>
m.Name == "DeleteTags" && m.GetParameters().Length == 2)
?? throw new MissingMethodException("HistorianAccess.DeleteTags");
StringCollection tagsToDelete = [];
tagsToDelete.Add(sandboxTag);
object?[] deleteArgs = [tagsToDelete, deleteError];
bool deleteSuccess = (bool)deleteMethod.Invoke(access, deleteArgs)!;
deleteError = deleteArgs[1]!;
rows.Add(new
{
Kind = "DeleteTags",
Success = deleteSuccess,
ErrorDescription = GetPropertyText(deleteError, "ErrorDescription"),
});
}
Kind = "DeleteTags",
Success = deleteSuccess,
ErrorDescription = GetPropertyText(deleteError, "ErrorDescription"),
});
}
}
else if (openSuccess && status.ConnectedToServer && IsTagScenario(scenario))