R0.1 browse probe: StartTagQuery over gRPC takes an OData filter (live)

Probes the 2023 R2 gRPC browse path and records the finding. The front door does
NOT hit the 2020 WCF metadata-server-pipe wall.

- RetrievalService.StartTagQuery is cracked: the server (CMdServer::StartActiveTagnamesQuery
  over \.\pipe\aahMetadataServer\console) parses the filter as OData. startswith()/
  contains()/eq/empty succeed and return the 8-byte (queryHandle, tagCount); SQL-LIKE "%"
  and glob "*" fail with "ODataFilter: bad token". Live: 220 Sys* tags counted.
- QueryTag (paging) remains: every guessed btRequest returns a constant native error
  type 4 / code 72 (content-independent) -> framing needs a native capture, not guessing.

Adds RE probe helpers Grpc/HistorianGrpcTagClient.ProbeStartTagQuery + ProbeTagQuerySequence,
a gated StartTagQuery_OverGrpc_AcceptsODataFilter test, and the finding doc
docs/reverse-engineering/grpc-tag-query-odata.md. Browse is not yet wired (QueryTag open).

217 unit tests pass; 5/5 live gRPC tests pass. No tag names/identities committed.

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 14:58:12 -04:00
parent 0e19adae68
commit 26ef5e5645
4 changed files with 165 additions and 0 deletions
@@ -86,6 +86,30 @@ public sealed class HistorianGrpcIntegrationTests
Assert.True(Enum.IsDefined(metadata.DataType));
}
[Fact]
public async Task StartTagQuery_OverGrpc_AcceptsODataFilter()
{
string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST");
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HISTORIAN_USER")))
{
return;
}
// R0.1 finding (2026-06-21): on the 2023 R2 gRPC front door the metadata-server pipe IS
// reachable (unlike 2020 WCF) and StartActiveTagnamesQuery parses the filter as OData —
// startswith/contains/eq succeed; SQL-LIKE "%"/glob "*" fail with "ODataFilter: bad token".
// StartTagQuery returns the 8-byte (queryHandle, tagCount) response. The follow-on QueryTag
// paging request format is not yet captured (see roadmap R0.1), so browse is not yet wired.
HistorianClientOptions options = BuildOptions(host);
var result = await Task.Run(() =>
AVEVA.Historian.Client.Grpc.HistorianGrpcTagClient.ProbeStartTagQuery(options, "startswith(TagName,'Sys')", CancellationToken.None));
Assert.True(result.Success,
$"StartTagQuery(OData) should succeed; errLen={result.ErrorLength} " +
$"err=\"{System.Text.Encoding.ASCII.GetString(result.Error).Replace('\0', '.')}\"");
Assert.Equal(8, result.ResponseLength); // (queryHandle, tagCount)
}
private static HistorianClientOptions BuildOptions(string host)
{
string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER");