From 20e3d7872a4bcb768ef91685b3a66ea4490a53d3 Mon Sep 17 00:00:00 2001 From: MayurK-cmd Date: Mon, 10 Aug 2026 17:40:22 +0530 Subject: [PATCH] feat(scenarios): add repeated-boot-notification scenario --- .../repeated-boot-notification-scenario.md | 5 + README.md | 2 +- packages/toolkit/README.md | 2 +- .../repeated-boot-notification.ts | 117 ++++++++++++++++++ packages/toolkit/src/scenarios/index.test.ts | 31 ++++- packages/toolkit/src/scenarios/index.ts | 20 ++- tests/external-fixture/test.mjs | 4 +- 7 files changed, 168 insertions(+), 13 deletions(-) create mode 100644 .changeset/repeated-boot-notification-scenario.md create mode 100644 packages/toolkit/src/scenarios/__scenarios__/repeated-boot-notification.ts diff --git a/.changeset/repeated-boot-notification-scenario.md b/.changeset/repeated-boot-notification-scenario.md new file mode 100644 index 0000000..ef94c21 --- /dev/null +++ b/.changeset/repeated-boot-notification-scenario.md @@ -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. diff --git a/README.md b/README.md index 4284e72..c71f59c 100644 --- a/README.md +++ b/README.md @@ -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** — 18 predefined scenarios with expected failure +- **Scenario Evaluator** — 20 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. diff --git a/packages/toolkit/README.md b/packages/toolkit/README.md index 683ba65..68dfafd 100644 --- a/packages/toolkit/README.md +++ b/packages/toolkit/README.md @@ -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** — 18 predefined scenarios with expected failure +- **Scenario Evaluator** — 20 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. diff --git a/packages/toolkit/src/scenarios/__scenarios__/repeated-boot-notification.ts b/packages/toolkit/src/scenarios/__scenarios__/repeated-boot-notification.ts new file mode 100644 index 0000000..3fc43cb --- /dev/null +++ b/packages/toolkit/src/scenarios/__scenarios__/repeated-boot-notification.ts @@ -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 }, + }, + ], +}; diff --git a/packages/toolkit/src/scenarios/index.test.ts b/packages/toolkit/src/scenarios/index.test.ts index 4ec5d8e..af2fc8c 100644 --- a/packages/toolkit/src/scenarios/index.test.ts +++ b/packages/toolkit/src/scenarios/index.test.ts @@ -21,7 +21,9 @@ import { firmwareUpdateSuccessScenario, firmwareUpdateFailureScenario, refusedAuthorizationScenario, -} from './index.js'; + heartbeatTimeoutScenario, + repeatedBootNotificationScenario, +>} from './index.js'; import { parseTrace, buildSessionTimeline, detectFailures } from '../core/index.js'; // --------------------------------------------------------------------------- @@ -29,8 +31,8 @@ import { parseTrace, buildSessionTimeline, detectFailures } from '../core/index. // --------------------------------------------------------------------------- describe('scenario registry', () => { - it('exports exactly 18 scenarios', () => { - expect(scenarios).toHaveLength(18); + it('exports exactly 20 scenarios', () => { + expect(scenarios).toHaveLength(20); }); it('exports scenario names in order', () => { @@ -53,6 +55,8 @@ describe('scenario registry', () => { 'firmware-update-success', 'firmware-update-failure', 'refused-authorization', + 'heartbeat-timeout', + 'repeated-boot-notification', ]); }); @@ -83,6 +87,9 @@ describe('scenario registry', () => { expect(getScenario('unresponsive-csms')).toBe(unresponsiveCsmsScenario); expect(getScenario('firmware-update-success')).toBe(firmwareUpdateSuccessScenario); 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); }); it('getScenario returns undefined for unknown name', () => { @@ -230,7 +237,23 @@ describe('scenario engine integration', () => { 'ConcurrentTx', ]); }); -}); + + it('heartbeat-timeout: detects TIMEOUT_NO_HEARTBEAT', () => { + const trace = JSON.stringify(heartbeatTimeoutScenario.trace); + const result = parseTrace(trace); + const sessions = buildSessionTimeline(result.events); + const failures = detectFailures(result.events, sessions); + 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); + }); +>}); // --------------------------------------------------------------------------- // Synthetic data policy diff --git a/packages/toolkit/src/scenarios/index.ts b/packages/toolkit/src/scenarios/index.ts index 2676f61..45a3185 100644 --- a/packages/toolkit/src/scenarios/index.ts +++ b/packages/toolkit/src/scenarios/index.ts @@ -23,7 +23,9 @@ import unresponsiveCsms from './__scenarios__/unresponsive-csms.js'; 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'; +> // --------------------------------------------------------------------------- // Scenarios derived from core fixtures // --------------------------------------------------------------------------- @@ -72,7 +74,9 @@ const unresponsiveCsmsScenario: Scenario = unresponsiveCsms as unknown as Scenar const firmwareUpdateSuccessScenario: Scenario = firmwareUpdateSuccess as unknown as Scenario; 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; +> // --------------------------------------------------------------------------- // Registry // --------------------------------------------------------------------------- @@ -96,7 +100,9 @@ export const scenarios = [ firmwareUpdateSuccessScenario, firmwareUpdateFailureScenario, refusedAuthorizationScenario, -] as const; + heartbeatTimeoutScenario, + repeatedBootNotificationScenario, +>] as const; export const scenarioNames = [ 'normal-session', @@ -117,7 +123,9 @@ export const scenarioNames = [ 'firmware-update-success', 'firmware-update-failure', 'refused-authorization', -] as const; + 'heartbeat-timeout', + 'repeated-boot-notification', +>] as const; export { normalSessionScenario, @@ -138,7 +146,9 @@ export { firmwareUpdateSuccessScenario, firmwareUpdateFailureScenario, refusedAuthorizationScenario, -}; + heartbeatTimeoutScenario, + repeatedBootNotificationScenario, +>}; export { compareScenarioReports } from './compare.js'; export type { ScenarioComparison } from './compare.js'; diff --git a/tests/external-fixture/test.mjs b/tests/external-fixture/test.mjs index 79bcee4..964a886 100644 --- a/tests/external-fixture/test.mjs +++ b/tests/external-fixture/test.mjs @@ -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 === 18, - `18 scenarios exported (got ${scenarios.scenarios.length})`, + scenarios.scenarios.length === 20, + `20 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');