Phase 3 PR 40 — LiveStack write + subscribe tests against TestMachine_001 #39

Merged
dohertj2 merged 1 commits from phase-3-pr40-livestack-write-subscribe into v2 2026-04-18 19:41:56 -04:00
Owner

Closes the code side of LMX #5's two final deferred facts: end-to-end write-roundtrip and end-to-end subscribe-receives-OnDataChange against the live Galaxy through the just-installed OtOpcUaGalaxyHost service.

Target attribute

Nominated by the user: any tag from TestMachine_001. Direct ZB query found DelmiaReceiver_001.TestAttribute — a writable Boolean UDA on the TestMachine_001 hierarchy whose name announces it's intended for exactly this. mx_data_type=1 (Boolean), lock_type=0 (writable), security_classification=1 (Operate-tier writable).

New facts

  • Write_then_read_roundtrips_a_writable_Boolean_attribute_on_TestMachine_001 — read baseline, invert, WriteAsync, poll ReadAsync in a 5s scan-window loop until the new value comes back, restore on finally.
  • Subscribe_fires_OnDataChange_with_initial_value_then_again_after_a_write — subscribe with 250ms publishing interval, drain the initial-value notification onto a ConcurrentQueue (MXAccess advisory fires on its own thread — must not block it), record baseline depth, write the toggled value, wait up to 8s for the post-write notification, search the queue tail for the matching value. Unsubscribe + restore baseline on finally.

Design notes

  • Polling vs single read — Galaxy's Engine scan latency varies on a fresh service start; a single read after a fixed delay would be flaky. Both tests poll within a budget (5s read, 8s notification).
  • Restore-on-finally — Galaxy holds UDA values across runs since they're deployed objects. Restore keeps re-runs idempotent. Best-effort (swallows exceptions) so restore failure doesn't mask primary assertion.
  • Convert.ToBoolean(value ?? false) — defensive against the boxed-vs-typed quirk in MessagePack-deserialized Galaxy values. Same pattern OnReadValue uses.
  • Tail search — the initial-value notification may appear multiple times before the write commits; checking the queue tail finds the post-write delta even when the queue grew during the wait.

Run from a non-elevated shell

$env:OTOPCUA_GALAXY_SECRET = Get-Content C:\Users\dohertj2\Desktop\lmxopcua\.local\galaxy-host-secret.txt
cd C:\Users\dohertj2\Desktop\lmxopcua
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests --filter "FullyQualifiedName~LiveStackSmokeTests"

Expected: 7/7 pass against the running OtOpcUaGalaxyHost service.

Test posture

  • LiveStackSmokeTests: 7 facts discovered (was 5; +2 new). All skip cleanly with the elevation message under bash + sc.exe context (verified). Build clean.
  • lmx-followups.md #5 updated with the run command + the one remaining genuine follow-up (alarm-condition fact when an alarm-flagged attribute exists on TestMachine_001).
Closes the code side of LMX #5's two final deferred facts: end-to-end write-roundtrip and end-to-end subscribe-receives-OnDataChange against the live Galaxy through the just-installed `OtOpcUaGalaxyHost` service. ## Target attribute Nominated by the user: any tag from `TestMachine_001`. Direct ZB query found `DelmiaReceiver_001.TestAttribute` — a writable Boolean UDA on the `TestMachine_001` hierarchy whose name announces it's intended for exactly this. `mx_data_type=1` (Boolean), `lock_type=0` (writable), `security_classification=1` (Operate-tier writable). ## New facts - **`Write_then_read_roundtrips_a_writable_Boolean_attribute_on_TestMachine_001`** — read baseline, invert, `WriteAsync`, poll `ReadAsync` in a 5s scan-window loop until the new value comes back, restore on finally. - **`Subscribe_fires_OnDataChange_with_initial_value_then_again_after_a_write`** — subscribe with 250ms publishing interval, drain the initial-value notification onto a `ConcurrentQueue` (MXAccess advisory fires on its own thread — must not block it), record baseline depth, write the toggled value, wait up to 8s for the post-write notification, search the queue tail for the matching value. Unsubscribe + restore baseline on finally. ## Design notes - **Polling vs single read** — Galaxy's Engine scan latency varies on a fresh service start; a single read after a fixed delay would be flaky. Both tests poll within a budget (5s read, 8s notification). - **Restore-on-finally** — Galaxy holds UDA values across runs since they're deployed objects. Restore keeps re-runs idempotent. Best-effort (swallows exceptions) so restore failure doesn't mask primary assertion. - **`Convert.ToBoolean(value ?? false)`** — defensive against the boxed-vs-typed quirk in MessagePack-deserialized Galaxy values. Same pattern `OnReadValue` uses. - **Tail search** — the initial-value notification may appear multiple times before the write commits; checking the queue tail finds the post-write delta even when the queue grew during the wait. ## Run from a non-elevated shell ```powershell $env:OTOPCUA_GALAXY_SECRET = Get-Content C:\Users\dohertj2\Desktop\lmxopcua\.local\galaxy-host-secret.txt cd C:\Users\dohertj2\Desktop\lmxopcua dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests --filter "FullyQualifiedName~LiveStackSmokeTests" ``` Expected: **7/7 pass** against the running `OtOpcUaGalaxyHost` service. ## Test posture - LiveStackSmokeTests: 7 facts discovered (was 5; +2 new). All skip cleanly with the elevation message under bash + sc.exe context (verified). Build clean. - `lmx-followups.md` #5 updated with the run command + the one remaining genuine follow-up (alarm-condition fact when an alarm-flagged attribute exists on `TestMachine_001`).
dohertj2 added 1 commit 2026-04-18 19:41:53 -04:00
Two new facts target DelmiaReceiver_001.TestAttribute — the writable Boolean UDA on the TestMachine_001 hierarchy in this dev Galaxy. The user nominated TestMachine_001 (the deployed test-target object) as a scratch surface for live testing; ZB query showed DelmiaReceiver_001 carries one dynamic_attribute named TestAttribute (mx_data_type=1=Boolean, lock_type=0=writable, security_classification=1=Operate). Naming makes the intent obvious — the attribute exists for exactly this kind of integration testing — and Boolean keeps the assertions simple (invert, write, read back).
Write_then_read_roundtrips_a_writable_Boolean_attribute_on_TestMachine_001: reads the current value as the baseline (Galaxy may return Uncertain quality until the Engine has scanned the attribute at least once — we don't read into a typed bool until Status is Good), inverts it, writes via IWritable, then polls reads in a 5s loop until either the new value comes back or the budget expires. The scan-window poll (rather than a single read after a fixed delay) accommodates Galaxy's variable scan latency on a fresh service start. Restore-on-finally writes the original value back so re-running the test doesn't accumulate a flipped TestAttribute on the dev box (Galaxy holds UDA values across runs since they're deployed). Best-effort restore — swallows exceptions so a failure in restore doesn't mask the primary assertion.
Subscribe_fires_OnDataChange_with_initial_value_then_again_after_a_write: subscribes to the same attribute with a 250ms publishing interval, captures every OnDataChange notification onto a thread-safe ConcurrentQueue (MXAccess advisory fires on its own thread per Galaxy's COM apartment model — must not block it), waits up to 5s for the initial-value callback (per ISubscribable's contract: 'driver MAY fire OnDataChange immediately with the current value'), records the queue depth as a baseline, writes the toggled value, waits up to 8s for at least one MORE notification, then searches the queue tail for the notification carrying the toggled value (initial value may appear multiple times before the write commits — looking at the tail finds the post-write delta even if the queue grew during the wait window). Unsubscribes on finally + restores baseline.
Both tests use Convert.ToBoolean(value ?? false) to defensively handle the Boxed-vs-typed quirk in MessagePack-deserialized Galaxy values — depending on the wire encoding the Boolean might come back as System.Boolean or System.Object boxing one. Convert.ToBoolean handles both. Same pattern in OnReadValue's existing usage.
WaitForAsync helper does the loop+budget pattern shared by both tests.
PR 40 is the code side of LMX #5's final two deferred facts. To actually run them green requires re-executing from a normal (non-admin) PowerShell — the elevated-shell skip from PR 39 fires correctly under bash + sc.exe-context (verified). lmx-followups.md #5 updated to note the new facts + the run command + the one remaining genuine follow-up (alarm-condition fact when an alarm-flagged attribute is deployed on TestMachine_001).
Test posture from elevated bash: 7 LiveStackSmokeTests facts discovered (was 5; +2 new), all skip cleanly with the elevation message. Build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dohertj2 merged commit af35fac0ef into v2 2026-04-18 19:41:56 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#39