fix(opcua): carry the condition Comment across a rebuild; the restore report never rolls Time back (#641 review)

Native conditions support AddComment / commented Acknowledge, so the comment (and its
ClientUserId) is condition state the rebuild dropped. And the deferred restore report
re-stamped Time with the carried instant even when a client method call had reported a
newer event in the restore->complete window; it now reports at the node's current Time.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 17:20:13 -04:00
parent 25f02e4102
commit 17dc171940
2 changed files with 45 additions and 6 deletions
@@ -94,6 +94,35 @@ public sealed class NodeManagerNativeAlarmRebuildCarryOverTests : IDisposable
await host.DisposeAsync();
}
/// <summary>
/// Gitea #641 review — the comment a client left with its acknowledgement is condition state too (native
/// conditions support AddComment), and an event the condition reported AFTER the restore (a client method
/// call landing on an SDK thread before the apply completes) must not have its newer <c>Time</c> rolled
/// back by the deferred restore report.
/// </summary>
[Trait("Category", "Unit")]
[Fact]
public async Task The_comment_is_carried_and_a_newer_report_is_not_rolled_back()
{
var (host, nm) = await BootAsync();
var raisedAt = new DateTime(2026, 10, 8, 9, 0, 0, DateTimeKind.Utc);
Materialise(nm, native: true);
nm.WriteAlarmCondition(Alarm, Fault(acked: false), raisedAt, AddressSpaceRealm.Raw);
Condition(nm).Comment.Value = new LocalizedText("on it");
nm.RebuildAddressSpace();
Materialise(nm, native: true);
var later = raisedAt.AddMinutes(5);
nm.WriteAlarmQuality(Alarm, StatusCodes.BadNoCommunication, later, AddressSpaceRealm.Raw); // reports at `later`
nm.CompleteAddressSpaceApply();
var restored = Condition(nm);
restored.Comment.Value.Text.ShouldBe("on it");
restored.Time.Value.ShouldBe(later, "the restore report must not stamp the condition back to the raise time");
await host.DisposeAsync();
}
/// <summary>Falsification control: a condition in the no-event position comes back exactly as a fresh
/// materialise builds it, and reports nothing.</summary>
[Trait("Category", "Unit")]