Skip to content
Merged
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/repeated-boot-notification-scenario.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ocpp-debugkit/toolkit': patch
---

Add `repeated-boot-notification` scenario covering the `REPEATED_BOOT_NOTIFICATION` detection rule. The synthetic trace reboots a station three times in three minutes (one `BootNotification` per minute) followed by a single `Heartbeat`, exercising the 5 minute window used by the rule.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** — 20 predefined scenarios with expected failure
- **Scenario Evaluator** — 21 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
2 changes: 1 addition & 1 deletion packages/toolkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** — 20 predefined scenarios with expected failure
- **Scenario Evaluator** — 21 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,117 @@
export default {
name: 'repeated-boot-notification',
description:
'Station rebooting in a loop: three BootNotification calls one minute apart, followed by a single Heartbeat. All three boots land inside the five minute window. Expects REPEATED_BOOT_NOTIFICATION failure. Uses event_count assertion for BootNotification.',
trace: {
traceId: 'scenario-repeated-boot-notification',
metadata: {
stationId: 'CS-SYNTHETIC-020',
ocppVersion: '1.6',
source: 'synthetic-scenario',
description: 'Station reboots three times in three minutes — reboot loop expected.',
},
events: [
{
timestamp: '2026-01-15T08:00:00.000Z',
direction: 'CS_TO_CSMS',
message: [
2,
'msg-boot-1',
'BootNotification',
{
chargePointVendor: 'SyntheticVendor',
chargePointModel: 'SM-100',
chargePointSerialNumber: 'CS-SYNTHETIC-020',
firmwareVersion: '1.0.0',
},
],
},
{
timestamp: '2026-01-15T08:00:00.500Z',
direction: 'CSMS_TO_CS',
message: [
3,
'msg-boot-1',
{
currentTime: '2026-01-15T08:00:00.500Z',
interval: 300,
status: 'Accepted',
},
],
},
{
timestamp: '2026-01-15T08:01:00.000Z',
direction: 'CS_TO_CSMS',
message: [
2,
'msg-boot-2',
'BootNotification',
{
chargePointVendor: 'SyntheticVendor',
chargePointModel: 'SM-100',
chargePointSerialNumber: 'CS-SYNTHETIC-020',
firmwareVersion: '1.0.0',
},
],
},
{
timestamp: '2026-01-15T08:01:00.500Z',
direction: 'CSMS_TO_CS',
message: [
3,
'msg-boot-2',
{
currentTime: '2026-01-15T08:01:00.500Z',
interval: 300,
status: 'Accepted',
},
],
},
{
timestamp: '2026-01-15T08:02:00.000Z',
direction: 'CS_TO_CSMS',
message: [
2,
'msg-boot-3',
'BootNotification',
{
chargePointVendor: 'SyntheticVendor',
chargePointModel: 'SM-100',
chargePointSerialNumber: 'CS-SYNTHETIC-020',
firmwareVersion: '1.0.0',
},
],
},
{
timestamp: '2026-01-15T08:02:00.500Z',
direction: 'CSMS_TO_CS',
message: [
3,
'msg-boot-3',
{
currentTime: '2026-01-15T08:02:00.500Z',
interval: 300,
status: 'Accepted',
},
],
},
{
timestamp: '2026-01-15T08:03:00.000Z',
direction: 'CS_TO_CSMS',
message: [2, 'msg-hb-1', 'Heartbeat', {}],
},
{
timestamp: '2026-01-15T08:03:00.500Z',
direction: 'CSMS_TO_CS',
message: [3, 'msg-hb-1', { currentTime: '2026-01-15T08:03:00.500Z' }],
},
],
},
expectedFailures: ['REPEATED_BOOT_NOTIFICATION'],
assertions: [
{
type: 'event_count',
params: { action: 'BootNotification', min: 3 },
},
],
};
15 changes: 13 additions & 2 deletions packages/toolkit/src/scenarios/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
firmwareUpdateFailureScenario,
refusedAuthorizationScenario,
heartbeatTimeoutScenario,
repeatedBootNotificationScenario,
meterValueZeroScenario,
} from './index.js';
import { parseTrace, buildSessionTimeline, detectFailures } from '../core/index.js';
Expand All @@ -31,8 +32,8 @@ import { parseTrace, buildSessionTimeline, detectFailures } from '../core/index.
// ---------------------------------------------------------------------------

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

it('exports scenario names in order', () => {
Expand All @@ -56,6 +57,7 @@ describe('scenario registry', () => {
'firmware-update-failure',
'refused-authorization',
'heartbeat-timeout',
'repeated-boot-notification',
'meter-value-zero',
]);
});
Expand Down Expand Up @@ -89,6 +91,7 @@ describe('scenario registry', () => {
expect(getScenario('firmware-update-failure')).toBe(firmwareUpdateFailureScenario);
expect(getScenario('refused-authorization')).toBe(refusedAuthorizationScenario);
expect(getScenario('heartbeat-timeout')).toBe(heartbeatTimeoutScenario);
expect(getScenario('repeated-boot-notification')).toBe(repeatedBootNotificationScenario);
expect(getScenario('meter-value-zero')).toBe(meterValueZeroScenario);
});

Expand Down Expand Up @@ -246,6 +249,14 @@ describe('scenario engine integration', () => {
expect(failures.some((f) => f.code === 'TIMEOUT_NO_HEARTBEAT')).toBe(true);
});

it('repeated-boot-notification: detects REPEATED_BOOT_NOTIFICATION', () => {
const trace = JSON.stringify(repeatedBootNotificationScenario.trace);
const result = parseTrace(trace);
const sessions = buildSessionTimeline(result.events);
const failures = detectFailures(result.events, sessions);
expect(failures.some((f) => f.code === 'REPEATED_BOOT_NOTIFICATION')).toBe(true);
});

it('meter-value-zero: no failures detected', () => {
const trace = JSON.stringify(meterValueZeroScenario.trace);
const result = parseTrace(trace);
Expand Down
5 changes: 5 additions & 0 deletions packages/toolkit/src/scenarios/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import firmwareUpdateSuccess from './__scenarios__/firmware-update-success.js';
import firmwareUpdateFailure from './__scenarios__/firmware-update-failure.js';
import refusedAuthorization from './__scenarios__/refused-authorization.js';
import heartbeatTimeout from './__scenarios__/heartbeat-timeout.js';
import repeatedBootNotification from './__scenarios__/repeated-boot-notification.js';
import meterValueZero from './__scenarios__/meter-value-zero.js';

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -75,6 +76,7 @@ const firmwareUpdateSuccessScenario: Scenario = firmwareUpdateSuccess as unknown
const firmwareUpdateFailureScenario: Scenario = firmwareUpdateFailure as unknown as Scenario;
const refusedAuthorizationScenario: Scenario = refusedAuthorization as unknown as Scenario;
const heartbeatTimeoutScenario: Scenario = heartbeatTimeout as unknown as Scenario;
const repeatedBootNotificationScenario: Scenario = repeatedBootNotification as unknown as Scenario;
const meterValueZeroScenario: Scenario = meterValueZero as unknown as Scenario;

// ---------------------------------------------------------------------------
Expand All @@ -101,6 +103,7 @@ export const scenarios = [
firmwareUpdateFailureScenario,
refusedAuthorizationScenario,
heartbeatTimeoutScenario,
repeatedBootNotificationScenario,
meterValueZeroScenario,
] as const;

Expand All @@ -124,6 +127,7 @@ export const scenarioNames = [
'firmware-update-failure',
'refused-authorization',
'heartbeat-timeout',
'repeated-boot-notification',
'meter-value-zero',
] as const;

Expand All @@ -147,6 +151,7 @@ export {
firmwareUpdateFailureScenario,
refusedAuthorizationScenario,
heartbeatTimeoutScenario,
repeatedBootNotificationScenario,
meterValueZeroScenario,
};

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 === 20,
`20 scenarios exported (got ${scenarios.scenarios.length})`,
scenarios.scenarios.length === 21,
`21 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
Loading