Speculative-items sweep: IntegralDivisor, cert tests, D3/D1/D2 findings

Plan: docs/plans/speculative-items-sweep.md (also covers parallelism +
findings).

Implemented:
- C3: HistorianTagDefinition.IntegralDivisor (default 1.0). Wire bytes
  flip per the captured native serializer; live probe shows the server
  stores IntegralDivisor on EngineeringUnit (shared) rather than per-tag,
  so the value is accepted on the wire but doesn't visibly persist for
  the test EU. Documented in the property's doc-comment.
- E: HistorianWcfCertOptionTests (5 tests) covering AllowUntrustedServer-
  Certificate validator installation + ServerDnsIdentity propagation
  through CreateEndpointAddress and CreateBindingPair.

Investigated + documented (deferred):
- D3: Discrete/String/Int1/Int8/UInt8 EnsT2 root cause — server-side
  ValidationFailed: "Transaction validation failed". Native AddTag's
  validator rejects non-analog types; not a wire-format issue. To unlock,
  need to capture a working native flow via a different code path
  (likely SMC's tag-import path or AddTagExtendedProperties carrying
  type-specific metadata). Defer until a customer asks.
- D1: AddTagExtendedProperties feasibility — managed surface confirmed
  (ArchestrA.HistorianAccess.AddTagExtendedProperties + WCF op
  AddTagExtendedPropertyGroups). Cost estimated at 1-2 days of focused
  RE work due to CTagExtendedPropertyGroup payload complexity. Defer.
- D2: AddRevisionValuesBegin/Value/End — sub-plan written at
  docs/plans/revision-write-path.md with 5-step capture sequence,
  workstream estimates, and risk register. Implementation deferred.

177/177 tests pass (was 172; +5 cert tests + 1 IntegralDivisor unit
test, harness probe results not committed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-05 00:11:40 -04:00
parent 549995e4a9
commit f4709ff143
7 changed files with 420 additions and 3 deletions
@@ -181,6 +181,33 @@ public sealed class HistorianTagWriteProtocolTests
Assert.Equal(cyclic[11], delta[11]);
}
[Fact]
public void SerializeAnalogCTagMetadata_NonDefaultIntegralDivisor_FlipsEightBytesBeforeTrailer()
{
byte[] @default = HistorianTagWriteProtocol.SerializeAnalogCTagMetadata(
tagName: "RetestSdkWriteIntDiv",
description: "x",
engineeringUnit: "test",
dateCreatedUtc: DateTime.FromFileTimeUtc(0x01dcdc34_5a1dff6dL));
byte[] custom = HistorianTagWriteProtocol.SerializeAnalogCTagMetadata(
tagName: "RetestSdkWriteIntDiv",
description: "x",
engineeringUnit: "test",
dateCreatedUtc: DateTime.FromFileTimeUtc(0x01dcdc34_5a1dff6dL),
integralDivisor: 2.5);
Assert.Equal(@default.Length, custom.Length);
// The 8 bytes immediately before the 2-byte trailer are the IntegralDivisor double.
ReadOnlySpan<byte> defaultDivisor = @default.AsSpan(@default.Length - 10, 8);
ReadOnlySpan<byte> customDivisor = custom.AsSpan(custom.Length - 10, 8);
Assert.Equal(1.0, BitConverter.ToDouble(defaultDivisor));
Assert.Equal(2.5, BitConverter.ToDouble(customDivisor));
// Bytes preceding the divisor are identical.
Assert.Equal(
Convert.ToHexString(@default.AsSpan(0, @default.Length - 10)),
Convert.ToHexString(custom.AsSpan(0, custom.Length - 10)));
}
[Fact]
public void SerializeAnalogCTagMetadata_ApplyScalingTrue_FlipsTrailerSecondByte()
{
@@ -0,0 +1,97 @@
using System.IdentityModel.Selectors;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Security;
using AVEVA.Historian.Client;
using AVEVA.Historian.Client.Wcf;
using AVEVA.Historian.Client.Wcf.Contracts;
namespace AVEVA.Historian.Client.Tests;
public sealed class HistorianWcfCertOptionTests
{
private static HistorianClientOptions BaseOptions(bool allowUntrusted = false, string? dnsIdentity = null) =>
new()
{
Host = "10.0.0.1",
Port = HistorianClientOptions.DefaultPort,
Transport = HistorianTransport.RemoteTcpCertificate,
IntegratedSecurity = false,
UserName = "user",
Password = "pass",
AllowUntrustedServerCertificate = allowUntrusted,
ServerDnsIdentity = dnsIdentity,
};
[Fact]
public void ClientCredentialsHelper_Disabled_LeavesValidationModeAtDefault()
{
Binding binding = HistorianWcfBindingFactory.CreateMdasNetTcpBinding(TimeSpan.FromSeconds(5));
ChannelFactory<IHistoryServiceContract2> factory = new(binding, new EndpointAddress("net.tcp://10.0.0.1:32568/Hist"));
try
{
HistorianWcfClientCredentialsHelper.Configure(factory, BaseOptions(allowUntrusted: false));
X509ServiceCertificateAuthentication auth = factory.Credentials.ServiceCertificate.SslCertificateAuthentication
?? factory.Credentials.ServiceCertificate.Authentication;
// Default validation mode is ChainTrust — explicitly NOT None / Custom.
Assert.NotEqual(X509CertificateValidationMode.None, auth.CertificateValidationMode);
Assert.Null(auth.CustomCertificateValidator);
}
finally
{
factory.Abort();
}
}
[Fact]
public void ClientCredentialsHelper_Enabled_InstallsAcceptAnyValidator()
{
Binding binding = HistorianWcfBindingFactory.CreateMdasNetTcpBinding(TimeSpan.FromSeconds(5));
ChannelFactory<IHistoryServiceContract2> factory = new(binding, new EndpointAddress("net.tcp://10.0.0.1:32568/Hist"));
try
{
HistorianWcfClientCredentialsHelper.Configure(factory, BaseOptions(allowUntrusted: true));
X509ServiceCertificateAuthentication auth = factory.Credentials.ServiceCertificate.SslCertificateAuthentication;
Assert.NotNull(auth);
Assert.Equal(X509CertificateValidationMode.Custom, auth.CertificateValidationMode);
Assert.Equal(X509RevocationMode.NoCheck, auth.RevocationMode);
Assert.NotNull(auth.CustomCertificateValidator);
Assert.IsAssignableFrom<X509CertificateValidator>(auth.CustomCertificateValidator);
}
finally
{
factory.Abort();
}
}
[Fact]
public void CreateEndpointAddress_WithoutDnsIdentity_HasNullIdentity()
{
EndpointAddress address = HistorianWcfBindingFactory.CreateEndpointAddress("10.0.0.1", 32568, "Hist");
Assert.Null(address.Identity);
}
[Fact]
public void CreateEndpointAddress_WithDnsIdentity_AttachesDnsEndpointIdentity()
{
EndpointAddress address = HistorianWcfBindingFactory.CreateEndpointAddress("10.0.0.1", 32568, "HistCert", "localhost");
Assert.NotNull(address.Identity);
DnsEndpointIdentity dns = Assert.IsType<DnsEndpointIdentity>(address.Identity);
Assert.Equal("localhost", dns.IdentityClaim.Resource);
}
[Fact]
public void CreateBindingPair_RemoteTcpCertificate_PropagatesServerDnsIdentity()
{
HistorianClientOptions options = BaseOptions(dnsIdentity: "localhost");
var (_, historyEndpoint, _, retrievalEndpoint) = HistorianWcfBindingFactory.CreateBindingPair(options);
DnsEndpointIdentity historyIdentity = Assert.IsType<DnsEndpointIdentity>(historyEndpoint.Identity);
Assert.Equal("localhost", historyIdentity.IdentityClaim.Resource);
// The Retrieval endpoint uses plain MdasNetTcp without TLS — no DNS identity needed.
Assert.Null(retrievalEndpoint.Identity);
}
}