]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commit
fix(ocpp): reset stationInfo.firmwareStatus on abort/exception in simulateFirmwareUpd...
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Wed, 8 Jul 2026 12:46:10 +0000 (14:46 +0200)
committerGitHub <noreply@github.com>
Wed, 8 Jul 2026 12:46:10 +0000 (14:46 +0200)
commit4f959e9f8a939b309b0d74650a07e96abfba2860
tree6d812cac12a6aaeb985b94de39f4c9c0e7155bd5
parent56479e44db4a670d9bcffbfb4ce351c5048f50d5
fix(ocpp): reset stationInfo.firmwareStatus on abort/exception in simulateFirmwareUpdateLifecycle (#1976)

simulateFirmwareUpdateLifecycle wrote stationInfo.firmwareStatus at every
status transition via sendFirmwareStatusNotification, but its finally block
only cleared the per-station WeakMap state (activeFirmwareUpdateAbortController,
activeFirmwareUpdateRequestId). On exception or abort (supersession, stop()),
stationInfo.firmwareStatus stayed at the last-emitted non-terminal value
(e.g. 'Downloading'), leaving a latent data-model split any future
consumer that does not cross-check activeFirmwareUpdateRequestId would
observe.

Track the current lifecycle stage inside the lifecycle scope and, in the
existing finally, reset stationInfo.firmwareStatus in-place when the
lifecycle did not reach Installed. Per OCPP 2.0.1 FirmwareStatusEnumType,
Idle SHALL only be used in TriggerMessage-triggered notifications, so as
a persistent local state Idle is only valid before install starts:
  - clean download-phase abort              -> Idle
  - clean install-phase abort               -> InstallationFailed
  - exception during download stage         -> DownloadFailed
  - exception during install stage          -> InstallationFailed

Per OCPP 2.0.1 L01.FR.24 Note the terminal *Failed FirmwareStatusNotification
on supersession is optional; L02.FR.15 Note omits any such clause. Emitting
no FirmwareStatusNotification from finally is safe under both profiles.
The field is reset in-place; no FirmwareStatusNotification is emitted
from finally, so the simulator does not race the caller driving the
supersession.

Stage transitions are placed only after successful advancement (after the
Installing / Installed notification awaits resolve), never on error/abort
branches, so the terminal-value selection in finally is unambiguous. The
finally guards mirror clearActiveFirmwareUpdate's requestId-supersession
pattern to avoid clobbering a superseder's live status, and preserve any
explicit terminal already emitted from inside try (DownloadFailed /
InvalidSignature / InstallationFailed).

Stage->terminal mapping uses `Record<Exclude<FirmwareStage, 'installed'>,
OCPP20FirmwareStatusEnumType>` with `as const satisfies` for
compile-time exhaustiveness over the failure-eligible stages. Naming
convention (SCREAMING_SNAKE_CASE) matches CoherentMeterValueBuilder's
PHASE_FAMILY/PHASE_RANK pattern for private module-scope Record lookup
tables.

Add OCPP20VariableManager.getInstance().resetRuntimeOverrides() to the
shared standardCleanup test helper so per-station variable overrides do
not leak between tests (all mock stations share a hardcoded hashId).

Add 8 tests covering: supersession mid-download -> Idle, supersession
mid-install -> InstallationFailed, throw during download -> DownloadFailed,
throw during install -> InstallationFailed, happy path preserved -> Installed,
supersession-with-emit race (T2 launched via constructor listener) ->
Downloading preserved, InvalidSignature preserved, retries-exhausted
DownloadFailed preserved.

Closes #1969
src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-UpdateFirmware.test.ts
tests/helpers/TestLifecycleHelpers.ts