Commit Graph

3 Commits

Author SHA1 Message Date
Joseph Doherty
69e1d320ac Cold-start guard for script engines — skip evaluation with empty upstream
Both VirtualTagEngine and ScriptedAlarmEngine share a pattern: the
BuildReadCache helper iterates the script's declared input set, reading
from _valueCache with a fallback to _upstream.ReadTag. When an upstream
tag hasn't yet delivered its first subscription push, ReadTag returns a
DataValueSnapshot with a null Value and BadNotConnected quality. User
scripts then cast `(double)ctx.GetTag(path).Value` unconditionally and
throw NullReferenceException — once per evaluation tick until the cache
fills, spamming the log with identical stack traces. The existing catch
block recovered (kept the prior state) but didn't silence the churn.

Add AreInputsReady(cache) to both engines: return true only when every
entry has a non-null Value and a non-Bad StatusCode (Good + Uncertain
are both considered ready). Skip script evaluation when the check
returns false — the engine holds the prior state (alarm) or the prior
snapshot (virtual tag) until upstream delivers. Eliminates the cold-
start NRE spam at root without changing the script-engine contract.

Also: fix $changeLines.Count in test-galaxy.ps1 — PowerShell's
Set-StrictMode -Version 3.0 errors on .Count when Where-Object returns
0 or 1 items. Wrap in `@(...)` to force an array; same pattern the
sibling _common.ps1 already uses in Write-Summary for the same reason.

Task #112 — the Galaxy live E2E now passes 3/7 stages (probe + source
read + reverse-bridge-ACL). The remaining 4 stages (virtual-tag,
subscribe-sees-change, alarm-fires, history-read) are deployment-
specific: MoveInBatchID is idle in this Galaxy + its AccessLevel blocks
writes + it's not historized. Cold-start behaviour is now correct, so
once the seed points at a live attribute those stages should light up.

Tests: 36/36 VirtualTags.Tests + 47/47 ScriptedAlarms.Tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:43:48 -04:00
Joseph Doherty
8be82e02c2 Path-based NodeIds — decouple client contract from driver address
The pre-refactor design minted OPC UA NodeIds directly from the driver's
FullReference (the native-address string). That had three long-term
problems:

1. OPC UA Part 3 §5.2.2 requires NodeIds to be immutable across a node's
   lifetime. A rename of the underlying device address — Galaxy attribute,
   S7 tag, Modbus register alias — changed the NodeId and broke every
   client that had pinned the previous identifier.
2. Two drivers with coincidentally-matching native addresses (e.g. `temp`
   in Modbus and `temp` in S7 under different Equipment rows) collided on
   the NodeId identifier.
3. TagConfig was being placed verbatim on the wire; for drivers whose
   TagConfig is JSON (every driver shipped today, per the
   CK_Tag_TagConfig_IsJson check constraint), clients saw the raw JSON
   blob as the NodeId string.

Refactor:

* DriverNodeManager.Variable now mints a stable path-based NodeId
  `{driverId}/{folder-path}/{browseName}` and records the driver-side
  FullReference in a new _fullRefByNodeId map. OnReadValue / OnWriteValue
  / ResolveFullRef look the FullReference up via that map instead of
  casting NodeId.Identifier. The old cast path is preserved as a
  fallback so any test fixture that still registers variables with
  FullRef-shaped NodeIds keeps working.

* EquipmentNodeWalker.AddTagVariable now extracts the cross-driver
  `FullName` field from Tag.TagConfig before handing the address to
  DriverAttributeInfo. Every shipped driver stores the wire reference in
  TagConfig[FullName]; falling back to the raw string covers any future
  driver that wants an opaque non-JSON address. ExtractFullName is
  exposed internal for unit coverage.

* scripts/e2e/test-galaxy.ps1 defaults updated to the new path-based
  NodeIds. Verified live against p7-smoke-galaxy on the dev box:
  `ns=2;s=p7-smoke-galaxy/lab-floor/galaxy-line/reactor-1/Source` reads
  return Status=0x00000000 with a real Galaxy byte-array value.

Test suite: 195/195 Core.Tests + 283/283 Server.Tests green. Five new
ExtractFullName / FullName-passthrough tests added.

Task #112 GA-3 — golden-path read verified end-to-end; remaining E2E
script stages still blocked on pre-existing issues (ScriptedAlarm
predicate NRE on empty upstream cache, PowerShell $changeLines.Count
guard), tracked separately.
Task #134 — complete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:57:20 -04:00
Joseph Doherty
5fc596a9a1 E2E test script — Galaxy (MXAccess) driver: read / write / subscribe / alarms / history
Seven-stage e2e script covering every Galaxy-specific capability surface:
IReadable + IWritable + ISubscribable + IAlarmSource + IHistoryProvider.
Unlike the other drivers there is no per-protocol CLI — Galaxy's proxy
lives in-process with the server + talks to OtOpcUaGalaxyHost over a
named pipe (MXAccess COM is 32-bit-only), so every stage runs through
`otopcua-cli` against the published OPC UA address space.

## Stages

1. Probe                   — otopcua-cli read on the source NodeId
2. Source read             — capture value for downstream comparison
3. Virtual-tag bridge      — Phase 7 VirtualTag (source × 2) through
                             CachedTagUpstreamSource
4. Subscribe-sees-change   — data-change events propagate
5. Reverse bridge          — opc-ua write → Galaxy; soft-passes if the
                             attribute's Galaxy-side ACL forbids writes
                             (`BadUserAccessDenied` / `BadNotWritable`)
6. Alarm fires             — scripted-alarm Condition fires with Active
                             state when source crosses threshold
7. History read            — historyread returns samples from the Aveva
                             Historian → IHistoryProvider path

## Two new helpers in _common.ps1

- `Test-AlarmFiresOnThreshold` — start `otopcua-cli alarms --refresh`
  in the background on a Condition NodeId, drive the source change,
  assert captured stdout contains `ALARM` + `Active`. Uses the same
  Start-Process + temp-file pattern as `Test-SubscribeSeesChange` since
  the alarms command runs until Ctrl+C (no built-in --duration).
- `Test-HistoryHasSamples` — call `otopcua-cli historyread` over a
  configurable lookback window, parse `N values returned.` marker, fail
  if below MinSamples. Works for driver-sourced, virtual, or scripted-
  alarm historized nodes.

## Wiring

- `test-all.ps1` picks up the optional `galaxy` sidecar section and
  runs the script with the configured NodeIds + wait windows.
- `e2e-config.sample.json` adds a `galaxy` section seeded with the
  Phase 7 defaults (`p7-smoke-tag-source` / `-vt-derived` /
  `-al-overtemp`) — matches `scripts/smoke/seed-phase-7-smoke.sql`.
- `scripts/e2e/README.md` expected-matrix gains a Galaxy row.

## Prereqs

- OtOpcUaGalaxyHost running (NSSM-wrapped) with the Galaxy + MXAccess
  runtime available
- `seed-phase-7-smoke.sql` applied with a live Galaxy attribute
  substituted into `dbo.Tag.TagConfig`
- OtOpcUa server running against the `p7-smoke` cluster
- Non-elevated shell (Galaxy.Host pipe ACL denies Admins)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 12:59:06 -04:00