D2 (new path): SDK-direct WCF revision orchestrator + probe
Implemented HistorianWcfRevisionOrchestrator that talks WCF directly to /Trx, bypassing the native wrapper entirely. Probes AddNonStreamValuesBegin2 against the live local Historian and surfaces what the server returns. Internal-only API; no public surface added — the path isn't viable yet. Findings (live test against localhost): - ✅ The wire path is reachable. After moving from V1 (uint handle, no errorBuffer) to V2 (string handle GUID, out errorBuffer), the server recognizes the call (no ContractFilter mismatch, no exception). - ✅ Server processes the call and returns a structured 5-byte error buffer: 04 33 00 00 00 = type 4 (CustomError) + code 51 (UnknownClient). - ❌ Tried four handle formats (contextKey upper/lower, storageSessionId upper, ClientHandle as decimal string) — all return the same UnknownClient. - ❌ Adding the full priming chain (Stat.GetV ×2, Stat.GETHI ×2, UpdC3, 6× Stat.GetSystemParameter, AllowRenameTags, Trx.GetV, Stat.GetV, Retr.GetV) — same result. ITransactionServiceContract2 has no Validate/Register/Open op of its own. The client-with-Trx registration must happen via some cross- service side effect we haven't isolated. Important takeaway: the wire-format mismatch is solved (contract method names + parameter shapes match what the server expects). The remaining gap is a single missing initialization step. Documented in docs/plans/revision-write-path.md as concrete next-session steps. 178/178 tests pass (one new probe test added). Probe is gated on HISTORIAN_HOST=localhost. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -134,6 +134,51 @@ path is implementable. If it fails with a server-side cache error,
|
|||||||
try `RTag2` first. If it still fails, the path is genuinely blocked
|
try `RTag2` first. If it still fails, the path is genuinely blocked
|
||||||
server-side.
|
server-side.
|
||||||
|
|
||||||
|
### SDK-direct probe results (2026-05-05)
|
||||||
|
|
||||||
|
`HistorianWcfRevisionOrchestrator` wires up the priming chain + a probe
|
||||||
|
of `ITransactionServiceContract2.AddNonStreamValuesBegin2(string handle, out string transactionId, out byte[] errorBuffer)`.
|
||||||
|
Live test against `localhost`:
|
||||||
|
|
||||||
|
- ✅ `OpenSucceeded: True` — Hist auth chain + Open2 still work end-to-end
|
||||||
|
- ✅ Trx channel opens, `Trx.GetV` returns interface version 2
|
||||||
|
- ✅ Wire path is recognized — server processes the call (no
|
||||||
|
`ActionNotSupportedException` after switching from the abbreviated
|
||||||
|
`AddNonS2B` to the default action name)
|
||||||
|
- ❌ Server returns structured error `04 33 00 00 00` =
|
||||||
|
type 4 (CustomError) + code 51 (`UnknownClient`) for all four handle
|
||||||
|
formats tried (contextKey GUID upper, storageSessionId upper, contextKey
|
||||||
|
lower, ClientHandle as string)
|
||||||
|
- ❌ Adding the full priming chain (Stat.GetV ×2, Stat.GETHI ×2, UpdC3,
|
||||||
|
6× Stat.GetSystemParameter, AllowRenameTags, Trx.GetV, Stat.GetV,
|
||||||
|
Retr.GetV) doesn't change the result — Trx still rejects with
|
||||||
|
`UnknownClient`
|
||||||
|
|
||||||
|
`ITransactionServiceContract2` exposes only `GetV`, `ForwardSnapshot*`,
|
||||||
|
and `AddNonStreamValues*`. There is no `ValidateClient`, `RegisterClient`,
|
||||||
|
or `Open` on Trx. So the client-with-Trx registration must happen via
|
||||||
|
some cross-service side effect we haven't identified.
|
||||||
|
|
||||||
|
**Important takeaway:** the wire path works at the WCF protocol layer.
|
||||||
|
We're past the "is this even reachable" question. The remaining gap is
|
||||||
|
finding what populates Trx's session table — likely:
|
||||||
|
|
||||||
|
1. `RTag2` on /Hist with a tag whose registration cascades to Trx
|
||||||
|
2. Some `IStorageServiceContract` op that we haven't tried
|
||||||
|
3. An aspect of the C++ HistorianClient initialization that doesn't
|
||||||
|
show up in the IL we've inspected (e.g., the
|
||||||
|
`aahClientCommon.CClientCommon` calls during InitializeProxy)
|
||||||
|
|
||||||
|
A future session that wants to push further should:
|
||||||
|
1. Add `RTag2` for the sandbox tag and retry Begin2 — quick experiment
|
||||||
|
2. If that fails, try sending the IStorageServiceContract.AddT or
|
||||||
|
similar to "introduce" the tag to Trx
|
||||||
|
3. If that fails, do an IL walk of `aahClientCommon.CClientCommon`
|
||||||
|
methods called between Open2 and AddNonStreamValuesBegin in a
|
||||||
|
working native scenario (using a system tag the wrapper would
|
||||||
|
accept — or capturing actual on-wire bytes via the IL-rewrite
|
||||||
|
instrumentation if possible)
|
||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
Do **not** add public `WriteRevisionsAsync` / `BeginRevisionAsync` to
|
Do **not** add public `WriteRevisionsAsync` / `BeginRevisionAsync` to
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.ServiceModel;
|
using System.ServiceModel;
|
||||||
|
|
||||||
namespace AVEVA.Historian.Client.Wcf.Contracts;
|
namespace AVEVA.Historian.Client.Wcf.Contracts;
|
||||||
@@ -26,3 +27,40 @@ internal interface ITransactionServiceContract
|
|||||||
[OperationContract]
|
[OperationContract]
|
||||||
uint AddNonStreamValuesEnd(uint handle, string transactionId, bool commit);
|
uint AddNonStreamValuesEnd(uint handle, string transactionId, bool commit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// V2 surface — discovered by inspecting CHistoryConnectionWCF.AddNonStreamValuesBegin's
|
||||||
|
/// IL (token 0x06004051), which calls
|
||||||
|
/// <c>ITransactionServiceContract2::AddNonStreamValuesBegin2(string, ref string, ref byte[])</c>
|
||||||
|
/// before falling back to V1. The V2 ops use the GUID-string handle pattern matching
|
||||||
|
/// other V2 ops on /Hist (EnsT2, AddS2, RTag2) plus an out-byte[] errorBuffer.
|
||||||
|
/// </remarks>
|
||||||
|
[ServiceContract(Name = HistorianWcfServiceNames.Transaction, Namespace = HistorianWcfServiceNames.Namespace)]
|
||||||
|
internal interface ITransactionServiceContract2
|
||||||
|
{
|
||||||
|
[OperationContract(Name = "GetV")]
|
||||||
|
uint GetInterfaceVersion(out uint version);
|
||||||
|
|
||||||
|
[OperationContract]
|
||||||
|
[return: MarshalAs(UnmanagedType.U1)]
|
||||||
|
bool AddNonStreamValuesBegin2(
|
||||||
|
string handle,
|
||||||
|
out string transactionId,
|
||||||
|
out byte[] errorBuffer);
|
||||||
|
|
||||||
|
[OperationContract]
|
||||||
|
[return: MarshalAs(UnmanagedType.U1)]
|
||||||
|
bool AddNonStreamValues2(
|
||||||
|
string handle,
|
||||||
|
string transactionId,
|
||||||
|
[MessageParameter(Name = "pBuf")] byte[] buffer,
|
||||||
|
out byte[] errorBuffer);
|
||||||
|
|
||||||
|
[OperationContract]
|
||||||
|
[return: MarshalAs(UnmanagedType.U1)]
|
||||||
|
bool AddNonStreamValuesEnd2(
|
||||||
|
string handle,
|
||||||
|
string transactionId,
|
||||||
|
[MarshalAs(UnmanagedType.U1)] bool commit,
|
||||||
|
out byte[] errorBuffer);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.ServiceModel;
|
||||||
|
using System.ServiceModel.Channels;
|
||||||
|
using AVEVA.Historian.Client.Wcf.Contracts;
|
||||||
|
|
||||||
|
namespace AVEVA.Historian.Client.Wcf;
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// Drives the AddNonStreamValuesBegin / AddNonStreamValues / AddNonStreamValuesEnd
|
||||||
|
/// WCF op group on the <c>/Trx</c> service end-to-end. The native AVEVA wrapper's
|
||||||
|
/// equivalent surface (<c>HistorianAccess.AddRevisionValues*</c>) is gated by the
|
||||||
|
/// C++ <c>HistorianClient</c>'s per-connection cache and rejects all writes from a
|
||||||
|
/// managed client with err 129 <c>TagNotFoundInCache</c>. This SDK orchestrator
|
||||||
|
/// bypasses the wrapper entirely — talks WCF directly — to test whether the SERVER
|
||||||
|
/// gates on the same condition.
|
||||||
|
///
|
||||||
|
/// Live behavior is unverified. The first iteration is probe-only: open the auth
|
||||||
|
/// chain, drive the standard write priming, call AddNonStreamValuesBegin and
|
||||||
|
/// surface whatever the server returns.
|
||||||
|
/// </remarks>
|
||||||
|
internal sealed class HistorianWcfRevisionOrchestrator
|
||||||
|
{
|
||||||
|
private readonly HistorianClientOptions _options;
|
||||||
|
|
||||||
|
public HistorianWcfRevisionOrchestrator(HistorianClientOptions options)
|
||||||
|
{
|
||||||
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<HistorianRevisionProbeResult> ProbeBeginAsync(CancellationToken cancellationToken)
|
||||||
|
=> Task.Run(() => ProbeBegin(cancellationToken), cancellationToken);
|
||||||
|
|
||||||
|
private HistorianRevisionProbeResult ProbeBegin(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Guid contextKey = Guid.NewGuid();
|
||||||
|
var (histBinding, histEndpoint, _, _) = HistorianWcfBindingFactory.CreateBindingPair(_options);
|
||||||
|
Binding auxBinding = HistorianWcfBindingFactory.CreateAuxiliaryBinding(_options);
|
||||||
|
EndpointAddress transactionEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(_options, HistorianWcfServiceNames.Transaction);
|
||||||
|
|
||||||
|
HistorianRevisionProbeResult result = new();
|
||||||
|
|
||||||
|
HistorianWcfAuthChainHelper.OpenAuthenticatedConnection(
|
||||||
|
_options, histBinding, histEndpoint, contextKey, cancellationToken,
|
||||||
|
connectionMode: HistorianWcfAuthChainHelper.NativeIntegratedWriteEnabledConnectionMode,
|
||||||
|
additionalSetup: (historyChannel, context) =>
|
||||||
|
{
|
||||||
|
result.OpenSucceeded = true;
|
||||||
|
result.ClientHandle = context.ClientHandle;
|
||||||
|
result.StorageSessionId = context.StorageSessionId;
|
||||||
|
|
||||||
|
// Run the same priming chain that EnsT2/DelT use — without it, the Trx
|
||||||
|
// service rejects calls with err 51 UnknownClient because the client
|
||||||
|
// hasn't registered itself across the auxiliary services.
|
||||||
|
EndpointAddress statusEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(_options, HistorianWcfServiceNames.Status);
|
||||||
|
EndpointAddress retrievalEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(_options, HistorianWcfServiceNames.Retrieval);
|
||||||
|
RunPrimingChain(historyChannel, context, auxBinding, statusEndpoint, transactionEndpoint, retrievalEndpoint);
|
||||||
|
|
||||||
|
ChannelFactory<ITransactionServiceContract2> trxFactory = new(auxBinding, transactionEndpoint);
|
||||||
|
HistorianWcfClientCredentialsHelper.Configure(trxFactory, _options);
|
||||||
|
ITransactionServiceContract2 trxChannel = trxFactory.CreateChannel();
|
||||||
|
ICommunicationObject trxCo = (ICommunicationObject)trxChannel;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Get interface version first to register the client in the Trx service's
|
||||||
|
// session table (matches the cross-service GetV priming pattern used by
|
||||||
|
// RunWritePriming for EnsT2/DelT).
|
||||||
|
try
|
||||||
|
{
|
||||||
|
uint trxRc = trxChannel.GetInterfaceVersion(out uint trxVersion);
|
||||||
|
result.TrxInterfaceVersionReturnCode = trxRc;
|
||||||
|
result.TrxInterfaceVersion = trxVersion;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
result.TrxInterfaceVersionException = $"{ex.GetType().Name}: {ex.Message}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Probe V2 AddNonStreamValuesBegin2. Try BOTH possible handle formats —
|
||||||
|
// the server returns 0433000000 (UnknownClient = 51) when the wrong one
|
||||||
|
// is sent. Capture which one (if any) is recognized.
|
||||||
|
foreach ((string label, string handle) in new[]
|
||||||
|
{
|
||||||
|
("contextKey", contextKey.ToString("D").ToUpperInvariant()),
|
||||||
|
("storageSessionId", context.StorageSessionId.ToString("D").ToUpperInvariant()),
|
||||||
|
("contextKey-lower", contextKey.ToString("D")),
|
||||||
|
("clientHandle-as-string", context.ClientHandle.ToString()),
|
||||||
|
})
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string? transactionId = null;
|
||||||
|
byte[]? errorBuffer = null;
|
||||||
|
bool ok = trxChannel.AddNonStreamValuesBegin2(handle, out transactionId, out errorBuffer);
|
||||||
|
result.BeginAttempts.Add(new HistorianRevisionBeginAttempt
|
||||||
|
{
|
||||||
|
HandleLabel = label,
|
||||||
|
HandleSent = handle,
|
||||||
|
Succeeded = ok,
|
||||||
|
TransactionId = transactionId,
|
||||||
|
ErrorHex = errorBuffer is null || errorBuffer.Length == 0 ? null : Convert.ToHexString(errorBuffer),
|
||||||
|
});
|
||||||
|
if (ok && !string.IsNullOrEmpty(transactionId))
|
||||||
|
{
|
||||||
|
result.BeginSucceeded = true;
|
||||||
|
result.BeginTransactionId = transactionId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
result.BeginAttempts.Add(new HistorianRevisionBeginAttempt
|
||||||
|
{
|
||||||
|
HandleLabel = label,
|
||||||
|
HandleSent = handle,
|
||||||
|
Exception = $"{ex.GetType().Name}: {ex.Message}",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { if (trxCo.State == CommunicationState.Faulted) trxCo.Abort(); else trxCo.Close(); } catch { try { trxCo.Abort(); } catch { } }
|
||||||
|
try { if (trxFactory.State == CommunicationState.Faulted) trxFactory.Abort(); else trxFactory.Close(); } catch { try { trxFactory.Abort(); } catch { } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mirrors HistorianWcfTagWriteOrchestrator.RunWritePriming. The cross-service GetV
|
||||||
|
/// calls + UpdC3 register the client in each aux service's session table so that
|
||||||
|
/// subsequent ops (like AddNonStreamValuesBegin2 on /Trx) recognize the handle.
|
||||||
|
/// </summary>
|
||||||
|
private static void RunPrimingChain(
|
||||||
|
IHistoryServiceContract2 historyChannel,
|
||||||
|
HistorianWcfAuthChainHelper.OpenConnectionContext context,
|
||||||
|
Binding auxBinding,
|
||||||
|
EndpointAddress statusEndpoint,
|
||||||
|
EndpointAddress transactionEndpoint,
|
||||||
|
EndpointAddress retrievalEndpoint)
|
||||||
|
{
|
||||||
|
string handle = context.StorageSessionId.ToString("D").ToUpperInvariant();
|
||||||
|
|
||||||
|
ChannelFactory<IStatusServiceContract2> statusFactory = new(auxBinding, statusEndpoint);
|
||||||
|
IStatusServiceContract2 statusChannel = statusFactory.CreateChannel();
|
||||||
|
ChannelFactory<ITransactionServiceContract> transactionFactory = new(auxBinding, transactionEndpoint);
|
||||||
|
ITransactionServiceContract transactionChannel = transactionFactory.CreateChannel();
|
||||||
|
ChannelFactory<IRetrievalServiceContract4> retrievalFactory = new(auxBinding, retrievalEndpoint);
|
||||||
|
IRetrievalServiceContract4 retrievalChannel = retrievalFactory.CreateChannel();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TryRun(() => statusChannel.GetInterfaceVersion(out _));
|
||||||
|
TryRun(() => statusChannel.GetInterfaceVersion(out _));
|
||||||
|
byte[] historianVersionRequest = BuildGetHistorianInfoRequest("HistorianVersion");
|
||||||
|
TryRun(() => statusChannel.GetHistorianInfo(handle, historianVersionRequest, out _, out _));
|
||||||
|
TryRun(() => statusChannel.GetHistorianInfo(handle, historianVersionRequest, out _, out _));
|
||||||
|
|
||||||
|
byte[] clientStatus = BuildUpdC3ClientStatusBlob();
|
||||||
|
TryRun(() => historyChannel.UpdateClientStatus3(handle, (uint)clientStatus.Length, ref clientStatus, out _, out _, out _, out _));
|
||||||
|
|
||||||
|
foreach (string parameterName in new[] { "AllowOriginals", "HistorianPartner", "HistorianVersion", "MaxCyclicStorageTimeout", "RealTimeWindow", "FutureTimeThreshold", "AllowRenameTags" })
|
||||||
|
{
|
||||||
|
TryRun(() => statusChannel.GetSystemParameter(context.ClientHandle, parameterName, out _, out _, out _));
|
||||||
|
}
|
||||||
|
TryRun(() => transactionChannel.GetInterfaceVersion(out _));
|
||||||
|
TryRun(() => statusChannel.GetInterfaceVersion(out _));
|
||||||
|
TryRun(() => retrievalChannel.GetInterfaceVersion(out _));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseSafely(retrievalChannel, retrievalFactory);
|
||||||
|
CloseSafely(transactionChannel, transactionFactory);
|
||||||
|
CloseSafely(statusChannel, statusFactory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] BuildUpdC3ClientStatusBlob()
|
||||||
|
{
|
||||||
|
byte[] blob = new byte[81];
|
||||||
|
blob[0] = 0x02;
|
||||||
|
blob[1] = 0x01;
|
||||||
|
blob[77] = 0x1E;
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] BuildGetHistorianInfoRequest(string parameterName)
|
||||||
|
{
|
||||||
|
byte[] nameBytes = System.Text.Encoding.Unicode.GetBytes(parameterName);
|
||||||
|
int payloadLength = nameBytes.Length > 0 ? nameBytes.Length - 1 : 0;
|
||||||
|
byte[] buffer = new byte[8 + payloadLength];
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(0, 2), 0x6753);
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(2, 2), 0x0002);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(4, 4), (uint)parameterName.Length);
|
||||||
|
Buffer.BlockCopy(nameBytes, 0, buffer, 8, payloadLength);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryRun(Action a) { try { a(); } catch { } }
|
||||||
|
|
||||||
|
private static void CloseSafely(object channel, ICommunicationObject factory)
|
||||||
|
{
|
||||||
|
try { if (channel is ICommunicationObject co) { if (co.State == CommunicationState.Faulted) co.Abort(); else co.Close(); } } catch { }
|
||||||
|
try { if (factory.State == CommunicationState.Faulted) factory.Abort(); else factory.Close(); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class HistorianRevisionProbeResult
|
||||||
|
{
|
||||||
|
public bool OpenSucceeded { get; set; }
|
||||||
|
public uint ClientHandle { get; set; }
|
||||||
|
public Guid StorageSessionId { get; set; }
|
||||||
|
public uint? TrxInterfaceVersionReturnCode { get; set; }
|
||||||
|
public uint? TrxInterfaceVersion { get; set; }
|
||||||
|
public string? TrxInterfaceVersionException { get; set; }
|
||||||
|
public string? BeginTransactionId { get; set; }
|
||||||
|
public bool BeginSucceeded { get; set; }
|
||||||
|
public string? BeginErrorHex { get; set; }
|
||||||
|
public string? BeginException { get; set; }
|
||||||
|
public List<HistorianRevisionBeginAttempt> BeginAttempts { get; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class HistorianRevisionBeginAttempt
|
||||||
|
{
|
||||||
|
public string HandleLabel { get; set; } = "";
|
||||||
|
public string HandleSent { get; set; } = "";
|
||||||
|
public bool Succeeded { get; set; }
|
||||||
|
public string? TransactionId { get; set; }
|
||||||
|
public string? ErrorHex { get; set; }
|
||||||
|
public string? Exception { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Runtime.Versioning;
|
||||||
|
using AVEVA.Historian.Client;
|
||||||
|
using AVEVA.Historian.Client.Models;
|
||||||
|
using AVEVA.Historian.Client.Wcf;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace AVEVA.Historian.Client.Tests;
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// Probes the SDK-direct WCF revision-write path (D2 new path). Calls
|
||||||
|
/// <c>AddNonStreamValuesBegin</c> through <see cref="HistorianWcfRevisionOrchestrator"/>
|
||||||
|
/// against the live local Historian and surfaces what the server returns. The
|
||||||
|
/// underlying native wrapper is gated client-side by err 129 TagNotFoundInCache;
|
||||||
|
/// this test bypasses the wrapper entirely and asks the SERVER directly. Gated on
|
||||||
|
/// HISTORIAN_HOST=localhost; skips otherwise.
|
||||||
|
/// </remarks>
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
public sealed class HistorianWcfRevisionProbeTests
|
||||||
|
{
|
||||||
|
private readonly ITestOutputHelper _output;
|
||||||
|
|
||||||
|
public HistorianWcfRevisionProbeTests(ITestOutputHelper output)
|
||||||
|
{
|
||||||
|
_output = output;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddNonStreamValuesBegin_ProbeReturnsServerResult()
|
||||||
|
{
|
||||||
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
||||||
|
if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HistorianClientOptions options = new()
|
||||||
|
{
|
||||||
|
Host = host,
|
||||||
|
IntegratedSecurity = true,
|
||||||
|
Transport = HistorianTransport.LocalPipe,
|
||||||
|
};
|
||||||
|
|
||||||
|
HistorianWcfRevisionOrchestrator orchestrator = new(options);
|
||||||
|
HistorianRevisionProbeResult result = await orchestrator.ProbeBeginAsync(CancellationToken.None);
|
||||||
|
|
||||||
|
_output.WriteLine($"OpenSucceeded: {result.OpenSucceeded}");
|
||||||
|
_output.WriteLine($"ClientHandle: {result.ClientHandle}");
|
||||||
|
_output.WriteLine($"StorageSessionId: {result.StorageSessionId}");
|
||||||
|
_output.WriteLine($"TrxInterfaceVersion: {result.TrxInterfaceVersion} (rc={result.TrxInterfaceVersionReturnCode}) ex={result.TrxInterfaceVersionException}");
|
||||||
|
_output.WriteLine($"BeginSucceeded: {result.BeginSucceeded}");
|
||||||
|
_output.WriteLine($"BeginTransactionId: {result.BeginTransactionId}");
|
||||||
|
foreach (HistorianRevisionBeginAttempt attempt in result.BeginAttempts)
|
||||||
|
{
|
||||||
|
_output.WriteLine($" attempt[{attempt.HandleLabel}] handle={attempt.HandleSent} ok={attempt.Succeeded} tx={attempt.TransactionId} err={attempt.ErrorHex} ex={attempt.Exception}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(result.OpenSucceeded, "Auth chain failed; revision probe never reached the Trx endpoint.");
|
||||||
|
// Don't assert BeginSucceeded — we're surfacing whatever the server says, not requiring success.
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user