fix(scripts): legacy WriteBatchAndWaitAsync waits for the run's own reset echo (ScadaBridge#36)

When the run reset the response flag via SetAttribute before the six-argument
handshake, wait for that reset to echo (shared timeout) before writing batch +
trigger, so the previous run's cached CompleteFlag can't satisfy it. Covers the
unported DB-owned MES/receiver scripts. Pending reset consumed per handshake.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-10 04:38:05 -05:00
parent 08fe5acfe4
commit 545d3e8ba0
3 changed files with 216 additions and 5 deletions
@@ -212,3 +212,32 @@ fast-path, and the trigger was not written until the device echoed the reset 751
response wait is dominated by the Device script's own two data writes plus the subscription
round-trip. The optional negative control (planting a stale `True` with the OPC UA tool) was
skipped: `infra/tools/opcua_tool.py` needs the `asyncua` module, which is not installed here.
## 11. Addendum 2026-11-10 — implicit reset echo on the six-argument overload
The database-owned MES/Delmia scripts (13, 14, 36; receiver scripts 15, 16, 25, 26, 37, 38) were
never ported onto the nine-argument overload (§9 / register row 119), so every unported run still
had the §1 race. §9's "existing scripts behave identically" is therefore relaxed for **exactly
one** shape — the defect itself:
- `ScriptRuntimeContext` records each successful `SetAttribute` of the run (scope-resolved name →
encoded value, last write wins).
- The six-argument `WriteBatchAndWait`, when THIS run wrote `responseKey` to a value other than
`responseValue` (the script's own `CompleteFlag = false` reset), first waits event-driven for
that value to echo and only then writes batch + trigger — the same ordering §2 requires and the
reset-aware overload enforces. The pending write is consumed by that handshake (and by the
reset-aware overload's reset leg), so a second handshake in the same run without a new reset is
unchanged.
- The echo wait **shares** the call's `timeout` with the response wait (unlike the additive
nine-argument form), so an unported script can never run past the deadline it was sized for; a
reset that never echoes returns `false` with nothing written and logs a warning.
- A call with no prior reset write in the run, and the edge-triggered question of §2, are
unchanged. `WaitAsync` used as a handshake is NOT gated: there the trigger is written before the
wait, so a device flipping inside one sampling interval would never report the reset (§2).
Tests: `HandshakeResetEchoTests` — `LegacyOverload_AfterTheScriptsOwnReset_*` (held until the
echo / no echo ⇒ false and no batch), `LegacyOverload_ResetIsConsumedByTheHandshakeThatObservedIt`,
`ResetAwareOverload_ConsumesItsReset_SoALaterLegacyHandshakeIsNotGated`; the cached-true
legacy test still passes unchanged (no reset in that run). Porting the scripts to the nine-argument
overload is still recommended (phase-aware result, explicit reset timeout) but is no longer needed
to close the race; dropping their `Task.Delay(1000)` remains a script edit.