Phase 7 follow-up #245 — ScriptedAlarmReadable adapter over engine state

Task #245 — exposes each scripted alarm's current ActiveState as IReadable so
OPC UA variable reads on Source=ScriptedAlarm nodes return the live predicate
truth instead of BadNotFound.

## ScriptedAlarmReadable

Wraps ScriptedAlarmEngine + implements IReadable:
- Known alarm + Active → DataValueSnapshot(true, Good)
- Known alarm + Inactive → DataValueSnapshot(false, Good)
- Unknown alarm id → DataValueSnapshot(null, BadNodeIdUnknown) — surfaces
  misconfiguration rather than silently reading false
- Batch reads preserve request order

Phase7EngineComposer.Compose now returns this as ScriptedAlarmReadable when
ScriptedAlarm rows are present. ScriptedAlarmSource (IAlarmSource for the event
stream) stays in place — the IReadable is a separate adapter over the same engine.

## Tests — 6 new + 1 updated composer test = 19 total Phase 7 tests

ScriptedAlarmReadableTests covers: inactive + active predicate → bool snapshot,
unknown alarm id → BadNodeIdUnknown, batch order preservation, null-engine +
null-fullReferences guards. The active-predicate test uses ctx.GetTag on a seeded
upstream value to drive a real cascade through the engine.

Updated Phase7EngineComposerTests to assert ScriptedAlarmReadable is non-null
when alarms compose, null when only virtual tags.

## Follow-ups remaining
- #244 — driver-bridge feed populating CachedTagUpstreamSource
- #246 — Program.cs Compose call + SqliteStoreAndForwardSink lifecycle
This commit is contained in:
Joseph Doherty
2026-04-20 21:30:56 -04:00
parent f3053580a0
commit d6a8bb1064
4 changed files with 205 additions and 8 deletions

View File

@@ -72,9 +72,28 @@ public sealed class Phase7EngineComposerTests
loggerFactory: NullLoggerFactory.Instance);
result.VirtualReadable.ShouldNotBeNull();
result.ScriptedAlarmReadable.ShouldBeNull("no alarms configured");
result.Disposables.Count.ShouldBeGreaterThan(0);
}
[Fact]
public void Compose_ScriptedAlarm_rows_returns_non_null_ScriptedAlarmReadable()
{
var scripts = new[] { ScriptRow("scr-1", "return false;") };
var alarms = new[] { AlarmRow("al-1", "scr-1") };
var result = Phase7EngineComposer.Compose(
scripts, [], alarms,
upstream: new CachedTagUpstreamSource(),
alarmStateStore: new InMemoryAlarmStateStore(),
historianSink: NullAlarmHistorianSink.Instance,
rootScriptLogger: new LoggerConfiguration().CreateLogger(),
loggerFactory: NullLoggerFactory.Instance);
result.ScriptedAlarmReadable.ShouldNotBeNull("task #245 — alarm Active state readable");
result.VirtualReadable.ShouldBeNull();
}
[Fact]
public void Compose_missing_script_reference_throws_with_actionable_message()
{