Skip to content

Commit 8fe2185

Browse files
feat(scenarios): add firmware-update-failure scenario (#147)
Adds a `firmware-update-failure` scenario: a station boots, reports the firmware `Downloading`, then `InstallFailed`, followed by a heartbeat. `expectedFailures` is `['FIRMWARE_UPDATE_FAILURE']`, the failing counterpart to the firmware-update-success scenario from #133. The rule treats `DownloadFailed`, `DownloadPaused`, `InstallFailed` and `InstallRebootingFailed` as failures, and this covers the `InstallFailed` path. Takes the scenario registry to 17, updating the count in the registry test, the external fixture test, and both READMEs. The detection-rule count stays 16, since a scenario is not a rule. Closes #138
1 parent ab968f0 commit 8fe2185

7 files changed

Lines changed: 104 additions & 6 deletions

File tree

‎.changeset/QFWTPHO.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ocpp-debugkit/toolkit': patch
3+
---
4+
5+
feat(scenarios): add firmware-update-failure scenario

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ validate behavior against known scenarios.
2222
firmware update failures, suspicious session duration, slow CSMS responses,
2323
heartbeat interval violations, meter value anomalies, unresponsive CSMS, and
2424
repeated boot notifications.
25-
- **Scenario Evaluator** — 16 predefined scenarios with expected failure
25+
- **Scenario Evaluator** — 17 predefined scenarios with expected failure
2626
outcomes for testing the analysis engine. Supports external scenario files.
2727
- **Replay Engine** — Deterministic, pure replay engine with step forward/back,
2828
jump-to-event, and configurable playback speed.

‎packages/toolkit/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ report generation, React components, and CLI.
2020
firmware update failures, suspicious session duration, slow CSMS responses,
2121
heartbeat interval violations, meter value anomalies, unresponsive CSMS, and
2222
repeated boot notifications.
23-
- **Scenario Evaluator** — 16 predefined scenarios with expected failure
23+
- **Scenario Evaluator** — 17 predefined scenarios with expected failure
2424
outcomes for testing the analysis engine. Supports external scenario files.
2525
- **Replay Engine** — Deterministic, pure replay engine with step forward/back,
2626
jump-to-event, and configurable playback speed. No timers or I/O.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
export default {
2+
name: 'firmware-update-failure',
3+
description:
4+
'Firmware update failure: station boots, firmware download initiates but install fails with InstallFailed status. Expects FIRMWARE_UPDATE_FAILURE failure.',
5+
trace: {
6+
traceId: 'scenario-firmware-update-failure',
7+
metadata: {
8+
stationId: 'CS-SYNTHETIC-019',
9+
ocppVersion: '1.6',
10+
source: 'synthetic-scenario',
11+
description:
12+
'Station boots, reports Downloading firmware, then InstallFailed within seconds, followed by a heartbeat to confirm station is still online.',
13+
},
14+
events: [
15+
{
16+
timestamp: '2026-01-15T07:00:00.000Z',
17+
direction: 'CS_TO_CSMS',
18+
message: [
19+
2,
20+
'msg-001',
21+
'BootNotification',
22+
{
23+
chargePointVendor: 'SyntheticVendor',
24+
chargePointModel: 'SM-100',
25+
chargePointSerialNumber: 'CS-SYNTHETIC-019',
26+
firmwareVersion: '1.0.0',
27+
},
28+
],
29+
},
30+
{
31+
timestamp: '2026-01-15T07:00:00.500Z',
32+
direction: 'CSMS_TO_CS',
33+
message: [
34+
3,
35+
'msg-001',
36+
{
37+
currentTime: '2026-01-15T07:00:00.500Z',
38+
interval: 300,
39+
status: 'Accepted',
40+
},
41+
],
42+
},
43+
{
44+
timestamp: '2026-01-15T07:00:30.000Z',
45+
direction: 'CS_TO_CSMS',
46+
message: [2, 'msg-002', 'FirmwareStatusNotification', { status: 'Downloading' }],
47+
},
48+
{
49+
timestamp: '2026-01-15T07:00:30.500Z',
50+
direction: 'CSMS_TO_CS',
51+
message: [3, 'msg-002', {}],
52+
},
53+
{
54+
timestamp: '2026-01-15T07:01:00.000Z',
55+
direction: 'CS_TO_CSMS',
56+
message: [2, 'msg-003', 'FirmwareStatusNotification', { status: 'InstallFailed' }],
57+
},
58+
{
59+
timestamp: '2026-01-15T07:01:00.500Z',
60+
direction: 'CSMS_TO_CS',
61+
message: [3, 'msg-003', {}],
62+
},
63+
{
64+
timestamp: '2026-01-15T07:02:00.000Z',
65+
direction: 'CS_TO_CSMS',
66+
message: [2, 'msg-hb-1', 'Heartbeat', {}],
67+
},
68+
{
69+
timestamp: '2026-01-15T07:02:00.500Z',
70+
direction: 'CSMS_TO_CS',
71+
message: [3, 'msg-hb-1', { currentTime: '2026-01-15T07:02:00.500Z' }],
72+
},
73+
],
74+
},
75+
expectedFailures: ['FIRMWARE_UPDATE_FAILURE'],
76+
assertions: [{ type: 'failure_count', params: { code: 'FIRMWARE_UPDATE_FAILURE', min: 1 } }],
77+
};

‎packages/toolkit/src/scenarios/index.test.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
heartbeatIrregularScenario,
2020
unresponsiveCsmsScenario,
2121
firmwareUpdateSuccessScenario,
22+
firmwareUpdateFailureScenario,
2223
} from './index.js';
2324
import { parseTrace, buildSessionTimeline, detectFailures } from '../core/index.js';
2425

@@ -27,8 +28,8 @@ import { parseTrace, buildSessionTimeline, detectFailures } from '../core/index.
2728
// ---------------------------------------------------------------------------
2829

2930
describe('scenario registry', () => {
30-
it('exports exactly 16 scenarios', () => {
31-
expect(scenarios).toHaveLength(16);
31+
it('exports exactly 17 scenarios', () => {
32+
expect(scenarios).toHaveLength(17);
3233
});
3334

3435
it('exports scenario names in order', () => {
@@ -49,6 +50,7 @@ describe('scenario registry', () => {
4950
'heartbeat-irregular',
5051
'unresponsive-csms',
5152
'firmware-update-success',
53+
'firmware-update-failure',
5254
]);
5355
});
5456

@@ -78,6 +80,7 @@ describe('scenario registry', () => {
7880
expect(getScenario('heartbeat-irregular')).toBe(heartbeatIrregularScenario);
7981
expect(getScenario('unresponsive-csms')).toBe(unresponsiveCsmsScenario);
8082
expect(getScenario('firmware-update-success')).toBe(firmwareUpdateSuccessScenario);
83+
expect(getScenario('firmware-update-failure')).toBe(firmwareUpdateFailureScenario);
8184
});
8285

8386
it('getScenario returns undefined for unknown name', () => {
@@ -202,6 +205,14 @@ describe('scenario engine integration', () => {
202205
const failures = detectFailures(result.events, sessions);
203206
expect(failures.some((f) => f.code === 'DIAGNOSTICS_FAILURE')).toBe(true);
204207
});
208+
209+
it('firmware-update-failure: detects FIRMWARE_UPDATE_FAILURE', () => {
210+
const trace = JSON.stringify(firmwareUpdateFailureScenario.trace);
211+
const result = parseTrace(trace);
212+
const sessions = buildSessionTimeline(result.events);
213+
const failures = detectFailures(result.events, sessions);
214+
expect(failures.some((f) => f.code === 'FIRMWARE_UPDATE_FAILURE')).toBe(true);
215+
});
205216
});
206217

207218
// ---------------------------------------------------------------------------

‎packages/toolkit/src/scenarios/index.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import shortSession from './__scenarios__/short-session.js';
2121
import heartbeatIrregular from './__scenarios__/heartbeat-irregular.js';
2222
import unresponsiveCsms from './__scenarios__/unresponsive-csms.js';
2323
import firmwareUpdateSuccess from './__scenarios__/firmware-update-success.js';
24+
import firmwareUpdateFailure from './__scenarios__/firmware-update-failure.js';
2425

2526
// ---------------------------------------------------------------------------
2627
// Scenarios derived from core fixtures
@@ -68,6 +69,7 @@ const shortSessionScenario: Scenario = shortSession as unknown as Scenario;
6869
const heartbeatIrregularScenario: Scenario = heartbeatIrregular as unknown as Scenario;
6970
const unresponsiveCsmsScenario: Scenario = unresponsiveCsms as unknown as Scenario;
7071
const firmwareUpdateSuccessScenario: Scenario = firmwareUpdateSuccess as unknown as Scenario;
72+
const firmwareUpdateFailureScenario: Scenario = firmwareUpdateFailure as unknown as Scenario;
7173

7274
// ---------------------------------------------------------------------------
7375
// Registry
@@ -90,6 +92,7 @@ export const scenarios = [
9092
heartbeatIrregularScenario,
9193
unresponsiveCsmsScenario,
9294
firmwareUpdateSuccessScenario,
95+
firmwareUpdateFailureScenario,
9396
] as const;
9497

9598
export const scenarioNames = [
@@ -109,6 +112,7 @@ export const scenarioNames = [
109112
'heartbeat-irregular',
110113
'unresponsive-csms',
111114
'firmware-update-success',
115+
'firmware-update-failure',
112116
] as const;
113117

114118
export {
@@ -128,6 +132,7 @@ export {
128132
heartbeatIrregularScenario,
129133
unresponsiveCsmsScenario,
130134
firmwareUpdateSuccessScenario,
135+
firmwareUpdateFailureScenario,
131136
};
132137

133138
export { compareScenarioReports } from './compare.js';

‎tests/external-fixture/test.mjs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,8 @@ const scenarios = await import('@ocpp-debugkit/toolkit/scenarios');
158158

159159
assert(Array.isArray(scenarios.scenarios), 'scenarios is an array');
160160
assert(
161-
scenarios.scenarios.length === 16,
162-
`16 scenarios exported (got ${scenarios.scenarios.length})`,
161+
scenarios.scenarios.length === 17,
162+
`17 scenarios exported (got ${scenarios.scenarios.length})`,
163163
);
164164
assert(typeof scenarios.getScenario === 'function', 'getScenario is a function');
165165
assert(scenarios.getScenario('normal-session') !== undefined, 'normal-session scenario exists');

0 commit comments

Comments
 (0)