Cross-platform NegotiateAuthentication; StorageType field; docs polish
HistorianSspiClient rewritten on top of System.Net.Security.NegotiateAuthentication
in place of P/Invoke into secur32.dll's InitializeSecurityContextW. The class
keeps the same Next() / Dispose() / two-constructor surface so callers don't
change. RequiredProtectionLevel=EncryptAndSign + AllowedImpersonationLevel=
Identification produces a request-flag set equivalent to the captured native
0x2081C / 0x81C bitmasks (still preserved as constants for the existing unit
tests). Removes the only Windows P/Invoke in the production SDK; the
[SupportedOSPlatform("windows")] gating elsewhere stays in place pending a
separate sweep.
HistorianStorageType (Cyclic = 1, Delta = 2):
Captured 2026-05-04 via --write-storage-type on the harness. Delta differs
from Cyclic in three places — header byte 10 (0x02 -> 0x06), flag-block
byte 1 (0x01 -> 0x02), and 4 zero bytes inserted after StorageRate before
the FILETIME. Server persists Tag.StorageType=1/2 accordingly. Plumbed
through HistorianTagDefinition.StorageType + serializer + orchestrator + 2
new tests (golden bytes + live SQL persistence verification).
Docs polish:
CLAUDE.md no longer claims "no P/Invoke" (HistorianSspiClient is the one
allowed P/Invoke surface); updated test count to 169+; AGENTS.md Required
SDK Surface and Repository Layout brought up to date with the live state
including the write surface; handoff.md "not a git working tree" obsolete
note removed.
171/171 tests pass with the NegotiateAuthentication replacement (was 169;
+2 new tests for StorageType).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -488,6 +488,50 @@ public sealed class HistorianClientIntegrationTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnsureTagAsync_StorageTypeDelta_PersistsToTagTableAsTwo()
|
||||
{
|
||||
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
||||
if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const string sandboxTag = "RetestSdkWriteStorageTypeDeltaRT";
|
||||
HistorianClient client = new(new HistorianClientOptions
|
||||
{
|
||||
Host = host,
|
||||
IntegratedSecurity = true,
|
||||
Transport = HistorianTransport.LocalPipe,
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
bool ok = await client.EnsureTagAsync(new AVEVA.Historian.Client.Models.HistorianTagDefinition
|
||||
{
|
||||
TagName = sandboxTag,
|
||||
Description = "SDK Delta round-trip",
|
||||
EngineeringUnit = "test",
|
||||
DataType = AVEVA.Historian.Client.Models.HistorianDataType.Float,
|
||||
StorageType = AVEVA.Historian.Client.Models.HistorianStorageType.Delta,
|
||||
}, CancellationToken.None);
|
||||
Assert.True(ok, "EnsureTagAsync(Delta) returned false");
|
||||
|
||||
using Microsoft.Data.SqlClient.SqlConnection sql = new("Server=.;Database=Runtime;Integrated Security=SSPI;Encrypt=False;TrustServerCertificate=True");
|
||||
sql.Open();
|
||||
using Microsoft.Data.SqlClient.SqlCommand cmd = sql.CreateCommand();
|
||||
cmd.CommandText = "SELECT StorageType FROM Tag WHERE TagName = @t";
|
||||
cmd.Parameters.AddWithValue("@t", sandboxTag);
|
||||
object? st = cmd.ExecuteScalar();
|
||||
Assert.NotNull(st);
|
||||
Assert.Equal((int)AVEVA.Historian.Client.Models.HistorianStorageType.Delta, Convert.ToInt32(st));
|
||||
}
|
||||
finally
|
||||
{
|
||||
await client.DeleteTagAsync(sandboxTag, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnsureTagAsync_NonDefaultStorageRate_PersistsToTagTable()
|
||||
{
|
||||
|
||||
@@ -153,6 +153,34 @@ public sealed class HistorianTagWriteProtocolTests
|
||||
storageRateMs: 0u));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializeAnalogCTagMetadata_StorageTypeDelta_FlipsHeaderByte10AndFlagBlockByte1AndAddsFourBytePadding()
|
||||
{
|
||||
// Captured 2026-05-04 by toggling --write-storage-type on the native harness:
|
||||
// Delta differs from Cyclic in three places — header byte 10 (0x02 -> 0x06),
|
||||
// flag-block byte 1 (0x01 -> 0x02), and 4 zero bytes inserted after StorageRate
|
||||
// before the FILETIME. Net length difference is +4 bytes for Delta.
|
||||
byte[] cyclic = HistorianTagWriteProtocol.SerializeAnalogCTagMetadata(
|
||||
tagName: "RetestSdkWriteStorageTypeRT",
|
||||
description: "x",
|
||||
engineeringUnit: "test",
|
||||
dateCreatedUtc: DateTime.FromFileTimeUtc(0x01dcdc34_5a1dff6dL),
|
||||
storageType: AVEVA.Historian.Client.Models.HistorianStorageType.Cyclic);
|
||||
byte[] delta = HistorianTagWriteProtocol.SerializeAnalogCTagMetadata(
|
||||
tagName: "RetestSdkWriteStorageTypeRT",
|
||||
description: "x",
|
||||
engineeringUnit: "test",
|
||||
dateCreatedUtc: DateTime.FromFileTimeUtc(0x01dcdc34_5a1dff6dL),
|
||||
storageType: AVEVA.Historian.Client.Models.HistorianStorageType.Delta);
|
||||
|
||||
Assert.Equal(cyclic.Length + 4, delta.Length);
|
||||
// Header byte 10 (storage-type sub-marker before the data-type code).
|
||||
Assert.Equal(0x02, cyclic[10]);
|
||||
Assert.Equal(0x06, delta[10]);
|
||||
// The data-type code at byte 11 is unchanged.
|
||||
Assert.Equal(cyclic[11], delta[11]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializeAnalogCTagMetadata_ApplyScalingTrue_FlipsTrailerSecondByte()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user