M3 R3.2 SHIPPED: AddHistoricalValuesAsync — historical backfill writes over gRPC (live-validated)

Public HistorianClient.AddHistoricalValuesAsync(tag, values) inserts non-streamed original
(backfill) values for an existing tag over the 2023 R2 gRPC front door. The pure-managed SDK
wrote a value and read it back live (gated test AddHistoricalValuesAsync_OverGrpc_WritesAndReadsBack
PASSED against the real server).

- HistorianGrpcHistoricalWriteOrchestrator: write-enabled (0x401) session ->
  RetrievalService.GetTagInfosFromName (resolves the per-tag GUID = the tag-info TypeId, and
  registers the tag on the session) -> HistoryService.AddStreamValues("ON" buffer) per sample.
- HistorianHistoricalValue (public record: TimestampUtc, Value, OpcQuality=192).
- gRPC-only: non-RemoteGrpc transports throw ProtocolEvidenceMissingException (the 2020 WCF
  non-streamed write is architecturally blocked, D2).
- Float value encoding only (the captured type); other types rejected by the serializer.

275 unit tests pass; the new gated live write/read-back test is green against the 2023 R2 server.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
This commit is contained in:
Joseph Doherty
2026-06-21 21:23:08 -04:00
parent 85f0c2f0fa
commit aa36e58d58
4 changed files with 183 additions and 0 deletions
@@ -173,6 +173,40 @@ public sealed class HistorianGrpcIntegrationTests
Assert.All(result.Attempts, a => Assert.False(a.Succeeded));
}
[Fact]
public async Task AddHistoricalValuesAsync_OverGrpc_WritesAndReadsBack()
{
string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST");
// Gated additionally on a dedicated sandbox-tag env var so this WRITE test never runs by
// accident — set HISTORIAN_WRITE_SANDBOX_TAG to an existing Float tag you are happy to write
// backfill samples to. M3 R3.2: HistoryService.AddStreamValues ("ON" buffer).
string? sandboxTag = Environment.GetEnvironmentVariable("HISTORIAN_WRITE_SANDBOX_TAG");
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(sandboxTag)
|| string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HISTORIAN_USER")))
{
return;
}
HistorianClient client = new(BuildOptions(host));
// A backfill sample at a fixed historical second, with a distinctive value.
DateTime stamp = new DateTime(DateTime.UtcNow.Year, 1, 2, 3, 4, 5, DateTimeKind.Utc);
const double expected = 222.5;
bool wrote = await client.AddHistoricalValuesAsync(
sandboxTag!,
[new HistorianHistoricalValue(stamp, expected)],
CancellationToken.None);
Assert.True(wrote);
// Read the window around the sample back and confirm it landed.
List<HistorianSample> samples = [];
await foreach (HistorianSample s in client.ReadRawAsync(sandboxTag!, stamp.AddMinutes(-1), stamp.AddMinutes(1), maxValues: 16, CancellationToken.None))
{
samples.Add(s);
}
Assert.Contains(samples, s => s.NumericValue is { } v && Math.Abs(v - expected) < 0.01);
}
private static HistorianClientOptions BuildOptions(string host)
{
string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER");