fix(alarms): SDK timed-unshelve timer is a no-op; commented re-ack comment is durable (#597)

Review R1 M2: since #597 the SDK timer fires at real expiry, and OnTimedUnshelve routed an operator-style
Unshelve by 'system' from both pair nodes, pre-empting the engine's AutoUnshelve. The engine owns expiry,
so the callback now routes nothing. Integration test: an engine stand-in that acts on a routed Unshelve and
expires late, plus a wrapped OnTimedUnshelve (positive control: the SDK timer fired).
Review R1 m1: Part9StateMachine.ApplyAcknowledge records a commented re-ack as AddComment (persisted and
replicated) instead of a NoOp; an uncommented re-ack is still a no-op.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 07:47:28 -04:00
parent 8a9bf23048
commit 83e9ffd27a
7 changed files with 140 additions and 71 deletions
@@ -105,7 +105,17 @@ public static class Part9StateMachine
throw new ArgumentException("User identity required for audit.", nameof(user));
if (current.Acked == AlarmAckedState.Acknowledged)
return TransitionResult.NoOp(current, "already acknowledged");
{
// A COMMENTED re-ack carries a real comment even though nothing is acknowledged (Gitea #597 review
// m1): the OPC UA node reports it — the SDK writes the comment on a re-ack — so the system of
// record must hold it too, or the event advertises a comment that exists only on the node that
// served the call (never persisted, never replicated to the pair peer). Recorded exactly like
// AddComment; the ack fields (LastAck*) stay those of the real acknowledge. An uncommented re-ack
// is still a no-op.
return string.IsNullOrWhiteSpace(comment)
? TransitionResult.NoOp(current, "already acknowledged")
: ApplyAddComment(current, user, comment, nowUtc);
}
var audit = AppendComment(current.Comments, nowUtc, user, "Acknowledge", comment);
var next = current with