gRPC M0: probe (R0.4, live-verified) + system-param (R0.3) + shared handshake

Roadmap docs/plans/hcal-roadmap.md, milestone M0 (gRPC parity for the DONE
surface). Now unblocked for live verification by a reachable 2023 R2 server.

- R0.4 Probe over gRPC: new HistorianGrpcProbe calls History/Retrieval/Status
  GetInterfaceVersion (unauthenticated). ProbeAsync routes over gRPC when
  Transport==RemoteGrpc. LIVE-VERIFIED against a real 2023 R2 server — needs no
  credentials (runs before the auth loop), so it works despite the auth blocker.

- R0.3 System parameter over gRPC: new HistorianGrpcStatusClient calls
  StatusService.GetSystemParameter over the authenticated session; routed in the
  dialect. Built + unit-tested (request/response field mapping pinned).
  Live-verification pending an auth fix (see below).

- Extracted the proven auth handshake from HistorianGrpcReadOrchestrator into
  shared Grpc/HistorianGrpcHandshake (reused by read + status + future
  browse/metadata). Repointed the IL structural guardrail test to it.

- Diagnostics: round-failure now decodes the native server error + hex/ASCII
  preview (HistorianNativeHandshake.DescribeError). This surfaced the live auth
  blocker as SEC_E_LOGON_DENIED (0x8009030C) at NTLM round 1 — framing is correct,
  the credential did not validate. Probable cause: stale file password or NAM-domain
  NTLM restriction (Kerberos/RDP works, NTLM denied; no SPN path over the tunnel).

216 unit tests pass; live gRPC probe passes. Sanitization scan clean.

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 13:32:04 -04:00
parent 22e9c5e5f8
commit c4b8d0dde4
11 changed files with 293 additions and 56 deletions
@@ -100,6 +100,41 @@ public sealed class HistorianGrpcTransportTests
Assert.Equal((uint)HistorianDataQueryProtocol.QueryRequestTypeData, decoded.UiQueryRequestType);
}
[Fact]
public void InterfaceVersionResponses_ExposeErrorAndVersion_AsProbeExpects()
{
// R0.4 ProbeAsync reads uiError/uiVersion off each service's GetInterfaceVersion response.
// Pin that field mapping (success = uiError 0 + uiVersion > 0) via a protobuf round-trip.
var history = GrpcHistory.GetInterfaceVersionResponse.Parser.ParseFrom(
new GrpcHistory.GetInterfaceVersionResponse { UiError = 0, UiVersion = 12 }.ToByteArray());
var retrieval = GetRetrievalInterfaceVersionResponse.Parser.ParseFrom(
new GetRetrievalInterfaceVersionResponse { UiError = 0, UiVersion = 4 }.ToByteArray());
Assert.Equal(0u, history.UiError);
Assert.Equal(12u, history.UiVersion);
Assert.Equal(0u, retrieval.UiError);
Assert.Equal(4u, retrieval.UiVersion);
}
[Fact]
public void GetSystemParameterMessages_CarryHandleNameAndValue_AsStatusClientExpects()
{
// R0.3 sends {uiHandle, strParameterName} and reads strParameterValue when status succeeds.
var request = ArchestrA.Grpc.Contract.Status.GetSystemParameterRequest.Parser.ParseFrom(
new ArchestrA.Grpc.Contract.Status.GetSystemParameterRequest { UiHandle = 9, StrParameterName = "HistorianVersion" }.ToByteArray());
Assert.Equal(9u, request.UiHandle);
Assert.Equal("HistorianVersion", request.StrParameterName);
var response = ArchestrA.Grpc.Contract.Status.GetSystemParameterResponse.Parser.ParseFrom(
new ArchestrA.Grpc.Contract.Status.GetSystemParameterResponse
{
Status = new ArchestrA.Grpc.Contract.RequestStatus.Status { BSuccess = true },
StrParameterValue = "20.0.000"
}.ToByteArray());
Assert.True(response.Status.BSuccess);
Assert.Equal("20.0.000", response.StrParameterValue);
}
[Fact]
public void OpenConnectionRequest_CarriesNativeOpen2BufferUnchanged()
{