diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorianDataSource.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorianDataSource.cs
index 3b9c1b97..309a0b63 100644
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorianDataSource.cs
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorianDataSource.cs
@@ -143,8 +143,8 @@ public interface IHistorianDataSource : IDisposable
///
///
/// The default forwards a forward read to the five-argument overload and throws
- /// for a reverse one — a backend that cannot read newest-first (Gateway,
- /// Null). The server never relies on that throw: it consults first and walks
+ /// for a reverse one — a backend that cannot read newest-first (the
+ /// Gateway). The server never relies on that throw: it consults first and walks
/// such a read back from forward reads (#691.4), never returning the earliest events in ascending order as if
/// they were the answer. A backend that serves reverse natively must override BOTH members.
///
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/NullHistorianDataSource.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/NullHistorianDataSource.cs
index 9d97b317..811e3dc2 100644
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/NullHistorianDataSource.cs
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/NullHistorianDataSource.cs
@@ -61,13 +61,12 @@ public sealed class NullHistorianDataSource : IHistorianDataSource
public bool SupportsReverseRaw => true;
///
- /// false (Gitea #675): there is no history to read in either direction; the server walks a reverse
- /// read back from (empty) forward reads like any backend that cannot read newest-first (#691.4).
- public bool SupportsReverseEvents => false;
+ /// true, like (Gitea #691.4 review; false under #675): there is no
+ /// history to read in either direction — no need for the server to walk back through empty forward reads.
+ public bool SupportsReverseEvents => true;
///
- /// Forward ⇒ the empty result; reverse ⇒ , the interface contract for a
- /// backend without .
+ /// The empty result in either direction.
public Task ReadEventsAsync(
string? sourceName,
DateTime startUtc,
@@ -75,9 +74,7 @@ public sealed class NullHistorianDataSource : IHistorianDataSource
int maxEvents,
bool reverse,
CancellationToken cancellationToken) =>
- reverse
- ? throw new NotSupportedException("The null historian cannot read events newest-first (reverse).")
- : Task.FromResult(EmptyEvents);
+ Task.FromResult(EmptyEvents);
///
///
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs
index 8488e171..a939ed3c 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs
@@ -5291,7 +5291,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
reverse = startUtc > endUtc;
}
- // A reverse read on a backend that cannot read newest-first (Gateway, Null) is never sent there (#675 refused it
+ // A reverse read on a backend that cannot read newest-first (the Gateway) is never sent there (#675 refused it
// here); since #691.4 it is walked back from bounded forward reads — the raw walk (#686), with ties ordered by
// EventId because the gateway orders by event time only. Decided by the capability, not by catching an
// exception, so a NotSupportedException thrown from inside a backend stays a backend fault.
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/ReverseEventsReader.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/ReverseEventsReader.cs
index 52ab3bfd..f19648d3 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/ReverseEventsReader.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/ReverseEventsReader.cs
@@ -9,7 +9,7 @@ internal readonly record struct ReverseEventsPage(List Events,
///
/// Gitea #691.4: serves a Part 11 reverse events read — (lo, hi] newest-first — on a backend that reads events
-/// forward only ( = false: Gateway, Null) with the
+/// forward only ( = false: the Gateway) with the
/// . A segment read is cut short when the source flags truncation (its continuation point); ties
/// are ordered by because the gateway orders by event time only.
///
diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/Historian/NullHistorianDataSourceTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/Historian/NullHistorianDataSourceTests.cs
index e9045e7b..4c4b1dd7 100644
--- a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/Historian/NullHistorianDataSourceTests.cs
+++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/Historian/NullHistorianDataSourceTests.cs
@@ -68,17 +68,19 @@ public sealed class NullHistorianDataSourceTests
}
///
- /// #675: the direction-aware overload's default — forward is the five-argument read, reverse is refused
- /// (the server answers BadHistoryOperationUnsupported).
+ /// #691.4 review: like raw, the null source reads events in either direction (empty) and says so — the server sends
+ /// it the reverse read directly instead of walking back through empty forward reads (it refused reverse under #675).
///
[Fact]
- public async Task Direction_aware_ReadEventsAsync_forwards_forward_and_refuses_reverse()
+ public async Task Direction_aware_ReadEventsAsync_is_empty_in_both_directions()
{
IHistorianDataSource source = Source;
+ source.SupportsReverseEvents.ShouldBeTrue();
(await source.ReadEventsAsync(null, DateTime.UtcNow.AddHours(-1), DateTime.UtcNow, 0, reverse: false, Ct))
.Events.ShouldBeEmpty();
- await Should.ThrowAsync(() =>
- source.ReadEventsAsync(null, DateTime.UtcNow, DateTime.UtcNow.AddHours(-1), 0, reverse: true, Ct));
+ var reverse = await source.ReadEventsAsync(null, DateTime.UtcNow, DateTime.UtcNow.AddHours(-1), 0, reverse: true, Ct);
+ reverse.Events.ShouldBeEmpty();
+ reverse.ContinuationPoint.ShouldBeNull();
}
/// GetHealthSnapshot reports a fully-disabled (zeroed, disconnected, no-node) shape.