Skip to content
Merged
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
54 changes: 49 additions & 5 deletions packages/toolkit/src/cli/commands/scenario.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
/**
* `ocpp-debugkit scenario list` and `ocpp-debugkit scenario run <name>` commands.
*
* `scenario run` runs built-in static fixtures through the local analysis
* engine only — it is NOT active endpoint testing, WebSocket simulation,
* live station/CSMS testing, or the v0.2 scenario evaluator.
* `scenario run` runs built-in or external static fixtures through the local
* analysis engine. It is NOT active endpoint testing, WebSocket simulation,
* live station/CSMS testing, or an active scenario runner.
*/

import { parseTrace, buildSessionTimeline, detectFailures, ParseError } from '../../core/index.js';
import type { Scenario } from '../../core/index.js';
import { scenarioNames, getScenario } from '../../scenarios/index.js';
import { CliError } from '../utils.js';
import { CliError, readTraceFile } from '../utils.js';

export function scenarioListCommand(): void {
console.log('');
Expand All @@ -32,6 +33,7 @@ export function scenarioListCommand(): void {
}

console.log('Run with: ocpp-debugkit scenario run <name>');
console.log(' ocpp-debugkit scenario run --file <path>');
console.log('');
}

Expand All @@ -44,6 +46,48 @@ export async function scenarioRunCommand(name: string): Promise<void> {
);
}

await runScenarioInternal(scenario);
}

/**
* `ocpp-debugkit scenario run --file <path>` — run an external scenario file
* through the analysis engine.
*
* The external file must be a JSON object with: name, description, trace, and
* expectedFailures fields (same shape as built-in scenarios).
*/
export async function scenarioRunFileCommand(filePath: string): Promise<void> {
const content = readTraceFile(filePath);

let scenario: Scenario;
try {
const parsed = JSON.parse(content) as unknown;
if (
typeof parsed !== 'object' ||
parsed === null ||
!('trace' in parsed) ||
!('name' in parsed)
) {
throw new CliError('Invalid scenario file: must contain "name" and "trace" fields.');
}
scenario = parsed as Scenario;
} catch (e) {
if (e instanceof CliError) throw e;
throw new CliError('Invalid JSON in scenario file.');
}

if (!scenario.expectedFailures) {
scenario.expectedFailures = [];
}

await runScenarioInternal(scenario);
}

// ---------------------------------------------------------------------------
// Shared scenario run logic
// ---------------------------------------------------------------------------

async function runScenarioInternal(scenario: Scenario): Promise<void> {
console.log('');
console.log('═'.repeat(60));
console.log(` Running Scenario: ${scenario.name}`);
Expand Down Expand Up @@ -71,7 +115,7 @@ export async function scenarioRunCommand(name: string): Promise<void> {

// Compare detected vs expected failures
const detectedCodes = new Set<string>(failures.map((f) => f.code));
const expectedCodes = new Set<string>(scenario.expectedFailures);
const expectedCodes = new Set<string>(scenario.expectedFailures as string[]);

const correctlyDetected = [...expectedCodes].filter((c) => detectedCodes.has(c));
const unexpectedFailures = [...detectedCodes].filter((c) => !expectedCodes.has(c));
Expand Down
21 changes: 16 additions & 5 deletions packages/toolkit/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
import { Command } from 'commander';
import { inspectCommand } from './commands/inspect.js';
import { reportCommand } from './commands/report.js';
import { scenarioListCommand, scenarioRunCommand } from './commands/scenario.js';
import {
scenarioListCommand,
scenarioRunCommand,
scenarioRunFileCommand,
} from './commands/scenario.js';

const program = new Command();

Expand Down Expand Up @@ -46,10 +50,17 @@ scenarioCmd
});

scenarioCmd
.command('run <name>')
.description('Run a scenario through the analysis engine')
.action(async (name: string) => {
await scenarioRunCommand(name);
.command('run [name]')
.description('Run a scenario through the analysis engine (built-in or --file <path>)')
.option('-f, --file <path>', 'Run an external scenario file instead of a built-in scenario')
.action(async (name: string | undefined, options: { file?: string }) => {
if (options.file) {
await scenarioRunFileCommand(options.file);
} else if (name) {
await scenarioRunCommand(name);
} else {
throw new Error('Either a scenario name or --file <path> is required.');
}
});

program.parse();
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
export default {
name: 'diagnostics-failure',
description:
'DiagnosticsStatusNotification with DiagnosisFailed status indicating a hardware diagnostics failure. Expects DIAGNOSTICS_FAILURE failure.',
trace: {
traceId: 'scenario-diagnostics-failure',
metadata: {
stationId: 'CS-SYNTHETIC-010',
ocppVersion: '1.6',
source: 'synthetic-scenario',
description:
'Station reports a diagnostics failure via DiagnosticsStatusNotification with status DiagnosisFailed.',
},
events: [
{
timestamp: '2024-01-15T06:00:00.000Z',
direction: 'CS_TO_CSMS',
message: [
2,
'msg-001',
'BootNotification',
{
chargePointVendor: 'SyntheticVendor',
chargePointModel: 'SM-100',
chargePointSerialNumber: 'CS-SYNTHETIC-010',
firmwareVersion: '1.0.0',
},
],
},
{
timestamp: '2024-01-15T06:00:00.500Z',
direction: 'CSMS_TO_CS',
message: [
3,
'msg-001',
{
currentTime: '2024-01-15T06:00:00.500Z',
interval: 300,
status: 'Accepted',
},
],
},
{
timestamp: '2024-01-15T06:01:00.000Z',
direction: 'CS_TO_CSMS',
message: [
2,
'msg-002',
'StatusNotification',
{
connectorId: 0,
status: 'Available',
errorCode: 'NoError',
},
],
},
{
timestamp: '2024-01-15T06:01:00.500Z',
direction: 'CSMS_TO_CS',
message: [3, 'msg-002', {}],
},
{
timestamp: '2024-01-15T06:07:30.000Z',
direction: 'CS_TO_CSMS',
message: [2, 'msg-hb-1', 'Heartbeat', {}],
},
{
timestamp: '2024-01-15T06:07:30.500Z',
direction: 'CSMS_TO_CS',
message: [3, 'msg-hb-1', { currentTime: '2024-01-15T06:07:30.500Z' }],
},
{
timestamp: '2024-01-15T06:12:00.000Z',
direction: 'CS_TO_CSMS',
message: [
2,
'msg-003',
'DiagnosticsStatusNotification',
{
status: 'DiagnosisFailed',
},
],
},
{
timestamp: '2024-01-15T06:12:00.500Z',
direction: 'CSMS_TO_CS',
message: [3, 'msg-003', {}],
},
],
},
expectedFailures: ['DIAGNOSTICS_FAILURE'],
};
Loading
Loading