refactor(scripted-alarms): review-fix polish for T5/T7/T8 (observer isolation, warning hoist, doc)

This commit is contained in:
Joseph Doherty
2026-06-10 14:32:49 -04:00
parent b28c6bdb62
commit 55101baaa4
3 changed files with 64 additions and 34 deletions
@@ -76,6 +76,14 @@ public sealed class EfAlarmConditionStateStore : IAlarmStateStore
}
/// <inheritdoc />
/// <remarks>
/// <b>Concurrency assumption</b>: saves for a given <c>alarmId</c> are serialized by the
/// owning host actor (one actor owns the engine per equipment), mirroring
/// <c>EfAlarmActorStateStore</c>. The check-then-insert pattern is therefore safe under
/// that guarantee — two concurrent inserts for the same alarm cannot occur in the live
/// runtime. The <see cref="DbUpdateConcurrencyException"/> catch handles the edge case of a
/// racing concurrent restart during crash recovery.
/// </remarks>
public async Task SaveAsync(AlarmConditionState state, CancellationToken ct)
{
using var db = await _dbFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
@@ -85,6 +93,8 @@ public sealed class EfAlarmConditionStateStore : IAlarmStateStore
if (row is null)
{
// Required members are set here to satisfy the compiler; ApplyState is the single
// source of truth for all columns and immediately overwrites these values below.
row = new ScriptedAlarmState
{
ScriptedAlarmId = state.AlarmId,
@@ -151,15 +161,15 @@ public sealed class EfAlarmConditionStateStore : IAlarmStateStore
AlarmId: row.ScriptedAlarmId,
Enabled: string.Equals(row.EnabledState, "Disabled", StringComparison.Ordinal)
? AlarmEnabledState.Disabled
: AlarmEnabledState.Enabled,
: AlarmEnabledState.Enabled, // unknown string → Enabled (safe default)
// Active is not persisted — the engine re-derives it from the predicate at startup.
Active: AlarmActiveState.Inactive,
Acked: string.Equals(row.AckedState, "Acknowledged", StringComparison.Ordinal)
? AlarmAckedState.Acknowledged
: AlarmAckedState.Unacknowledged,
: AlarmAckedState.Unacknowledged, // unknown string → Unacknowledged (safe default)
Confirmed: string.Equals(row.ConfirmedState, "Confirmed", StringComparison.Ordinal)
? AlarmConfirmedState.Confirmed
: AlarmConfirmedState.Unconfirmed,
: AlarmConfirmedState.Unconfirmed, // unknown string → Unconfirmed (safe default)
Shelving: new ShelvingState(MapShelvingFromColumn(row.ShelvingState), row.ShelvingExpiresUtc),
// No transition column — UpdatedAtUtc carries the last transition timestamp.
LastTransitionUtc: row.UpdatedAtUtc,
@@ -194,7 +204,7 @@ public sealed class EfAlarmConditionStateStore : IAlarmStateStore
{
"OneShotShelved" => ShelvingKind.OneShot,
"TimedShelved" => ShelvingKind.Timed,
_ => ShelvingKind.Unshelved,
_ => ShelvingKind.Unshelved, // unknown string → Unshelved (safe default)
};
private static string SerializeComments(ImmutableList<AlarmComment> comments)
@@ -202,6 +212,8 @@ public sealed class EfAlarmConditionStateStore : IAlarmStateStore
if (comments.IsEmpty) return "[]";
var dtos = comments.Select(c => new CommentDto
{
// AlarmComment.TimestampUtc must be DateTimeKind.Utc for correct ISO-8601 round-trip;
// the engine always creates AlarmComment instances with Utc kind.
TimestampUtc = c.TimestampUtc,
User = c.User,
Kind = c.Kind,