fix(historian): respect gateway ceilings and history value contracts
v2-ci / build (pull_request) Successful in 5m10s
v2-ci / unit-tests (pull_request) Failing after 28m34s

This commit is contained in:
Joseph Doherty
2026-10-13 03:51:04 -04:00
parent 66a2b0c3e9
commit 97668d26d3
15 changed files with 474 additions and 29 deletions
@@ -420,6 +420,54 @@ public sealed class GatewayCeilingPagingTests : IDisposable
finally { await host.DisposeAsync(); }
}
[Theory]
[InlineData(1709, 1366)] // #695 reverse-walk trace
[InlineData(1369, 1292)] // #695 bounds trace
public async Task A_warm_read_uses_the_last_room_below_the_ceiling_for_pre_window_ties(int ceiling, int cluster)
{
var start = At(1000);
var stored = Enumerable.Range(0, cluster).Select(i => (start.AddMilliseconds(-0.5), (double)i))
.Append((start, 9000d)).Append((start.AddSeconds(1), 9001d)).ToList();
var fake = Gateway(stored, ceiling);
using var ds = new GatewayHistorianDataSource(fake, NullLogger<GatewayHistorianDataSource>.Instance);
await ds.ReadRawAsync("T", At(0), At(1), 5000, Ct); // learn without crowding
fake.RawCalls.Clear();
var result = await ds.ReadRawAsync("T", start, start.AddSeconds(2), 1, Ct);
result.Samples.Select(s => s.Value).ShouldBe([9000d]);
fake.RawCalls.ShouldAllBe(c => c.MaxValues <= ceiling);
}
[Fact]
public async Task A_warm_reverse_segment_can_shrink_again_when_the_pre_window_cluster_grows()
{
var start = At(1000);
var stored = Enumerable.Range(0, 1366).Select(i => (start.AddMilliseconds(-0.5), (double)i))
.Concat(Enumerable.Range(0, 500).Select(i => (start.AddMilliseconds(i), 9000d + i))).ToList();
var fake = Gateway(stored, 1709);
using var ds = new GatewayHistorianDataSource(fake, NullLogger<GatewayHistorianDataSource>.Instance);
await ds.ReadRawAsync("T", At(0), At(1), 5000, Ct);
var page = await ReverseRawReader.ReadAsync(ds, "T", start.AddMilliseconds(499), start.AddTicks(-1),
249, 65536, At(2000), new ReverseWalkHint(499 * TimeSpan.TicksPerMillisecond), Ct);
page.Samples.Select(s => (double)s.Value!).ShouldBe(Enumerable.Range(9251, 249).Reverse().Select(i => (double)i));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task Gateway_transport_faults_surface_as_BadNoCommunication(bool deadline)
{
var fake = new FakeHistorianGatewayClient { ThrowOnRead = deadline
? new ZB.MOM.WW.HistorianGateway.Client.HistorianGatewayException("deadline", new Grpc.Core.RpcException(new Grpc.Core.Status(Grpc.Core.StatusCode.DeadlineExceeded, "deadline")))
: new ZB.MOM.WW.HistorianGateway.Client.HistorianGatewayUnavailableException("unavailable") };
var (host, nm, nodeId) = await BootAsync(fake);
try
{
var r = ReadRaw(nm, nodeId, At(0), At(10), 10, null);
r.Error.StatusCode.Code.ShouldBe(StatusCodes.BadNoCommunication);
}
finally { await host.DisposeAsync(); }
}
// ---- helpers ---------------------------------------------------------------------------------------------------------
private async Task<(OpcUaApplicationHost Host, OtOpcUaNodeManager Nm, NodeId NodeId)> BootAsync(