Skip to content

Commit 15c9837

Browse files
feat(scenarios): add boot-outside-repeat-window negative control (#179)
Adds a `boot-outside-repeat-window` scenario: two BootNotifications 390 seconds apart, with a Heartbeat after each. `expectedFailures` is empty and the scenario asserts `no_failures`. This is the negative control for the boundary of `REPEATED_BOOT_NOTIFICATION`. The rule groups BootNotifications into a five minute window, and #139 covers the inside of that window with three boots a minute apart. Nothing covered the outside, so a change that widened the window or made the comparison inclusive would have passed the whole suite while reporting ordinary restarts as reboot loops. Takes the scenario registry to 23, updating the count in the registry test, the external fixture test, and both READMEs. The detection-rule count stays 16. Closes #174
1 parent c32cfa7 commit 15c9837

7 files changed

Lines changed: 118 additions & 6 deletions

File tree

‎.changeset/happy-turkeys-cough.md‎

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+
added a new scenario: boot-outside-repeat-window

‎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** — 22 predefined scenarios with expected failure
25+
- **Scenario Evaluator** — 23 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** — 22 predefined scenarios with expected failure
23+
- **Scenario Evaluator** — 23 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: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
export default {
2+
name: 'boot-outside-repeat-window',
3+
description:
4+
'Two BootNotifications triggered outside the configured repeat window, expecting no failures.',
5+
trace: {
6+
traceId: 'scenario-boot-outside-repeat-window',
7+
metadata: {
8+
stationId: 'CS-SYNTHETIC-023',
9+
ocppVersion: '1.6',
10+
source: 'synthetic-scenario',
11+
description: '2 BootNotifications 300+ seconds apart should not trigger failure',
12+
},
13+
events: [
14+
{
15+
timestamp: '2026-02-01T11:00:00.000Z',
16+
direction: 'CS_TO_CSMS',
17+
message: [
18+
2,
19+
'msg-001',
20+
'BootNotification',
21+
{
22+
chargePointVendor: 'SyntheticVendor',
23+
chargePointModel: 'SM-100',
24+
chargePointSerialNumber: 'CS-SYNTHETIC-023',
25+
firmwareVersion: '1.0.0',
26+
},
27+
],
28+
},
29+
{
30+
timestamp: '2026-02-01T11:00:00.500Z',
31+
direction: 'CSMS_TO_CS',
32+
message: [
33+
3,
34+
'msg-001',
35+
{
36+
currentTime: '2026-02-01T11:00:00.500Z',
37+
interval: 300,
38+
status: 'Accepted',
39+
},
40+
],
41+
},
42+
{
43+
timestamp: '2026-02-01T11:03:00.000Z',
44+
direction: 'CS_TO_CSMS',
45+
message: [2, 'msg-hb-1', 'Heartbeat', {}],
46+
},
47+
{
48+
timestamp: '2026-02-01T11:03:00.500Z',
49+
direction: 'CSMS_TO_CS',
50+
message: [3, 'msg-hb-1', { currentTime: '2026-02-01T11:03:00.500Z' }],
51+
},
52+
{
53+
timestamp: '2026-02-01T11:06:30.000Z',
54+
direction: 'CS_TO_CSMS',
55+
message: [
56+
2,
57+
'msg-002',
58+
'BootNotification',
59+
{
60+
chargePointVendor: 'SyntheticVendor',
61+
chargePointModel: 'SM-100',
62+
chargePointSerialNumber: 'CS-SYNTHETIC-023',
63+
firmwareVersion: '1.0.0',
64+
},
65+
],
66+
},
67+
{
68+
timestamp: '2026-02-01T11:06:30.500Z',
69+
direction: 'CSMS_TO_CS',
70+
message: [
71+
3,
72+
'msg-002',
73+
{
74+
currentTime: '2026-02-01T11:06:30.500Z',
75+
interval: 300,
76+
status: 'Accepted',
77+
},
78+
],
79+
},
80+
{
81+
timestamp: '2026-02-01T11:09:00.000Z',
82+
direction: 'CS_TO_CSMS',
83+
message: [2, 'msg-hb-2', 'Heartbeat', {}],
84+
},
85+
{
86+
timestamp: '2026-02-01T11:09:00.500Z',
87+
direction: 'CSMS_TO_CS',
88+
message: [3, 'msg-hb-2', { currentTime: '2026-02-01T11:09:00.500Z' }],
89+
},
90+
],
91+
},
92+
expectedFailures: [],
93+
assertions: [
94+
{
95+
type: 'no_failures',
96+
params: {},
97+
},
98+
],
99+
};

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
repeatedBootNotificationScenario,
2626
meterValueZeroScenario,
2727
statusTransitionsLegalScenario,
28+
bootOutsideRepeatWindowScenario,
2829
} from './index.js';
2930
import { parseTrace, buildSessionTimeline, detectFailures } from '../core/index.js';
3031

@@ -33,8 +34,8 @@ import { parseTrace, buildSessionTimeline, detectFailures } from '../core/index.
3334
// ---------------------------------------------------------------------------
3435

3536
describe('scenario registry', () => {
36-
it('exports exactly 22 scenarios', () => {
37-
expect(scenarios).toHaveLength(22);
37+
it('exports exactly 23 scenarios', () => {
38+
expect(scenarios).toHaveLength(23);
3839
});
3940

4041
it('exports scenario names in order', () => {
@@ -61,6 +62,7 @@ describe('scenario registry', () => {
6162
'repeated-boot-notification',
6263
'meter-value-zero',
6364
'status-transitions-legal',
65+
'boot-outside-repeat-window',
6466
]);
6567
});
6668

@@ -96,6 +98,7 @@ describe('scenario registry', () => {
9698
expect(getScenario('repeated-boot-notification')).toBe(repeatedBootNotificationScenario);
9799
expect(getScenario('meter-value-zero')).toBe(meterValueZeroScenario);
98100
expect(getScenario('status-transitions-legal')).toBe(statusTransitionsLegalScenario);
101+
expect(getScenario('boot-outside-repeat-window')).toBe(bootOutsideRepeatWindowScenario);
99102
});
100103

101104
it('getScenario returns undefined for unknown name', () => {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import heartbeatTimeout from './__scenarios__/heartbeat-timeout.js';
2727
import repeatedBootNotification from './__scenarios__/repeated-boot-notification.js';
2828
import meterValueZero from './__scenarios__/meter-value-zero.js';
2929
import statusTransitionsLegal from './__scenarios__/status-transitions-legal.js';
30+
import bootOutsideRepeatWindow from './__scenarios__/boot-outside-repeat-window.js';
3031

3132
// ---------------------------------------------------------------------------
3233
// Scenarios derived from core fixtures
@@ -80,6 +81,7 @@ const heartbeatTimeoutScenario: Scenario = heartbeatTimeout as unknown as Scenar
8081
const repeatedBootNotificationScenario: Scenario = repeatedBootNotification as unknown as Scenario;
8182
const meterValueZeroScenario: Scenario = meterValueZero as unknown as Scenario;
8283
const statusTransitionsLegalScenario: Scenario = statusTransitionsLegal as unknown as Scenario;
84+
const bootOutsideRepeatWindowScenario: Scenario = bootOutsideRepeatWindow as unknown as Scenario;
8385

8486
// ---------------------------------------------------------------------------
8587
// Registry
@@ -108,6 +110,7 @@ export const scenarios = [
108110
repeatedBootNotificationScenario,
109111
meterValueZeroScenario,
110112
statusTransitionsLegalScenario,
113+
bootOutsideRepeatWindowScenario,
111114
] as const;
112115

113116
export const scenarioNames = [
@@ -133,6 +136,7 @@ export const scenarioNames = [
133136
'repeated-boot-notification',
134137
'meter-value-zero',
135138
'status-transitions-legal',
139+
'boot-outside-repeat-window',
136140
] as const;
137141

138142
export {
@@ -158,6 +162,7 @@ export {
158162
repeatedBootNotificationScenario,
159163
meterValueZeroScenario,
160164
statusTransitionsLegalScenario,
165+
bootOutsideRepeatWindowScenario,
161166
};
162167

163168
export { compareScenarioReports } 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 === 22,
162-
`22 scenarios exported (got ${scenarios.scenarios.length})`,
161+
scenarios.scenarios.length === 23,
162+
`23 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)