diff --git a/.changeset/QFWTPHO.md b/.changeset/QFWTPHO.md new file mode 100644 index 0000000..8760f6d --- /dev/null +++ b/.changeset/QFWTPHO.md @@ -0,0 +1,5 @@ +--- +'@ocpp-debugkit/toolkit': patch +--- + +feat(scenarios): add firmware-update-failure scenario diff --git a/README.md b/README.md index 46b71d3..091384b 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ validate behavior against known scenarios. firmware update failures, suspicious session duration, slow CSMS responses, heartbeat interval violations, meter value anomalies, unresponsive CSMS, and repeated boot notifications. -- **Scenario Evaluator** — 16 predefined scenarios with expected failure +- **Scenario Evaluator** — 17 predefined scenarios with expected failure outcomes for testing the analysis engine. Supports external scenario files. - **Replay Engine** — Deterministic, pure replay engine with step forward/back, jump-to-event, and configurable playback speed. diff --git a/packages/toolkit/README.md b/packages/toolkit/README.md index 5c3c3e3..e01efd6 100644 --- a/packages/toolkit/README.md +++ b/packages/toolkit/README.md @@ -20,7 +20,7 @@ report generation, React components, and CLI. firmware update failures, suspicious session duration, slow CSMS responses, heartbeat interval violations, meter value anomalies, unresponsive CSMS, and repeated boot notifications. -- **Scenario Evaluator** — 16 predefined scenarios with expected failure +- **Scenario Evaluator** — 17 predefined scenarios with expected failure outcomes for testing the analysis engine. Supports external scenario files. - **Replay Engine** — Deterministic, pure replay engine with step forward/back, jump-to-event, and configurable playback speed. No timers or I/O. diff --git a/packages/toolkit/src/scenarios/__scenarios__/firmware-update-failure.ts b/packages/toolkit/src/scenarios/__scenarios__/firmware-update-failure.ts new file mode 100644 index 0000000..bad0631 --- /dev/null +++ b/packages/toolkit/src/scenarios/__scenarios__/firmware-update-failure.ts @@ -0,0 +1,77 @@ +export default { + name: 'firmware-update-failure', + description: + 'Firmware update failure: station boots, firmware download initiates but install fails with InstallFailed status. Expects FIRMWARE_UPDATE_FAILURE failure.', + trace: { + traceId: 'scenario-firmware-update-failure', + metadata: { + stationId: 'CS-SYNTHETIC-019', + ocppVersion: '1.6', + source: 'synthetic-scenario', + description: + 'Station boots, reports Downloading firmware, then InstallFailed within seconds, followed by a heartbeat to confirm station is still online.', + }, + events: [ + { + timestamp: '2026-01-15T07:00:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-001', + 'BootNotification', + { + chargePointVendor: 'SyntheticVendor', + chargePointModel: 'SM-100', + chargePointSerialNumber: 'CS-SYNTHETIC-019', + firmwareVersion: '1.0.0', + }, + ], + }, + { + timestamp: '2026-01-15T07:00:00.500Z', + direction: 'CSMS_TO_CS', + message: [ + 3, + 'msg-001', + { + currentTime: '2026-01-15T07:00:00.500Z', + interval: 300, + status: 'Accepted', + }, + ], + }, + { + timestamp: '2026-01-15T07:00:30.000Z', + direction: 'CS_TO_CSMS', + message: [2, 'msg-002', 'FirmwareStatusNotification', { status: 'Downloading' }], + }, + { + timestamp: '2026-01-15T07:00:30.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-002', {}], + }, + { + timestamp: '2026-01-15T07:01:00.000Z', + direction: 'CS_TO_CSMS', + message: [2, 'msg-003', 'FirmwareStatusNotification', { status: 'InstallFailed' }], + }, + { + timestamp: '2026-01-15T07:01:00.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-003', {}], + }, + { + timestamp: '2026-01-15T07:02:00.000Z', + direction: 'CS_TO_CSMS', + message: [2, 'msg-hb-1', 'Heartbeat', {}], + }, + { + timestamp: '2026-01-15T07:02:00.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-hb-1', { currentTime: '2026-01-15T07:02:00.500Z' }], + }, + ], + }, + expectedFailures: ['FIRMWARE_UPDATE_FAILURE'], + assertions: [{ type: 'failure_count', params: { code: 'FIRMWARE_UPDATE_FAILURE', min: 1 } }], +}; diff --git a/packages/toolkit/src/scenarios/index.test.ts b/packages/toolkit/src/scenarios/index.test.ts index dcf3324..2b85c28 100644 --- a/packages/toolkit/src/scenarios/index.test.ts +++ b/packages/toolkit/src/scenarios/index.test.ts @@ -19,6 +19,7 @@ import { heartbeatIrregularScenario, unresponsiveCsmsScenario, firmwareUpdateSuccessScenario, + firmwareUpdateFailureScenario, } from './index.js'; import { parseTrace, buildSessionTimeline, detectFailures } from '../core/index.js'; @@ -27,8 +28,8 @@ import { parseTrace, buildSessionTimeline, detectFailures } from '../core/index. // --------------------------------------------------------------------------- describe('scenario registry', () => { - it('exports exactly 16 scenarios', () => { - expect(scenarios).toHaveLength(16); + it('exports exactly 17 scenarios', () => { + expect(scenarios).toHaveLength(17); }); it('exports scenario names in order', () => { @@ -49,6 +50,7 @@ describe('scenario registry', () => { 'heartbeat-irregular', 'unresponsive-csms', 'firmware-update-success', + 'firmware-update-failure', ]); }); @@ -78,6 +80,7 @@ describe('scenario registry', () => { expect(getScenario('heartbeat-irregular')).toBe(heartbeatIrregularScenario); expect(getScenario('unresponsive-csms')).toBe(unresponsiveCsmsScenario); expect(getScenario('firmware-update-success')).toBe(firmwareUpdateSuccessScenario); + expect(getScenario('firmware-update-failure')).toBe(firmwareUpdateFailureScenario); }); it('getScenario returns undefined for unknown name', () => { @@ -202,6 +205,14 @@ describe('scenario engine integration', () => { const failures = detectFailures(result.events, sessions); expect(failures.some((f) => f.code === 'DIAGNOSTICS_FAILURE')).toBe(true); }); + + it('firmware-update-failure: detects FIRMWARE_UPDATE_FAILURE', () => { + const trace = JSON.stringify(firmwareUpdateFailureScenario.trace); + const result = parseTrace(trace); + const sessions = buildSessionTimeline(result.events); + const failures = detectFailures(result.events, sessions); + expect(failures.some((f) => f.code === 'FIRMWARE_UPDATE_FAILURE')).toBe(true); + }); }); // --------------------------------------------------------------------------- diff --git a/packages/toolkit/src/scenarios/index.ts b/packages/toolkit/src/scenarios/index.ts index 7fd231b..6181adb 100644 --- a/packages/toolkit/src/scenarios/index.ts +++ b/packages/toolkit/src/scenarios/index.ts @@ -21,6 +21,7 @@ import shortSession from './__scenarios__/short-session.js'; import heartbeatIrregular from './__scenarios__/heartbeat-irregular.js'; import unresponsiveCsms from './__scenarios__/unresponsive-csms.js'; import firmwareUpdateSuccess from './__scenarios__/firmware-update-success.js'; +import firmwareUpdateFailure from './__scenarios__/firmware-update-failure.js'; // --------------------------------------------------------------------------- // Scenarios derived from core fixtures @@ -68,6 +69,7 @@ const shortSessionScenario: Scenario = shortSession as unknown as Scenario; const heartbeatIrregularScenario: Scenario = heartbeatIrregular as unknown as Scenario; const unresponsiveCsmsScenario: Scenario = unresponsiveCsms as unknown as Scenario; const firmwareUpdateSuccessScenario: Scenario = firmwareUpdateSuccess as unknown as Scenario; +const firmwareUpdateFailureScenario: Scenario = firmwareUpdateFailure as unknown as Scenario; // --------------------------------------------------------------------------- // Registry @@ -90,6 +92,7 @@ export const scenarios = [ heartbeatIrregularScenario, unresponsiveCsmsScenario, firmwareUpdateSuccessScenario, + firmwareUpdateFailureScenario, ] as const; export const scenarioNames = [ @@ -109,6 +112,7 @@ export const scenarioNames = [ 'heartbeat-irregular', 'unresponsive-csms', 'firmware-update-success', + 'firmware-update-failure', ] as const; export { @@ -128,6 +132,7 @@ export { heartbeatIrregularScenario, unresponsiveCsmsScenario, firmwareUpdateSuccessScenario, + firmwareUpdateFailureScenario, }; export { compareScenarioReports } from './compare.js'; diff --git a/tests/external-fixture/test.mjs b/tests/external-fixture/test.mjs index 3b64809..08896c7 100644 --- a/tests/external-fixture/test.mjs +++ b/tests/external-fixture/test.mjs @@ -158,8 +158,8 @@ const scenarios = await import('@ocpp-debugkit/toolkit/scenarios'); assert(Array.isArray(scenarios.scenarios), 'scenarios is an array'); assert( - scenarios.scenarios.length === 16, - `16 scenarios exported (got ${scenarios.scenarios.length})`, + scenarios.scenarios.length === 17, + `17 scenarios exported (got ${scenarios.scenarios.length})`, ); assert(typeof scenarios.getScenario === 'function', 'getScenario is a function'); assert(scenarios.getScenario('normal-session') !== undefined, 'normal-session scenario exists');