Skip to content

Commit 20e3d78

Browse files
committed
feat(scenarios): add repeated-boot-notification scenario
1 parent 84ba917 commit 20e3d78

7 files changed

Lines changed: 168 additions & 13 deletions

File tree

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+
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.

‎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** — 18 predefined scenarios with expected failure
25+
- **Scenario Evaluator** — 20 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** — 18 predefined scenarios with expected failure
23+
- **Scenario Evaluator** — 20 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: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
export default {
2+
name: 'repeated-boot-notification',
3+
description:
4+
'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.',
5+
trace: {
6+
traceId: 'scenario-repeated-boot-notification',
7+
metadata: {
8+
stationId: 'CS-SYNTHETIC-020',
9+
ocppVersion: '1.6',
10+
source: 'synthetic-scenario',
11+
description: 'Station reboots three times in three minutes — reboot loop expected.',
12+
},
13+
events: [
14+
{
15+
timestamp: '2026-01-15T08:00:00.000Z',
16+
direction: 'CS_TO_CSMS',
17+
message: [
18+
2,
19+
'msg-boot-1',
20+
'BootNotification',
21+
{
22+
chargePointVendor: 'SyntheticVendor',
23+
chargePointModel: 'SM-100',
24+
chargePointSerialNumber: 'CS-SYNTHETIC-020',
25+
firmwareVersion: '1.0.0',
26+
},
27+
],
28+
},
29+
{
30+
timestamp: '2026-01-15T08:00:00.500Z',
31+
direction: 'CSMS_TO_CS',
32+
message: [
33+
3,
34+
'msg-boot-1',
35+
{
36+
currentTime: '2026-01-15T08:00:00.500Z',
37+
interval: 300,
38+
status: 'Accepted',
39+
},
40+
],
41+
},
42+
{
43+
timestamp: '2026-01-15T08:01:00.000Z',
44+
direction: 'CS_TO_CSMS',
45+
message: [
46+
2,
47+
'msg-boot-2',
48+
'BootNotification',
49+
{
50+
chargePointVendor: 'SyntheticVendor',
51+
chargePointModel: 'SM-100',
52+
chargePointSerialNumber: 'CS-SYNTHETIC-020',
53+
firmwareVersion: '1.0.0',
54+
},
55+
],
56+
},
57+
{
58+
timestamp: '2026-01-15T08:01:00.500Z',
59+
direction: 'CSMS_TO_CS',
60+
message: [
61+
3,
62+
'msg-boot-2',
63+
{
64+
currentTime: '2026-01-15T08:01:00.500Z',
65+
interval: 300,
66+
status: 'Accepted',
67+
},
68+
],
69+
},
70+
{
71+
timestamp: '2026-01-15T08:02:00.000Z',
72+
direction: 'CS_TO_CSMS',
73+
message: [
74+
2,
75+
'msg-boot-3',
76+
'BootNotification',
77+
{
78+
chargePointVendor: 'SyntheticVendor',
79+
chargePointModel: 'SM-100',
80+
chargePointSerialNumber: 'CS-SYNTHETIC-020',
81+
firmwareVersion: '1.0.0',
82+
},
83+
],
84+
},
85+
{
86+
timestamp: '2026-01-15T08:02:00.500Z',
87+
direction: 'CSMS_TO_CS',
88+
message: [
89+
3,
90+
'msg-boot-3',
91+
{
92+
currentTime: '2026-01-15T08:02:00.500Z',
93+
interval: 300,
94+
status: 'Accepted',
95+
},
96+
],
97+
},
98+
{
99+
timestamp: '2026-01-15T08:03:00.000Z',
100+
direction: 'CS_TO_CSMS',
101+
message: [2, 'msg-hb-1', 'Heartbeat', {}],
102+
},
103+
{
104+
timestamp: '2026-01-15T08:03:00.500Z',
105+
direction: 'CSMS_TO_CS',
106+
message: [3, 'msg-hb-1', { currentTime: '2026-01-15T08:03:00.500Z' }],
107+
},
108+
],
109+
},
110+
expectedFailures: ['REPEATED_BOOT_NOTIFICATION'],
111+
assertions: [
112+
{
113+
type: 'event_count',
114+
params: { action: 'BootNotification', min: 3 },
115+
},
116+
],
117+
};

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

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,18 @@ import {
2121
firmwareUpdateSuccessScenario,
2222
firmwareUpdateFailureScenario,
2323
refusedAuthorizationScenario,
24-
} from './index.js';
24+
heartbeatTimeoutScenario,
25+
repeatedBootNotificationScenario,
26+
>} from './index.js';
2527
import { parseTrace, buildSessionTimeline, detectFailures } from '../core/index.js';
2628

2729
// ---------------------------------------------------------------------------
2830
// Registry tests
2931
// ---------------------------------------------------------------------------
3032

3133
describe('scenario registry', () => {
32-
it('exports exactly 18 scenarios', () => {
33-
expect(scenarios).toHaveLength(18);
34+
it('exports exactly 20 scenarios', () => {
35+
expect(scenarios).toHaveLength(20);
3436
});
3537

3638
it('exports scenario names in order', () => {
@@ -53,6 +55,8 @@ describe('scenario registry', () => {
5355
'firmware-update-success',
5456
'firmware-update-failure',
5557
'refused-authorization',
58+
'heartbeat-timeout',
59+
'repeated-boot-notification',
5660
]);
5761
});
5862

@@ -83,6 +87,9 @@ describe('scenario registry', () => {
8387
expect(getScenario('unresponsive-csms')).toBe(unresponsiveCsmsScenario);
8488
expect(getScenario('firmware-update-success')).toBe(firmwareUpdateSuccessScenario);
8589
expect(getScenario('firmware-update-failure')).toBe(firmwareUpdateFailureScenario);
90+
expect(getScenario('refused-authorization')).toBe(refusedAuthorizationScenario);
91+
expect(getScenario('heartbeat-timeout')).toBe(heartbeatTimeoutScenario);
92+
expect(getScenario('repeated-boot-notification')).toBe(repeatedBootNotificationScenario);
8693
});
8794

8895
it('getScenario returns undefined for unknown name', () => {
@@ -230,7 +237,23 @@ describe('scenario engine integration', () => {
230237
'ConcurrentTx',
231238
]);
232239
});
233-
});
240+
241+
it('heartbeat-timeout: detects TIMEOUT_NO_HEARTBEAT', () => {
242+
const trace = JSON.stringify(heartbeatTimeoutScenario.trace);
243+
const result = parseTrace(trace);
244+
const sessions = buildSessionTimeline(result.events);
245+
const failures = detectFailures(result.events, sessions);
246+
expect(failures.some((f) => f.code === 'TIMEOUT_NO_HEARTBEAT')).toBe(true);
247+
});
248+
249+
it('repeated-boot-notification: detects REPEATED_BOOT_NOTIFICATION', () => {
250+
const trace = JSON.stringify(repeatedBootNotificationScenario.trace);
251+
const result = parseTrace(trace);
252+
const sessions = buildSessionTimeline(result.events);
253+
const failures = detectFailures(result.events, sessions);
254+
expect(failures.some((f) => f.code === 'REPEATED_BOOT_NOTIFICATION')).toBe(true);
255+
});
256+
>});
234257

235258
// ---------------------------------------------------------------------------
236259
// Synthetic data policy

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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ import unresponsiveCsms from './__scenarios__/unresponsive-csms.js';
2323
import firmwareUpdateSuccess from './__scenarios__/firmware-update-success.js';
2424
import firmwareUpdateFailure from './__scenarios__/firmware-update-failure.js';
2525
import refusedAuthorization from './__scenarios__/refused-authorization.js';
26-
26+
import heartbeatTimeout from './__scenarios__/heartbeat-timeout.js';
27+
import repeatedBootNotification from './__scenarios__/repeated-boot-notification.js';
28+
>
2729
// ---------------------------------------------------------------------------
2830
// Scenarios derived from core fixtures
2931
// ---------------------------------------------------------------------------
@@ -72,7 +74,9 @@ const unresponsiveCsmsScenario: Scenario = unresponsiveCsms as unknown as Scenar
7274
const firmwareUpdateSuccessScenario: Scenario = firmwareUpdateSuccess as unknown as Scenario;
7375
const firmwareUpdateFailureScenario: Scenario = firmwareUpdateFailure as unknown as Scenario;
7476
const refusedAuthorizationScenario: Scenario = refusedAuthorization as unknown as Scenario;
75-
77+
const heartbeatTimeoutScenario: Scenario = heartbeatTimeout as unknown as Scenario;
78+
const repeatedBootNotificationScenario: Scenario = repeatedBootNotification as unknown as Scenario;
79+
>
7680
// ---------------------------------------------------------------------------
7781
// Registry
7882
// ---------------------------------------------------------------------------
@@ -96,7 +100,9 @@ export const scenarios = [
96100
firmwareUpdateSuccessScenario,
97101
firmwareUpdateFailureScenario,
98102
refusedAuthorizationScenario,
99-
] as const;
103+
heartbeatTimeoutScenario,
104+
repeatedBootNotificationScenario,
105+
>] as const;
100106

101107
export const scenarioNames = [
102108
'normal-session',
@@ -117,7 +123,9 @@ export const scenarioNames = [
117123
'firmware-update-success',
118124
'firmware-update-failure',
119125
'refused-authorization',
120-
] as const;
126+
'heartbeat-timeout',
127+
'repeated-boot-notification',
128+
>] as const;
121129

122130
export {
123131
normalSessionScenario,
@@ -138,7 +146,9 @@ export {
138146
firmwareUpdateSuccessScenario,
139147
firmwareUpdateFailureScenario,
140148
refusedAuthorizationScenario,
141-
};
149+
heartbeatTimeoutScenario,
150+
repeatedBootNotificationScenario,
151+
>};
142152

143153
export { compareScenarioReports } from './compare.js';
144154
export type { ScenarioComparison } 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 === 18,
162-
`18 scenarios exported (got ${scenarios.scenarios.length})`,
161+
scenarios.scenarios.length === 20,
162+
`20 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)