refactor(#655): delete the dead driver-side history capability

HistoryRead is served server-side by IHistorianDataSource; nothing ever dispatched
to a driver's IHistoryProvider. Removes IHistoryProvider, the OpcUaClient history
passthrough, DriverCapability.HistoryRead (+ resilience tier rows and AdminUI row),
HistoryReadStatusException and its node-manager catches, and the tests that only
covered them. Shared result types and HistoryAggregateNotSupportedException stay
(moved to Historian/HistoryReadContracts.cs). A stored ResilienceConfig still
carrying a HistoryRead key parses (diagnostic only; new test).

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 23:34:02 -04:00
parent ca17d2cf71
commit f7d497e532
23 changed files with 149 additions and 1608 deletions
@@ -312,61 +312,6 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
await host.DisposeAsync();
}
/// <summary>A passthrough backend's Bad status on a RESUME read surfaces verbatim for that node only (#649):
/// no outbound continuation point, the cursor is consumed (no stray cursor), and a sibling node in the same
/// batch is unaffected.</summary>
[Fact]
public async Task Raw_resume_read_that_throws_HistoryReadStatusException_surfaces_the_code_and_isolates_the_sibling()
{
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
nm.HistoryContinuationStore = new InMemoryHistoryContinuationStore();
var fake = new SeriesHistorianDataSource(MakeSeries(count: 250, stepSeconds: 1));
nm.HistorianDataSource = fake;
nm.EnsureVariable("eq-1/bad", parentFolderNodeId: null, displayName: "Bad", dataType: "Double",
writable: false, historianTagname: "WW.Bad", realm: AddressSpaceRealm.Uns);
nm.EnsureVariable("eq-1/ok", parentFolderNodeId: null, displayName: "Ok", dataType: "Double",
writable: false, historianTagname: "WW.Ok", realm: AddressSpaceRealm.Uns);
var badId = nm.TryGetVariable("eq-1/bad")!.NodeId;
var okId = nm.TryGetVariable("eq-1/ok")!.NodeId;
// Page 1 succeeds and emits a continuation point.
var (_, _, cp1) = ReadRaw(nm, badId, Epoch, Epoch.AddHours(1), max: 100, inboundCp: null);
cp1.ShouldNotBeNull();
// The backend now refuses the bad node; one batch resumes it and freshly reads the sibling.
fake.FailTagname = "WW.Bad";
var context = new OperationContext(
new RequestHeader(), secureChannelContext: null, RequestType.HistoryRead, identity: null);
var details = new ReadRawModifiedDetails
{
StartTime = Epoch, EndTime = Epoch.AddHours(1), NumValuesPerNode = 100, IsReadModified = false,
};
var nodesToRead = new List<HistoryReadValueId>
{
new() { NodeId = badId, ContinuationPoint = cp1 },
new() { NodeId = okId },
};
var results = new List<SdkHistoryReadResult> { null!, null! };
var errors = new List<ServiceResult> { null!, null! };
nm.HistoryRead(context, details, TimestampsToReturn.Both,
releaseContinuationPoints: false, nodesToRead, results, errors);
errors[0].StatusCode.Code.ShouldBe(StatusCodes.BadUserAccessDenied);
results[0].ContinuationPoint.ShouldBeNull();
errors[1].StatusCode.Code.ShouldBe(StatusCodes.Good);
((HistoryData)ExtensionObject.ToEncodeable(results[1].HistoryData)).DataValues.Count.ShouldBe(100);
results[1].ContinuationPoint.ShouldNotBeNull("the sibling is full-page and pages normally");
// The consumed cursor is gone: nothing stray to resume.
fake.FailTagname = null;
var (_, e, _) = ReadRaw(nm, badId, Epoch, Epoch.AddHours(1), max: 100, inboundCp: cp1);
e.StatusCode.Code.ShouldBe(StatusCodes.BadContinuationPointInvalid);
await host.DisposeAsync();
}
// --- helpers --------------------------------------------------------------------------------
/// <summary>Issue a single-node Raw HistoryRead and return that node's decoded sample values, error,
@@ -468,8 +413,6 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
private int _readCount;
public int ReadCount => _readCount;
/// <summary>When set, a Raw read of this tagname throws <see cref="HistoryReadStatusException"/> (#649).</summary>
public volatile string? FailTagname;
public void ResetReadCount() => _readCount = 0;
public Task<HistorianRead> ReadRawAsync(
@@ -477,8 +420,6 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
CancellationToken cancellationToken)
{
Interlocked.Increment(ref _readCount);
if (FailTagname is { } bad && bad == fullReference)
throw new HistoryReadStatusException(StatusCodes.BadUserAccessDenied);
var window = series
.Where(s => (s.SourceTimestampUtc ?? DateTime.MinValue) >= startUtc
&& (s.SourceTimestampUtc ?? DateTime.MinValue) <= endUtc)