Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-meter-value-negative.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ocpp-debugkit/toolkit': patch
---

meter-value-negative: add a scenario that drives the negative branch of METER_VALUE_ANOMALY. The register reads -50 Wh, which is physically impossible, so the rule fires. meter-anomaly only covers the decreasing branch, so this is the missing half.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** — 23 predefined scenarios with expected failure
- **Scenario Evaluator** — 24 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.
Expand Down
2 changes: 1 addition & 1 deletion packages/toolkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** — 23 predefined scenarios with expected failure
- **Scenario Evaluator** — 24 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.
Expand Down
114 changes: 114 additions & 0 deletions packages/toolkit/src/scenarios/__scenarios__/meter-value-negative.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
export default {
name: 'meter-value-negative',
description:
'Cumulative energy register goes negative during a transaction. A negative reading is physically impossible and triggers METER_VALUE_ANOMALY.',
trace: {
traceId: 'scenario-meter-value-negative',
metadata: {
stationId: 'CS-SYNTHETIC-025',
ocppVersion: '1.6',
source: 'synthetic-scenario',
description:
'Station reports a negative cumulative register (-50 Wh) — the register can never go below zero.',
},
events: [
{
timestamp: '2026-03-01T09:00:00.000Z',
direction: 'CS_TO_CSMS',
message: [
2,
'msg-001',
'BootNotification',
{
chargePointVendor: 'SyntheticVendor',
chargePointModel: 'SM-100',
chargePointSerialNumber: 'CS-SYNTHETIC-025',
firmwareVersion: '1.0.0',
},
],
},
{
timestamp: '2026-03-01T09:00:00.500Z',
direction: 'CSMS_TO_CS',
message: [3, 'msg-001', { currentTime: '2026-03-01T09:00:00.500Z', interval: 300, status: 'Accepted' }],
},
{
timestamp: '2026-03-01T09:00:30.000Z',
direction: 'CS_TO_CSMS',
message: [2, 'msg-002', 'Authorize', { idTag: 'SYNTHETIC-TAG-025' }],
},
{
timestamp: '2026-03-01T09:00:30.500Z',
direction: 'CSMS_TO_CS',
message: [3, 'msg-002', { idTagInfo: { status: 'Accepted' } }],
},
{
timestamp: '2026-03-01T09:01:00.000Z',
direction: 'CS_TO_CSMS',
message: [
2,
'msg-003',
'StartTransaction',
{ connectorId: 1, idTag: 'SYNTHETIC-TAG-025', meterStart: 0 },
],
},
{
timestamp: '2026-03-01T09:01:00.500Z',
direction: 'CSMS_TO_CS',
message: [3, 'msg-003', { idTagInfo: { status: 'Accepted' }, transactionId: 100025 }],
},
{
timestamp: '2026-03-01T09:02:00.000Z',
direction: 'CS_TO_CSMS',
message: [
2,
'msg-004',
'MeterValues',
{
connectorId: 1,
transactionId: 100025,
meterValue: [
{
sampledValue: [
{ value: '-50', measurand: 'Energy.Active.Import.Register', unit: 'Wh' },
],
},
],
},
],
},
{
timestamp: '2026-03-01T09:02:00.500Z',
direction: 'CSMS_TO_CS',
message: [3, 'msg-004', {}],
},
{
timestamp: '2026-03-01T09:05:00.000Z',
direction: 'CS_TO_CSMS',
message: [
2,
'msg-005',
'StopTransaction',
{
transactionId: 100025,
idTag: 'SYNTHETIC-TAG-025',
meterStop: 0,
reason: 'Local',
},
],
},
{
timestamp: '2026-03-01T09:05:00.500Z',
direction: 'CSMS_TO_CS',
message: [3, 'msg-005', { idTagInfo: { status: 'Accepted' } }],
},
],
},
expectedFailures: ['METER_VALUE_ANOMALY'],
assertions: [
{
type: 'failure_count',
params: { code: 'METER_VALUE_ANOMALY', min: 1 },
},
],
};
15 changes: 13 additions & 2 deletions packages/toolkit/src/scenarios/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
meterValueZeroScenario,
statusTransitionsLegalScenario,
bootOutsideRepeatWindowScenario,
meterValueNegativeScenario,
} from './index.js';
import { parseTrace, buildSessionTimeline, detectFailures } from '../core/index.js';

Expand All @@ -34,8 +35,8 @@ import { parseTrace, buildSessionTimeline, detectFailures } from '../core/index.
// ---------------------------------------------------------------------------

describe('scenario registry', () => {
it('exports exactly 23 scenarios', () => {
expect(scenarios).toHaveLength(23);
it('exports exactly 24 scenarios', () => {
expect(scenarios).toHaveLength(24);
});

it('exports scenario names in order', () => {
Expand Down Expand Up @@ -63,6 +64,7 @@ describe('scenario registry', () => {
'meter-value-zero',
'status-transitions-legal',
'boot-outside-repeat-window',
'meter-value-negative',
]);
});

Expand Down Expand Up @@ -99,6 +101,7 @@ describe('scenario registry', () => {
expect(getScenario('meter-value-zero')).toBe(meterValueZeroScenario);
expect(getScenario('status-transitions-legal')).toBe(statusTransitionsLegalScenario);
expect(getScenario('boot-outside-repeat-window')).toBe(bootOutsideRepeatWindowScenario);
expect(getScenario('meter-value-negative')).toBe(meterValueNegativeScenario);
});

it('getScenario returns undefined for unknown name', () => {
Expand Down Expand Up @@ -278,6 +281,14 @@ describe('scenario engine integration', () => {
const failures = detectFailures(result.events, sessions);
expect(failures).toHaveLength(0);
});

it('meter-value-negative: detects METER_VALUE_ANOMALY', () => {
const trace = JSON.stringify(meterValueNegativeScenario.trace);
const result = parseTrace(trace);
const sessions = buildSessionTimeline(result.events);
const failures = detectFailures(result.events, sessions);
expect(failures.some((f) => f.code === 'METER_VALUE_ANOMALY')).toBe(true);
});
});

// ---------------------------------------------------------------------------
Expand Down
5 changes: 5 additions & 0 deletions packages/toolkit/src/scenarios/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import repeatedBootNotification from './__scenarios__/repeated-boot-notification
import meterValueZero from './__scenarios__/meter-value-zero.js';
import statusTransitionsLegal from './__scenarios__/status-transitions-legal.js';
import bootOutsideRepeatWindow from './__scenarios__/boot-outside-repeat-window.js';
import meterValueNegative from './__scenarios__/meter-value-negative.js';

// ---------------------------------------------------------------------------
// Scenarios derived from core fixtures
Expand Down Expand Up @@ -82,6 +83,7 @@ const repeatedBootNotificationScenario: Scenario = repeatedBootNotification as u
const meterValueZeroScenario: Scenario = meterValueZero as unknown as Scenario;
const statusTransitionsLegalScenario: Scenario = statusTransitionsLegal as unknown as Scenario;
const bootOutsideRepeatWindowScenario: Scenario = bootOutsideRepeatWindow as unknown as Scenario;
const meterValueNegativeScenario: Scenario = meterValueNegative as unknown as Scenario;

// ---------------------------------------------------------------------------
// Registry
Expand Down Expand Up @@ -111,6 +113,7 @@ export const scenarios = [
meterValueZeroScenario,
statusTransitionsLegalScenario,
bootOutsideRepeatWindowScenario,
meterValueNegativeScenario,
] as const;

export const scenarioNames = [
Expand All @@ -137,6 +140,7 @@ export const scenarioNames = [
'meter-value-zero',
'status-transitions-legal',
'boot-outside-repeat-window',
'meter-value-negative',
] as const;

export {
Expand All @@ -163,6 +167,7 @@ export {
meterValueZeroScenario,
statusTransitionsLegalScenario,
bootOutsideRepeatWindowScenario,
meterValueNegativeScenario,
};

export { compareScenarioReports } from './compare.js';
Expand Down
4 changes: 2 additions & 2 deletions tests/external-fixture/test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 === 23,
`23 scenarios exported (got ${scenarios.scenarios.length})`,
scenarios.scenarios.length === 24,
`24 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');
Expand Down