namespace AVEVA.Historian.Client.Tests; public sealed class HistorianClientIntegrationTests { [Fact] public async Task ProbeAsync_ReturnsTrueForConfiguredHistorian() { string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); if (string.IsNullOrWhiteSpace(host)) { return; } int port = int.TryParse(Environment.GetEnvironmentVariable("HISTORIAN_PORT"), out int parsedPort) ? parsedPort : HistorianClientOptions.DefaultPort; HistorianClient client = new(new HistorianClientOptions { Host = host, Port = port }); Assert.True(await client.ProbeAsync(CancellationToken.None)); } [Fact] public async Task BrowseTagNamesAsync_ReturnsConfiguredTestTag() { string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG"); string? filter = Environment.GetEnvironmentVariable("HISTORIAN_TAG_FILTER") ?? testTag; if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag) || string.IsNullOrWhiteSpace(filter)) { return; } int port = int.TryParse(Environment.GetEnvironmentVariable("HISTORIAN_PORT"), out int parsedPort) ? parsedPort : HistorianClientOptions.DefaultPort; HistorianClient client = new(new HistorianClientOptions { Host = host, Port = port, IntegratedSecurity = true, UserName = Environment.GetEnvironmentVariable("HISTORIAN_USER") ?? string.Empty, Password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD") ?? string.Empty }); List tagNames = []; await foreach (string tagName in client.BrowseTagNamesAsync(filter, CancellationToken.None)) { tagNames.Add(tagName); } Assert.Contains(testTag, tagNames); } [Fact] public async Task ReadRawAsync_AgainstLocalHistorian_ReturnsAtLeastOneRow() { string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG"); if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag)) { return; } if (!string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase)) { // The managed read flow currently only supports the LocalPipe transport. return; } if (!OperatingSystem.IsWindows()) { return; } HistorianClient client = new(new HistorianClientOptions { Host = host, IntegratedSecurity = true, Transport = HistorianTransport.LocalPipe }); DateTime endUtc = DateTime.UtcNow; DateTime startUtc = endUtc - TimeSpan.FromDays(7); List samples = []; await foreach (AVEVA.Historian.Client.Models.HistorianSample sample in client.ReadRawAsync(testTag, startUtc, endUtc, maxValues: 8, CancellationToken.None)) { samples.Add(sample); } Assert.NotEmpty(samples); Assert.All(samples, s => Assert.Equal(testTag, s.TagName)); } [Fact] public async Task ReadAggregateAsync_AgainstLocalHistorian_ReturnsTimeWeightedAverageRows() { string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG"); if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag)) { return; } if (!string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) { return; } HistorianClient client = new(new HistorianClientOptions { Host = host, IntegratedSecurity = true, Transport = HistorianTransport.LocalPipe }); DateTime endUtc = DateTime.UtcNow; DateTime startUtc = endUtc - TimeSpan.FromMinutes(10); List samples = []; await foreach (AVEVA.Historian.Client.Models.HistorianAggregateSample sample in client.ReadAggregateAsync( testTag, startUtc, endUtc, AVEVA.Historian.Client.Models.RetrievalMode.TimeWeightedAverage, TimeSpan.FromMinutes(1), CancellationToken.None)) { samples.Add(sample); } Assert.NotEmpty(samples); Assert.All(samples, s => Assert.Equal(testTag, s.TagName)); Assert.All(samples, s => Assert.Equal(AVEVA.Historian.Client.Models.RetrievalMode.TimeWeightedAverage, s.RetrievalMode)); } // Verifies a previously-unmapped RetrievalMode (one of the 11 modes that prior to // 2026-05-04 threw ProtocolEvidenceMissingException). MinimumWithTime → QueryType=6 // exercises the "QueryType is the native enum ordinal" mapping against the live server. [Theory] [InlineData(AVEVA.Historian.Client.Models.RetrievalMode.MinimumWithTime)] [InlineData(AVEVA.Historian.Client.Models.RetrievalMode.MaximumWithTime)] [InlineData(AVEVA.Historian.Client.Models.RetrievalMode.BestFit)] public async Task ReadAggregateAsync_AgainstLocalHistorian_AcceptsPreviouslyUnmappedRetrievalMode( AVEVA.Historian.Client.Models.RetrievalMode mode) { string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG"); if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) { return; } HistorianClient client = new(new HistorianClientOptions { Host = host, IntegratedSecurity = true, Transport = HistorianTransport.LocalPipe }); DateTime endUtc = DateTime.UtcNow; DateTime startUtc = endUtc - TimeSpan.FromMinutes(10); List samples = []; await foreach (AVEVA.Historian.Client.Models.HistorianAggregateSample s in client.ReadAggregateAsync( testTag, startUtc, endUtc, mode, TimeSpan.FromMinutes(2), CancellationToken.None)) { samples.Add(s); } // Server should accept the request without error. Even if no rows come back // (unlikely for a 10-minute window on a steadily-counting tag), the absence of an // exception proves the QueryType byte was accepted. Assert.All(samples, s => Assert.Equal(mode, s.RetrievalMode)); } [Fact] public async Task ReadAtTimeAsync_AgainstLocalHistorian_ReturnsRequestedTimestamps() { string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG"); if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag)) { return; } if (!string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) { return; } HistorianClient client = new(new HistorianClientOptions { Host = host, IntegratedSecurity = true, Transport = HistorianTransport.LocalPipe }); DateTime nowUtc = DateTime.UtcNow; DateTime[] timestamps = [ nowUtc - TimeSpan.FromMinutes(5), nowUtc - TimeSpan.FromMinutes(2), nowUtc - TimeSpan.FromMinutes(1) ]; IReadOnlyList samples = await client.ReadAtTimeAsync(testTag, timestamps, CancellationToken.None); Assert.NotEmpty(samples); Assert.All(samples, s => Assert.Equal(testTag, s.TagName)); } [Fact] public async Task ReadEventsAsync_AgainstLocalHistorian_DoesNotThrow() { string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) { return; } HistorianClient client = new(new HistorianClientOptions { Host = host, IntegratedSecurity = true, Transport = HistorianTransport.LocalPipe }); DateTime endUtc = DateTime.UtcNow; DateTime startUtc = endUtc - TimeSpan.FromDays(7); // The event-row WCF wire format is not yet decoded; this test verifies the chain // (ValCl + Open2 + Retr.IsOriginalAllowed + Retr.StartEventQuery) reaches the server // without throwing. An empty event list is acceptable until row parsing is wired. List events = []; await foreach (AVEVA.Historian.Client.Models.HistorianEvent evt in client.ReadEventsAsync(startUtc, endUtc, CancellationToken.None)) { events.Add(evt); } Assert.NotNull(events); } [Fact] public async Task GetSystemParameterAsync_AgainstLocalHistorian_ReturnsHistorianVersion() { string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) { return; } HistorianClient client = new(new HistorianClientOptions { Host = host, IntegratedSecurity = true, Transport = HistorianTransport.LocalPipe }); string? value = await client.GetSystemParameterAsync("HistorianVersion", CancellationToken.None); // The server returns a non-empty version string for the documented HistorianVersion parameter. Assert.False(string.IsNullOrWhiteSpace(value)); } [Fact] public async Task GetConnectionStatusAsync_AgainstLocalHistorian_ReportsConnectedToServer() { string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) { return; } HistorianClient client = new(new HistorianClientOptions { Host = host, IntegratedSecurity = true, Transport = HistorianTransport.LocalPipe }); AVEVA.Historian.Client.Models.HistorianConnectionStatus status = await client.GetConnectionStatusAsync(CancellationToken.None); Assert.True(status.ConnectedToServer); Assert.False(status.ErrorOccurred); Assert.False(status.Pending); Assert.Equal(host, status.ServerName); } [Fact] public async Task GetStoreForwardStatusAsync_AgainstLocalHistorian_ReturnsDefaults() { string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) { return; } HistorianClient client = new(new HistorianClientOptions { Host = host, IntegratedSecurity = true, Transport = HistorianTransport.LocalPipe }); AVEVA.Historian.Client.Models.HistorianStoreForwardStatus status = await client.GetStoreForwardStatusAsync(CancellationToken.None); // The synthesized status returns defaults — no store-forward sidecar to probe in this build. Assert.False(status.ErrorOccurred); Assert.False(status.Pending); Assert.Equal(host, status.ServerName); } // The validator inside HistorianWcfTagClient now allows IntegratedSecurity=false WHEN // explicit UserName + Password are provided (NTLM/Kerberos with non-current-user creds). // It still rejects the no-credentials-at-all case since there's no way to authenticate // against /Hist-Integrated. [Fact] public async Task GetTagMetadataAsync_NoAuthAndNoCredentials_Throws() { HistorianClient client = new(new HistorianClientOptions { Host = "localhost", IntegratedSecurity = false, UserName = string.Empty, Password = string.Empty, }); await Assert.ThrowsAsync( () => client.GetTagMetadataAsync("anytag", CancellationToken.None)); } [Fact] public async Task GetTagMetadataAsync_ExplicitCredentials_AgainstLocalHistorian() { // Live verification of the explicit-creds tag-metadata path. Gated on // HISTORIAN_USER + HISTORIAN_PASSWORD being set; skips cleanly otherwise. The path // routes through WCF Windows transport security with Credentials.Windows.ClientCredential. string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG"); string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER"); string? password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD"); if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag) || string.IsNullOrWhiteSpace(user) || string.IsNullOrWhiteSpace(password) || !OperatingSystem.IsWindows()) { return; } HistorianClient client = new(new HistorianClientOptions { Host = host, IntegratedSecurity = false, UserName = user, Password = password, }); AVEVA.Historian.Client.Models.HistorianTagMetadata? metadata = await client.GetTagMetadataAsync(testTag, CancellationToken.None); Assert.NotNull(metadata); Assert.Equal(testTag, metadata.Name); } [Fact] public async Task GetTagMetadataAsync_ReturnsConfiguredTestTagMetadata() { string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG"); if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag)) { return; } int port = int.TryParse(Environment.GetEnvironmentVariable("HISTORIAN_PORT"), out int parsedPort) ? parsedPort : HistorianClientOptions.DefaultPort; HistorianClient client = new(new HistorianClientOptions { Host = host, Port = port, IntegratedSecurity = true, UserName = Environment.GetEnvironmentVariable("HISTORIAN_USER") ?? string.Empty, Password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD") ?? string.Empty }); AVEVA.Historian.Client.Models.HistorianTagMetadata? metadata = await client.GetTagMetadataAsync(testTag, CancellationToken.None); Assert.NotNull(metadata); Assert.Equal(testTag, metadata.Name); Assert.NotNull(metadata.Key); } [Fact] public async Task EnsureTagAsync_AndDeleteTagAsync_RoundTrip_AgainstLocalHistorian() { // Per docs/plans/write-commands-reverse-engineering.md safety rules: localhost only, // sandbox tag name must start with "RetestSdkWrite", tag is created if missing and // always deleted at the end so the test leaves zero residue. string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); string? sandboxTag = Environment.GetEnvironmentVariable("HISTORIAN_WRITE_SANDBOX_TAG"); if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) { return; } if (string.IsNullOrWhiteSpace(sandboxTag) || !sandboxTag.StartsWith("RetestSdkWrite", StringComparison.Ordinal)) { return; // safety gate per the plan } HistorianClient client = new(new HistorianClientOptions { Host = host, IntegratedSecurity = true, Transport = HistorianTransport.LocalPipe }); AVEVA.Historian.Client.Models.HistorianTagDefinition definition = new() { TagName = sandboxTag, Description = "SDK live integration test sandbox", EngineeringUnit = "test", DataType = AVEVA.Historian.Client.Models.HistorianDataType.Float, MinEU = 0.0, MaxEU = 100.0, }; // Both EnsureTagAsync and DeleteTagAsync now work end-to-end against the live // Historian. Open2 must use write-enabled connectionMode 0x401 (not the default // 0x402 read-only); the EnsT2 InBuff layout is corrected to native parity (144 // bytes incl 0x4E leading marker, no trailing 01 01 01 closing markers). bool ensured = await client.EnsureTagAsync(definition, CancellationToken.None); Assert.True(ensured, "EnsureTagAsync returned false against the live Historian."); bool deleted = await client.DeleteTagAsync(sandboxTag, CancellationToken.None); Assert.True(deleted, "DeleteTagAsync returned false against the live Historian."); } // Round-trip every live-verified analog data type + the non-default-range case. The // sandbox tag name is suffixed per case so the runs don't collide. Always cleans up. [Theory] [InlineData("RetestSdkWriteFloatRT", AVEVA.Historian.Client.Models.HistorianDataType.Float, 0.0, 100.0, 0.0, 100.0)] [InlineData("RetestSdkWriteDoubleRT", AVEVA.Historian.Client.Models.HistorianDataType.Double, 0.0, 100.0, 0.0, 100.0)] [InlineData("RetestSdkWriteInt2RT", AVEVA.Historian.Client.Models.HistorianDataType.Int2, 0.0, 100.0, 0.0, 100.0)] [InlineData("RetestSdkWriteInt4RT", AVEVA.Historian.Client.Models.HistorianDataType.Int4, 0.0, 100.0, 0.0, 100.0)] [InlineData("RetestSdkWriteUInt4RT", AVEVA.Historian.Client.Models.HistorianDataType.UInt4, 0.0, 100.0, 0.0, 100.0)] [InlineData("RetestSdkWriteFloatRangesRT", AVEVA.Historian.Client.Models.HistorianDataType.Float, -50.0, 200.0, 10.0, 4095.0)] public async Task EnsureTagAsync_AndDeleteTagAsync_RoundTrip_PerDataTypeAndRange( string sandboxTag, AVEVA.Historian.Client.Models.HistorianDataType dataType, double minEU, double maxEU, double minRaw, double maxRaw) { string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) { return; } HistorianClient client = new(new HistorianClientOptions { Host = host, IntegratedSecurity = true, Transport = HistorianTransport.LocalPipe }); AVEVA.Historian.Client.Models.HistorianTagDefinition definition = new() { TagName = sandboxTag, Description = $"SDK round-trip {dataType}", EngineeringUnit = "test", DataType = dataType, MinEU = minEU, MaxEU = maxEU, MinRaw = minRaw, MaxRaw = maxRaw, }; try { bool ensured = await client.EnsureTagAsync(definition, CancellationToken.None); Assert.True(ensured, $"EnsureTagAsync({dataType}) returned false against the live Historian."); } finally { // Always clean up — DeleteTagAsync returns true on a freshly-created tag. await client.DeleteTagAsync(sandboxTag, CancellationToken.None); } } [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() { string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) { return; } const string sandboxTag = "RetestSdkWriteStorageRateRT"; 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 StorageRate round-trip", EngineeringUnit = "test", DataType = AVEVA.Historian.Client.Models.HistorianDataType.Float, // Server only accepts quantized rates — 1000, 5000, 10000, 60000, 300000 ms. StorageRateMs = 5000u, }, CancellationToken.None); Assert.True(ok, "EnsureTagAsync 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 StorageRate FROM Tag WHERE TagName = @t"; cmd.Parameters.AddWithValue("@t", sandboxTag); object? rate = cmd.ExecuteScalar(); Assert.NotNull(rate); Assert.Equal(5000, Convert.ToInt32(rate)); } finally { await client.DeleteTagAsync(sandboxTag, CancellationToken.None); } } [Fact] public async Task EnsureTagAsync_CalledTwiceOnSameTag_UpdatesFieldsInPlace() { string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) { return; } const string sandboxTag = "RetestSdkWriteIdempotencyRT"; HistorianClient client = new(new HistorianClientOptions { Host = host, IntegratedSecurity = true, Transport = HistorianTransport.LocalPipe, }); try { bool firstOk = await client.EnsureTagAsync(new AVEVA.Historian.Client.Models.HistorianTagDefinition { TagName = sandboxTag, Description = "First version", EngineeringUnit = "test", DataType = AVEVA.Historian.Client.Models.HistorianDataType.Float, MinEU = 0.0, MaxEU = 100.0, MinRaw = 0.0, MaxRaw = 100.0, ApplyScaling = false, }, CancellationToken.None); Assert.True(firstOk, "First EnsureTagAsync returned false"); (string desc1, double minEU1, double maxEU1, double minRaw1, double maxRaw1, int scaling1) = ReadTagState(sandboxTag); Assert.Equal("First version", desc1); Assert.Equal(0.0, minEU1); Assert.Equal(0, scaling1); bool secondOk = await client.EnsureTagAsync(new AVEVA.Historian.Client.Models.HistorianTagDefinition { TagName = sandboxTag, Description = "Second version", EngineeringUnit = "kPa", DataType = AVEVA.Historian.Client.Models.HistorianDataType.Float, MinEU = -50.0, MaxEU = 200.0, MinRaw = 10.0, MaxRaw = 4095.0, ApplyScaling = true, }, CancellationToken.None); Assert.True(secondOk, "Second EnsureTagAsync returned false"); (string desc2, double minEU2, double maxEU2, double minRaw2, double maxRaw2, int scaling2) = ReadTagState(sandboxTag); // EnsureTagAsync upserts: second call updates the existing row in place. Assert.Equal("Second version", desc2); Assert.Equal(-50.0, minEU2); Assert.Equal(200.0, maxEU2); Assert.Equal(10.0, minRaw2); Assert.Equal(4095.0, maxRaw2); Assert.Equal(1, scaling2); } finally { await client.DeleteTagAsync(sandboxTag, CancellationToken.None); } static (string desc, double minEU, double maxEU, double minRaw, double maxRaw, int scaling) ReadTagState(string tagName) { 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 t.[Description], a.MinEU, a.MaxEU, a.MinRaw, a.MaxRaw, a.Scaling FROM Tag t JOIN AnalogTag a ON a.TagName=t.TagName WHERE t.TagName=@t"; cmd.Parameters.AddWithValue("@t", tagName); using Microsoft.Data.SqlClient.SqlDataReader r = cmd.ExecuteReader(); Assert.True(r.Read(), $"Tag {tagName} not found"); return (r.GetString(0), r.GetDouble(1), r.GetDouble(2), r.GetDouble(3), r.GetDouble(4), Convert.ToInt32(r.GetValue(5))); } } [Fact] public async Task EnsureTagAsync_ApplyScalingTrue_PersistsDistinctMinRawAndMaxRaw() { string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) { return; } const string sandboxTag = "RetestSdkWriteApplyScalingRT"; HistorianClient client = new(new HistorianClientOptions { Host = host, IntegratedSecurity = true, Transport = HistorianTransport.LocalPipe, }); AVEVA.Historian.Client.Models.HistorianTagDefinition definition = new() { TagName = sandboxTag, Description = "SDK ApplyScaling round-trip", EngineeringUnit = "test", DataType = AVEVA.Historian.Client.Models.HistorianDataType.Float, MinEU = -50.0, MaxEU = 200.0, MinRaw = 10.0, MaxRaw = 4095.0, ApplyScaling = true, }; try { bool ensured = await client.EnsureTagAsync(definition, CancellationToken.None); Assert.True(ensured, "EnsureTagAsync(ApplyScaling=true) returned false against the live Historian."); // Verify directly against the AnalogTag table — the read-path GetTagMetadataAsync // surfaces only one of (MinRaw, MinEU); SQL is the unambiguous source of truth. 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 MinEU, MaxEU, MinRaw, MaxRaw, Scaling FROM AnalogTag WHERE TagName = @t"; cmd.Parameters.AddWithValue("@t", sandboxTag); using Microsoft.Data.SqlClient.SqlDataReader r = cmd.ExecuteReader(); Assert.True(r.Read(), $"AnalogTag row for {sandboxTag} not found after EnsureTag."); Assert.Equal(-50.0, r.GetDouble(0)); Assert.Equal(200.0, r.GetDouble(1)); Assert.Equal(10.0, r.GetDouble(2)); Assert.Equal(4095.0, r.GetDouble(3)); Assert.Equal(1, Convert.ToInt32(r.GetValue(4))); } finally { await client.DeleteTagAsync(sandboxTag, CancellationToken.None); } } [Fact] public async Task GetTagMetadataAsync_PopulatesDescriptionAndEuRangeForAnalogTag() { string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) { return; } // SysTimeSec is a built-in analog UInt16 tag with non-empty Description, MaxEU, // and an EngineeringUnit. Verifies the parser populates those new fields end-to-end. const string analogTag = "SysTimeSec"; HistorianClient client = new(new HistorianClientOptions { Host = host, IntegratedSecurity = true, Transport = HistorianTransport.LocalPipe }); AVEVA.Historian.Client.Models.HistorianTagMetadata? metadata = await client.GetTagMetadataAsync(analogTag, CancellationToken.None); Assert.NotNull(metadata); Assert.Equal(analogTag, metadata.Name); Assert.False(string.IsNullOrWhiteSpace(metadata.Description)); Assert.NotNull(metadata.MaxRaw); Assert.True(metadata.MaxRaw is > 0 and <= 1e15); Assert.False(string.IsNullOrWhiteSpace(metadata.EngineeringUnit)); } }