Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/QFWTPHO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ocpp-debugkit/toolkit': patch
---

feat(scenarios): add firmware-update-failure scenario
5 changes: 5 additions & 0 deletions .changeset/warm-dragons-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ocpp-debugkit/toolkit": minor
---

feat(scenarios): add firmware update success scenario
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ validate behavior against known scenarios.

- **Trace Inspector** — Load OCPP traces, view session timelines, inspect
individual messages, and identify failures.
- **Failure Detection** — 16 detection rules (4 critical, 10 warning, 2 info)
- **Failure Detection** — 17 detection rules (4 critical, 10 warning, 2 info)
covering common failure patterns: failed authorization, connector faults,
station offline, heartbeat timeout, meter value gaps, invalid stop reasons,
unexpected starts, status transition violations, diagnostics failures,
firmware update failures, suspicious session duration, slow CSMS responses,
heartbeat interval violations, meter value anomalies, unresponsive CSMS, and
repeated boot notifications.
- **Scenario Evaluator** — 15 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.
Expand Down
4 changes: 2 additions & 2 deletions packages/toolkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ report generation, React components, and CLI.
`parseOpenOcppTrace()` (with `parseTrace()` auto-detection) reads it,
`toOpenOcppTraceJsonl()` and the `convert` CLI command emit it, both checked
against the specification's conformance fixtures.
- **Failure Detection** — 16 detection rules (4 critical, 10 warning, 2 info)
- **Failure Detection** — 17 detection rules (4 critical, 10 warning, 2 info)
covering common failure patterns: failed authorization, connector faults,
station offline, heartbeat timeout, meter value gaps, invalid stop reasons,
unexpected starts, status transition violations, diagnostics failures,
firmware update failures, suspicious session duration, slow CSMS responses,
heartbeat interval violations, meter value anomalies, unresponsive CSMS, and
repeated boot notifications.
- **Scenario Evaluator** — 15 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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 } },
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
export default {
name: 'firmware-update-success',
description:
'Successful firmware update: station boots, downloads firmware, installs it successfully, and continues operating normally. No failures expected.',
trace: {
traceId: 'scenario-firmware-update-success',
metadata: {
stationId: 'CS-SYNTHETIC-016',
ocppVersion: '1.6',
source: 'synthetic-scenario',
description:
'Station boots, reports the firmware as downloaded and installed within seconds, then sends a heartbeat.',
},
events: [
{
timestamp: '2024-01-15T06:00:00.000Z',
direction: 'CS_TO_CSMS',
message: [
2,
'msg-001',
'BootNotification',
{
chargePointVendor: 'SyntheticVendor',
chargePointModel: 'SM-100',
chargePointSerialNumber: 'CS-SYNTHETIC-016',
firmwareVersion: '1.0.0',
},
],
},
{
timestamp: '2024-01-15T06:00:00.500Z',
direction: 'CSMS_TO_CS',
message: [
3,
'msg-001',
{
currentTime: '2024-01-15T06:00:00.500Z',
interval: 300,
status: 'Accepted',
},
],
},
{
timestamp: '2024-01-15T06:00:30.000Z',
direction: 'CS_TO_CSMS',
message: [2, 'msg-002', 'FirmwareStatusNotification', { status: 'Downloaded' }],
},
{
timestamp: '2024-01-15T06:00:30.500Z',
direction: 'CSMS_TO_CS',
message: [3, 'msg-002', {}],
},
{
timestamp: '2024-01-15T06:01:00.000Z',
direction: 'CS_TO_CSMS',
message: [2, 'msg-003', 'FirmwareStatusNotification', { status: 'Installed' }],
},
{
timestamp: '2024-01-15T06:01:00.500Z',
direction: 'CSMS_TO_CS',
message: [3, 'msg-003', {}],
},
{
timestamp: '2024-01-15T06:03:00.000Z',
direction: 'CS_TO_CSMS',
message: [2, 'msg-hb-1', 'Heartbeat', {}],
},
{
timestamp: '2024-01-15T06:03:00.500Z',
direction: 'CSMS_TO_CS',
message: [3, 'msg-hb-1', { currentTime: '2024-01-15T06:03:00.500Z' }],
},
],
},
expectedFailures: [],
assertions: [{ type: 'no_failures', params: {} }],
};
18 changes: 16 additions & 2 deletions packages/toolkit/src/scenarios/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
shortSessionScenario,
heartbeatIrregularScenario,
unresponsiveCsmsScenario,
firmwareUpdateSuccessScenario,
firmwareUpdateFailureScenario,
} from './index.js';
import { parseTrace, buildSessionTimeline, detectFailures } from '../core/index.js';

Expand All @@ -26,8 +28,8 @@ import { parseTrace, buildSessionTimeline, detectFailures } from '../core/index.
// ---------------------------------------------------------------------------

describe('scenario registry', () => {
it('exports exactly 15 scenarios', () => {
expect(scenarios).toHaveLength(15);
it('exports exactly 17 scenarios', () => {
expect(scenarios).toHaveLength(17);
});

it('exports scenario names in order', () => {
Expand All @@ -47,6 +49,8 @@ describe('scenario registry', () => {
'short-session',
'heartbeat-irregular',
'unresponsive-csms',
'firmware-update-success',
'firmware-update-failure',
]);
});

Expand Down Expand Up @@ -75,6 +79,8 @@ describe('scenario registry', () => {
expect(getScenario('short-session')).toBe(shortSessionScenario);
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', () => {
Expand Down Expand Up @@ -199,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);
});
});

// ---------------------------------------------------------------------------
Expand Down
10 changes: 10 additions & 0 deletions packages/toolkit/src/scenarios/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import meterAnomaly from './__scenarios__/meter-anomaly.js';
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
Expand Down Expand Up @@ -66,6 +68,8 @@ const meterAnomalyScenario: Scenario = meterAnomaly as unknown as Scenario;
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
Expand All @@ -87,6 +91,8 @@ export const scenarios = [
shortSessionScenario,
heartbeatIrregularScenario,
unresponsiveCsmsScenario,
firmwareUpdateSuccessScenario,
firmwareUpdateFailureScenario,
] as const;

export const scenarioNames = [
Expand All @@ -105,6 +111,8 @@ export const scenarioNames = [
'short-session',
'heartbeat-irregular',
'unresponsive-csms',
'firmware-update-success',
'firmware-update-failure',
] as const;

export {
Expand All @@ -123,6 +131,8 @@ export {
shortSessionScenario,
heartbeatIrregularScenario,
unresponsiveCsmsScenario,
firmwareUpdateSuccessScenario,
firmwareUpdateFailureScenario,
};

export { compareScenarioReports } from './compare.js';
Expand Down
4 changes: 2 additions & 2 deletions tests/external-fixture/test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 === 15,
`15 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');
Expand Down