Skip to content

Commit c67dfa9

Browse files
author
YANGCHUNHONG3000
committed
feat(scenarios): add status-transitions-legal scenario
1 parent 3aac9bb commit c67dfa9

7 files changed

Lines changed: 179 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 a status-transitions-legal scenario to ensure a fully legal OCPP 1.6 status path reports no failure.

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**21 predefined scenarios with expected failure
25+
- **Scenario Evaluator**22 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**21 predefined scenarios with expected failure
23+
- **Scenario Evaluator**22 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: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
export default {
2+
name: 'status-transitions-legal',
3+
description:
4+
'Connector walks a completely legal OCPP 1.6 status path (Available -> Preparing -> Charging -> Finishing -> Available). A legal negative control that must not trigger STATUS_TRANSITION_VIOLATION.',
5+
trace: {
6+
traceId: 'scenario-status-transitions-legal',
7+
metadata: {
8+
stationId: 'CS-SYNTHETIC-022',
9+
ocppVersion: '1.6',
10+
source: 'synthetic-scenario',
11+
description:
12+
'Connector status transitions strictly along the path the OCPP 1.6 section 4.9 table permits, with a trailing heartbeat to keep heartbeat rules quiet.',
13+
},
14+
events: [
15+
{
16+
timestamp: '2026-02-01T10:00:00.000Z',
17+
direction: 'CS_TO_CSMS',
18+
message: [
19+
2,
20+
'msg-001',
21+
'BootNotification',
22+
{
23+
chargePointVendor: 'SyntheticVendor',
24+
chargePointModel: 'SM-100',
25+
chargePointSerialNumber: 'CS-SYNTHETIC-022',
26+
firmwareVersion: '1.0.0',
27+
},
28+
],
29+
},
30+
{
31+
timestamp: '2026-02-01T10:00:00.500Z',
32+
direction: 'CSMS_TO_CS',
33+
message: [
34+
3,
35+
'msg-001',
36+
{
37+
currentTime: '2026-02-01T10:00:00.500Z',
38+
interval: 300,
39+
status: 'Accepted',
40+
},
41+
],
42+
},
43+
{
44+
timestamp: '2026-02-01T10:00:30.000Z',
45+
direction: 'CS_TO_CSMS',
46+
message: [
47+
2,
48+
'msg-002',
49+
'StatusNotification',
50+
{
51+
connectorId: 1,
52+
status: 'Available',
53+
errorCode: 'NoError',
54+
},
55+
],
56+
},
57+
{
58+
timestamp: '2026-02-01T10:00:30.500Z',
59+
direction: 'CSMS_TO_CS',
60+
message: [3, 'msg-002', {}],
61+
},
62+
{
63+
timestamp: '2026-02-01T10:01:00.000Z',
64+
direction: 'CS_TO_CSMS',
65+
message: [
66+
2,
67+
'msg-003',
68+
'StatusNotification',
69+
{
70+
connectorId: 1,
71+
status: 'Preparing',
72+
errorCode: 'NoError',
73+
},
74+
],
75+
},
76+
{
77+
timestamp: '2026-02-01T10:01:00.500Z',
78+
direction: 'CSMS_TO_CS',
79+
message: [3, 'msg-003', {}],
80+
},
81+
{
82+
timestamp: '2026-02-01T10:02:00.000Z',
83+
direction: 'CS_TO_CSMS',
84+
message: [
85+
2,
86+
'msg-004',
87+
'StatusNotification',
88+
{
89+
connectorId: 1,
90+
status: 'Charging',
91+
errorCode: 'NoError',
92+
},
93+
],
94+
},
95+
{
96+
timestamp: '2026-02-01T10:02:00.500Z',
97+
direction: 'CSMS_TO_CS',
98+
message: [3, 'msg-004', {}],
99+
},
100+
{
101+
timestamp: '2026-02-01T10:03:00.000Z',
102+
direction: 'CS_TO_CSMS',
103+
message: [
104+
2,
105+
'msg-005',
106+
'StatusNotification',
107+
{
108+
connectorId: 1,
109+
status: 'Finishing',
110+
errorCode: 'NoError',
111+
},
112+
],
113+
},
114+
{
115+
timestamp: '2026-02-01T10:03:00.500Z',
116+
direction: 'CSMS_TO_CS',
117+
message: [3, 'msg-005', {}],
118+
},
119+
{
120+
timestamp: '2026-02-01T10:04:00.000Z',
121+
direction: 'CS_TO_CSMS',
122+
message: [
123+
2,
124+
'msg-006',
125+
'StatusNotification',
126+
{
127+
connectorId: 1,
128+
status: 'Available',
129+
errorCode: 'NoError',
130+
},
131+
],
132+
},
133+
{
134+
timestamp: '2026-02-01T10:04:00.500Z',
135+
direction: 'CSMS_TO_CS',
136+
message: [3, 'msg-006', {}],
137+
},
138+
{
139+
timestamp: '2026-02-01T10:05:00.000Z',
140+
direction: 'CS_TO_CSMS',
141+
message: [2, 'msg-007', 'Heartbeat', {}],
142+
},
143+
{
144+
timestamp: '2026-02-01T10:05:00.500Z',
145+
direction: 'CSMS_TO_CS',
146+
message: [3, 'msg-007', { currentTime: '2026-02-01T10:05:00.500Z' }],
147+
},
148+
],
149+
},
150+
expectedFailures: [],
151+
assertions: [{ type: 'no_failures', params: {} }],
152+
};

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
heartbeatTimeoutScenario,
2525
repeatedBootNotificationScenario,
2626
meterValueZeroScenario,
27+
statusTransitionsLegalScenario,
2728
} from './index.js';
2829
import { parseTrace, buildSessionTimeline, detectFailures } from '../core/index.js';
2930

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

3435
describe('scenario registry', () => {
35-
it('exports exactly 21 scenarios', () => {
36-
expect(scenarios).toHaveLength(21);
36+
it('exports exactly 22 scenarios', () => {
37+
expect(scenarios).toHaveLength(22);
3738
});
3839

3940
it('exports scenario names in order', () => {
@@ -59,6 +60,7 @@ describe('scenario registry', () => {
5960
'heartbeat-timeout',
6061
'repeated-boot-notification',
6162
'meter-value-zero',
63+
'status-transitions-legal',
6264
]);
6365
});
6466

@@ -93,6 +95,7 @@ describe('scenario registry', () => {
9395
expect(getScenario('heartbeat-timeout')).toBe(heartbeatTimeoutScenario);
9496
expect(getScenario('repeated-boot-notification')).toBe(repeatedBootNotificationScenario);
9597
expect(getScenario('meter-value-zero')).toBe(meterValueZeroScenario);
98+
expect(getScenario('status-transitions-legal')).toBe(statusTransitionsLegalScenario);
9699
});
97100

98101
it('getScenario returns undefined for unknown name', () => {
@@ -264,6 +267,14 @@ describe('scenario engine integration', () => {
264267
const failures = detectFailures(result.events, sessions);
265268
expect(failures).toHaveLength(0);
266269
});
270+
271+
it('status-transitions-legal: no failures detected', () => {
272+
const trace = JSON.stringify(statusTransitionsLegalScenario.trace);
273+
const result = parseTrace(trace);
274+
const sessions = buildSessionTimeline(result.events);
275+
const failures = detectFailures(result.events, sessions);
276+
expect(failures).toHaveLength(0);
277+
});
267278
});
268279

269280
// ---------------------------------------------------------------------------

packages/toolkit/src/scenarios/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import refusedAuthorization from './__scenarios__/refused-authorization.js';
2626
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';
29+
import statusTransitionsLegal from './__scenarios__/status-transitions-legal.js';
2930

3031
// ---------------------------------------------------------------------------
3132
// Scenarios derived from core fixtures
@@ -78,6 +79,7 @@ const refusedAuthorizationScenario: Scenario = refusedAuthorization as unknown a
7879
const heartbeatTimeoutScenario: Scenario = heartbeatTimeout as unknown as Scenario;
7980
const repeatedBootNotificationScenario: Scenario = repeatedBootNotification as unknown as Scenario;
8081
const meterValueZeroScenario: Scenario = meterValueZero as unknown as Scenario;
82+
const statusTransitionsLegalScenario: Scenario = statusTransitionsLegal as unknown as Scenario;
8183

8284
// ---------------------------------------------------------------------------
8385
// Registry
@@ -105,6 +107,7 @@ export const scenarios = [
105107
heartbeatTimeoutScenario,
106108
repeatedBootNotificationScenario,
107109
meterValueZeroScenario,
110+
statusTransitionsLegalScenario,
108111
] as const;
109112

110113
export const scenarioNames = [
@@ -129,6 +132,7 @@ export const scenarioNames = [
129132
'heartbeat-timeout',
130133
'repeated-boot-notification',
131134
'meter-value-zero',
135+
'status-transitions-legal',
132136
] as const;
133137

134138
export {
@@ -153,6 +157,7 @@ export {
153157
heartbeatTimeoutScenario,
154158
repeatedBootNotificationScenario,
155159
meterValueZeroScenario,
160+
statusTransitionsLegalScenario,
156161
};
157162

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