fix(code-review): resolve Batch 3 wave A (OpcUaServer history/guard, ControlPlane topology gate)
- OpcUaServer-002: HistoryRead-Events NumValuesPerNode==0 now maps to unbounded (int.MaxValue) instead of the backend default-cap sentinel; no Core.Abstractions contract change (+EventMaxEvents helper tests) - OpcUaServer-004: EnsureAddressSpaceCreated guard on public mutators -> clear InvalidOperationException instead of bare NRE if called pre-start (+tests) - OpcUaServer-003: Deferred (endUtc inclusive/exclusive needs live Wonderware boundary confirmation) - Configuration-013: wire DraftValidator.ValidateClusterTopology into AdminOperationsActor deploy gate (read-only, no migration) (+2 tests)
This commit is contained in:
@@ -94,6 +94,44 @@ public sealed class NodeManagerHistoryReadEventsTests : IDisposable
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>OpcUaServer-002: a HistoryReadEvents with <c>NumValuesPerNode == 0</c> means "no limit —
|
||||
/// return ALL values" per OPC UA Part 4/11, so the backend must receive an UNBOUNDED cap
|
||||
/// (<see cref="int.MaxValue"/>), NOT the <c>maxEvents <= 0</c> "use the default cap" sentinel that
|
||||
/// would silently truncate a whole-window read.</summary>
|
||||
[Fact]
|
||||
public async Task Events_unbounded_request_passes_int_max_to_backend()
|
||||
{
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
var fake = new RecordingHistorianDataSource();
|
||||
nm.HistorianDataSource = fake;
|
||||
|
||||
const string equipmentId = "eq-unbounded";
|
||||
nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment");
|
||||
nm.MaterialiseAlarmCondition("alarm-0", equipmentId, "Cond", "OffNormalAlarm", severity: 600);
|
||||
var notifierNodeId = nm.TryGetFolder(equipmentId)!.NodeId;
|
||||
|
||||
fake.EventsResult = new HistoricalEventsResult(
|
||||
new[] { new HistoricalEvent("evt-x", "Src", DateTime.UtcNow, DateTime.UtcNow, "msg", 600) }, null);
|
||||
|
||||
var details = new ReadEventDetails
|
||||
{
|
||||
StartTime = DateTime.UtcNow.AddHours(-1),
|
||||
EndTime = DateTime.UtcNow,
|
||||
// 0 ⇒ "no limit" — the override must translate this to int.MaxValue for the backend.
|
||||
NumValuesPerNode = 0,
|
||||
Filter = SelectFilter("EventId"),
|
||||
};
|
||||
|
||||
var (_, errors) = InvokeHistoryRead(server, nm, details, notifierNodeId);
|
||||
|
||||
errors[0].StatusCode.Code.ShouldBe(StatusCodes.Good);
|
||||
// The backend saw the unbounded cap, NOT the 0/default-cap sentinel.
|
||||
fake.LastMaxEvents.ShouldBe(int.MaxValue);
|
||||
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>An unsupported select operand (BrowsePath ["EventType"]) projects to Variant.Null — a field
|
||||
/// the server can't supply is null (spec-conformant) — while supported siblings still project.</summary>
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user