Skip to content

Commit 49bc07e

Browse files
authored
feat(scenarios): add repeated-boot-notification scenario (#170)
Adds a `repeated-boot-notification` scenario covering `REPEATED_BOOT_NOTIFICATION`, the last detection rule without one. The synthetic trace reboots a station three times at one minute intervals, all inside the rule's five minute window, then sends a single Heartbeat so the heartbeat rules stay quiet. `expectedFailures` is `['REPEATED_BOOT_NOTIFICATION']`, with an `event_count` assertion on `BootNotification`. With this, all 16 detection rules have a scenario exercising them, which is the invariant #140 wrote into CONTRIBUTING. The registry reaches 21, clearing the 20+ scenario target in the v1.0 milestone. Takes the count to 21 in the registry test, the external fixture test, and both READMEs. The detection-rule count stays 16. Closes #139
1 parent ca2bb0e commit 49bc07e

7 files changed

Lines changed: 144 additions & 6 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** — 20 predefined scenarios with expected failure
25+
- **Scenario Evaluator** — 21 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** — 20 predefined scenarios with expected failure
23+
- **Scenario Evaluator** — 21 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: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
firmwareUpdateFailureScenario,
2323
refusedAuthorizationScenario,
2424
heartbeatTimeoutScenario,
25+
repeatedBootNotificationScenario,
2526
meterValueZeroScenario,
2627
} from './index.js';
2728
import { parseTrace, buildSessionTimeline, detectFailures } from '../core/index.js';
@@ -31,8 +32,8 @@ import { parseTrace, buildSessionTimeline, detectFailures } from '../core/index.
3132
// ---------------------------------------------------------------------------
3233

3334
describe('scenario registry', () => {
34-
it('exports exactly 20 scenarios', () => {
35-
expect(scenarios).toHaveLength(20);
35+
it('exports exactly 21 scenarios', () => {
36+
expect(scenarios).toHaveLength(21);
3637
});
3738

3839
it('exports scenario names in order', () => {
@@ -56,6 +57,7 @@ describe('scenario registry', () => {
5657
'firmware-update-failure',
5758
'refused-authorization',
5859
'heartbeat-timeout',
60+
'repeated-boot-notification',
5961
'meter-value-zero',
6062
]);
6163
});
@@ -89,6 +91,7 @@ describe('scenario registry', () => {
8991
expect(getScenario('firmware-update-failure')).toBe(firmwareUpdateFailureScenario);
9092
expect(getScenario('refused-authorization')).toBe(refusedAuthorizationScenario);
9193
expect(getScenario('heartbeat-timeout')).toBe(heartbeatTimeoutScenario);
94+
expect(getScenario('repeated-boot-notification')).toBe(repeatedBootNotificationScenario);
9295
expect(getScenario('meter-value-zero')).toBe(meterValueZeroScenario);
9396
});
9497

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

252+
it('repeated-boot-notification: detects REPEATED_BOOT_NOTIFICATION', () => {
253+
const trace = JSON.stringify(repeatedBootNotificationScenario.trace);
254+
const result = parseTrace(trace);
255+
const sessions = buildSessionTimeline(result.events);
256+
const failures = detectFailures(result.events, sessions);
257+
expect(failures.some((f) => f.code === 'REPEATED_BOOT_NOTIFICATION')).toBe(true);
258+
});
259+
249260
it('meter-value-zero: no failures detected', () => {
250261
const trace = JSON.stringify(meterValueZeroScenario.trace);
251262
const result = parseTrace(trace);

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ 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';
2626
import heartbeatTimeout from './__scenarios__/heartbeat-timeout.js';
27+
import repeatedBootNotification from './__scenarios__/repeated-boot-notification.js';
2728
import meterValueZero from './__scenarios__/meter-value-zero.js';
2829

2930
// ---------------------------------------------------------------------------
@@ -75,6 +76,7 @@ const firmwareUpdateSuccessScenario: Scenario = firmwareUpdateSuccess as unknown
7576
const firmwareUpdateFailureScenario: Scenario = firmwareUpdateFailure as unknown as Scenario;
7677
const refusedAuthorizationScenario: Scenario = refusedAuthorization as unknown as Scenario;
7778
const heartbeatTimeoutScenario: Scenario = heartbeatTimeout as unknown as Scenario;
79+
const repeatedBootNotificationScenario: Scenario = repeatedBootNotification as unknown as Scenario;
7880
const meterValueZeroScenario: Scenario = meterValueZero as unknown as Scenario;
7981

8082
// ---------------------------------------------------------------------------
@@ -101,6 +103,7 @@ export const scenarios = [
101103
firmwareUpdateFailureScenario,
102104
refusedAuthorizationScenario,
103105
heartbeatTimeoutScenario,
106+
repeatedBootNotificationScenario,
104107
meterValueZeroScenario,
105108
] as const;
106109

@@ -124,6 +127,7 @@ export const scenarioNames = [
124127
'firmware-update-failure',
125128
'refused-authorization',
126129
'heartbeat-timeout',
130+
'repeated-boot-notification',
127131
'meter-value-zero',
128132
] as const;
129133

@@ -147,6 +151,7 @@ export {
147151
firmwareUpdateFailureScenario,
148152
refusedAuthorizationScenario,
149153
heartbeatTimeoutScenario,
154+
repeatedBootNotificationScenario,
150155
meterValueZeroScenario,
151156
};
152157

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