test(grpc): live + gated coverage for the gRPC config ops

- GetRuntimeParameterAsync_OverGrpc_ReturnsValue (live)
- GetTagExtendedPropertiesAsync_OverGrpc_DoesNotThrow (live; empty for system tags)
- ExecuteSqlCommandAsync_OverGrpc_IsServerWalled (live; pins the captured wall)
- TagWriteLifecycle_OverGrpc_CreatesAddsPropRenamesDeletes — DESTRUCTIVE, gated on
  HISTORIAN_GRPC_WRITE_SANDBOX_TAG; self-cleaning create->addprop->verify->rename->delete

Full gRPC live suite 19/19 green against a real 2023 R2 server (write lifecycle
skips without the sandbox tag); 317 offline green.

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-22 01:26:33 -04:00
parent ef68016c7a
commit 0780cec9a7
@@ -351,6 +351,104 @@ public sealed class HistorianGrpcIntegrationTests
Assert.All(samples, s => Assert.Equal(testTag, s.TagName)); Assert.All(samples, s => Assert.Equal(testTag, s.TagName));
} }
[Fact]
public async Task GetRuntimeParameterAsync_OverGrpc_ReturnsValue()
{
string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST");
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HISTORIAN_USER")))
{
return;
}
// Config op tooled over gRPC: StatusService.GetRuntimeParameter carries the proven 2020 GETRP
// request/response buffers unchanged inside the protobuf bytes fields.
HistorianClient client = new(BuildOptions(host));
string? value = await client.GetRuntimeParameterAsync("HistorianVersion", CancellationToken.None);
Assert.False(string.IsNullOrWhiteSpace(value));
}
[Fact]
public async Task GetTagExtendedPropertiesAsync_OverGrpc_DoesNotThrow()
{
string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST");
string? tag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG");
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(tag)
|| string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HISTORIAN_USER")))
{
return;
}
// Config op tooled over gRPC: RetrievalService.GetTagExtendedPropertiesFromName carries the
// proven 2020 GetTepByNm buffers. A system tag may have no user-defined properties, so this
// asserts the call completes and returns a well-formed (possibly empty) list.
HistorianClient client = new(BuildOptions(host));
IReadOnlyList<HistorianTagExtendedProperty> props = await client.GetTagExtendedPropertiesAsync(tag!, CancellationToken.None);
Assert.NotNull(props);
}
[Fact]
public async Task ExecuteSqlCommandAsync_OverGrpc_IsServerWalled()
{
string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST");
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HISTORIAN_USER")))
{
return;
}
// ExecuteSqlCommand request rides the gRPC front door, but the server-side
// CSrvDbConnection.ExecuteSqlCommand faults (IndexOutOfRange / native error 38) — an unmet
// DB-connection precondition the pure managed gRPC session doesn't establish (captured
// 2026-06-22). The SDK surfaces this as ProtocolEvidenceMissingException. This test pins the
// wall so a future server/registration change that lifts it is noticed.
HistorianClient client = new(BuildOptions(host));
await Assert.ThrowsAsync<ProtocolEvidenceMissingException>(() => client.ExecuteSqlCommandAsync(
"SELECT 10 AS Num, 'alpha' AS Word UNION ALL SELECT 20, NULL",
cancellationToken: CancellationToken.None));
}
[Fact]
public async Task TagWriteLifecycle_OverGrpc_CreatesAddsPropRenamesDeletes()
{
string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST");
// DESTRUCTIVE: gated on a dedicated sandbox-tag name so it never mutates a server by accident.
// Set HISTORIAN_GRPC_WRITE_SANDBOX_TAG to a throwaway tag name the test may create/rename/delete.
string? sandbox = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_WRITE_SANDBOX_TAG");
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(sandbox)
|| string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HISTORIAN_USER")))
{
return;
}
// Exercises the full gRPC tag-config write surface end-to-end against a write-enabled (0x401)
// session, then cleans up after itself: EnsureTags -> AddTagExtendedProperties ->
// (read-back verify) -> StartJob rename -> DeleteTags.
HistorianClient client = new(BuildOptions(host));
string renamed = sandbox + "_R";
try
{
bool created = await client.EnsureTagAsync(
new HistorianTagDefinition { TagName = sandbox!, DataType = HistorianDataType.Float, EngineeringUnit = "u", MaxEU = 100 },
CancellationToken.None);
Assert.True(created, "EnsureTags over gRPC should create the sandbox tag.");
bool propAdded = await client.AddTagExtendedPropertyAsync(sandbox!, "GrpcToolingTest", "ok", CancellationToken.None);
Assert.True(propAdded, "AddTagExtendedProperties over gRPC should succeed.");
IReadOnlyList<HistorianTagExtendedProperty> props = await client.GetTagExtendedPropertiesAsync(sandbox!, CancellationToken.None);
Assert.Contains(props, p => string.Equals(p.Name, "GrpcToolingTest", StringComparison.OrdinalIgnoreCase));
HistorianTagRenameResult rename = await client.RenameTagsAsync([(sandbox!, renamed)], CancellationToken.None);
Assert.True(rename.Accepted, $"StartJob rename over gRPC should be accepted: {rename.Error}");
}
finally
{
// Best-effort cleanup of whichever name survives (rename is an async server job).
try { await client.DeleteTagAsync(sandbox!, CancellationToken.None); } catch { /* ignore */ }
try { await client.DeleteTagAsync(renamed, CancellationToken.None); } catch { /* ignore */ }
}
}
private static HistorianClientOptions BuildOptions(string host) private static HistorianClientOptions BuildOptions(string host)
{ {
string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER"); string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER");