From fb39fbfefecad3906a11c7e98831ed13951c83e7 Mon Sep 17 00:00:00 2001 From: sepehr-safari Date: Wed, 8 Jul 2026 02:16:01 +0300 Subject: [PATCH] feat(cli): scaffold + inspect + report + scenario commands + package config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ocpp-debugkit inspect — parse + analyze + output summary - ocpp-debugkit report — generate Markdown report (stdout or --output) - ocpp-debugkit scenario list — list all 5 built-in scenarios - ocpp-debugkit scenario run — run scenario through analysis engine, compare detected vs expected failures - Path safety: validated file paths, size limits - Input validation: safe parsing, non-sensitive error messages - 17 integration tests (execa-based) - Converted JSON fixtures to TS modules (fixes Node.js ESM JSON import issue) Closes #25 --- .changeset/cli-package.md | 15 + .github/workflows/ci.yml | 6 +- CURRENT_STATE.md | 23 +- packages/cli/package.json | 54 ++++ packages/cli/src/cli.test.ts | 174 ++++++++++++ packages/cli/src/commands/inspect.ts | 82 ++++++ packages/cli/src/commands/report.ts | 77 ++++++ packages/cli/src/commands/scenario.ts | 120 ++++++++ packages/cli/src/index.ts | 55 ++++ packages/cli/src/utils.ts | 50 ++++ packages/cli/tsconfig.json | 9 + .../src/__fixtures__/connector-fault.json | 230 --------------- .../core/src/__fixtures__/connector-fault.ts | 231 ++++++++++++++++ .../core/src/__fixtures__/failed-auth.json | 172 ------------ packages/core/src/__fixtures__/failed-auth.ts | 173 ++++++++++++ .../core/src/__fixtures__/normal-session.json | 260 ----------------- .../core/src/__fixtures__/normal-session.ts | 261 ++++++++++++++++++ packages/core/src/fixtures/index.ts | 6 +- .../src/__scenarios__/station-offline.json | 167 ----------- .../src/__scenarios__/station-offline.ts | 169 ++++++++++++ .../__scenarios__/unexpected-stop-reason.json | 215 --------------- .../__scenarios__/unexpected-stop-reason.ts | 217 +++++++++++++++ packages/scenarios/src/index.ts | 4 +- pnpm-lock.yaml | 149 ++++++++++ 24 files changed, 1863 insertions(+), 1056 deletions(-) create mode 100644 .changeset/cli-package.md create mode 100644 packages/cli/package.json create mode 100644 packages/cli/src/cli.test.ts create mode 100644 packages/cli/src/commands/inspect.ts create mode 100644 packages/cli/src/commands/report.ts create mode 100644 packages/cli/src/commands/scenario.ts create mode 100644 packages/cli/src/index.ts create mode 100644 packages/cli/src/utils.ts create mode 100644 packages/cli/tsconfig.json delete mode 100644 packages/core/src/__fixtures__/connector-fault.json create mode 100644 packages/core/src/__fixtures__/connector-fault.ts delete mode 100644 packages/core/src/__fixtures__/failed-auth.json create mode 100644 packages/core/src/__fixtures__/failed-auth.ts delete mode 100644 packages/core/src/__fixtures__/normal-session.json create mode 100644 packages/core/src/__fixtures__/normal-session.ts delete mode 100644 packages/scenarios/src/__scenarios__/station-offline.json create mode 100644 packages/scenarios/src/__scenarios__/station-offline.ts delete mode 100644 packages/scenarios/src/__scenarios__/unexpected-stop-reason.json create mode 100644 packages/scenarios/src/__scenarios__/unexpected-stop-reason.ts diff --git a/.changeset/cli-package.md b/.changeset/cli-package.md new file mode 100644 index 0000000..a240edd --- /dev/null +++ b/.changeset/cli-package.md @@ -0,0 +1,15 @@ +--- +'@ocpp-debugkit/cli': minor +--- + +Create CLI package with inspect, report, and scenario commands. + +- `ocpp-debugkit inspect ` — parse + analyze + output summary +- `ocpp-debugkit report ` — generate Markdown report (stdout or --output) +- `ocpp-debugkit scenario list` — list all 5 built-in scenarios +- `ocpp-debugkit scenario run ` — run scenario through analysis engine, + compare detected vs expected failures +- Path safety: validated file paths, size limits +- Input validation: safe parsing, non-sensitive error messages +- 17 integration tests (execa-based) +- Converted JSON fixtures to TS modules (fixes Node.js ESM JSON import issue) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0428b5a..0166d84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,8 +44,8 @@ jobs: - name: Typecheck run: pnpm typecheck - - name: Test - run: pnpm test - - name: Build run: pnpm build + + - name: Test + run: pnpm test diff --git a/CURRENT_STATE.md b/CURRENT_STATE.md index 2b0d5e9..325535c 100644 --- a/CURRENT_STATE.md +++ b/CURRENT_STATE.md @@ -99,21 +99,36 @@ report exported. CLI and web inspector. - ✅ Each scenario's `expectedFailures` aligns with v0.1 detection rules - ✅ 21 tests (registry, engine integration, synthetic data policy) -### Reporter Package (in progress — this PR) +### Reporter Package (PR #40) - ✅ `packages/reporter/` — new package - ✅ `generateMarkdownReport()` — session overview, timeline summary, failures, suggested steps, event appendix - ✅ `AnalysisResult` input type - ✅ 11 tests (structure, failure inclusion, readability, metadata, severity) +### CLI Package (in progress — this PR) + +- ✅ `packages/cli/` — new package +- ✅ `ocpp-debugkit inspect ` — parse + analyze + output +- ✅ `ocpp-debugkit report ` — generate Markdown report (stdout or file) +- ✅ `ocpp-debugkit scenario list` — list all 5 scenarios +- ✅ `ocpp-debugkit scenario run ` — run scenario through analysis engine, compare detected vs expected +- ✅ Path safety: validated file paths, size limits +- ✅ Input validation: safe parsing, non-sensitive errors +- ✅ 17 integration tests (execa-based) +- ✅ Converted JSON fixtures to TS modules (fixes Node.js ESM JSON import issue) + ## What's Next 1. **Issue #20** → complete (PR #33): data model + parser + normalizer 2. **Issue #21** → complete (PR #34): timeline + detection + summarizer + validator 3. **Issue #22** → complete (PR #35): public API export + package config 4. **Issue #23** → complete (PR #39): scenarios package (format + 5 initial scenarios) -5. **Issue #24** (this PR) → complete: reporter package (Markdown report generator) -6. **Issue #25**: CLI package (scaffold + inspect + report + scenario commands) +5. **Issue #24** → complete (PR #40): reporter package (Markdown report generator) +6. **Issue #25** (this PR) → complete: CLI package (scaffold + inspect + report + scenario commands) +7. **Issue #26**: Next.js app scaffold (Next.js + Nextra + Tailwind + Shadcn) +8. **Issue #27**: Landing page +9. **Issue #28**: Inspector (trace input + timeline + message inspector) ## Known Blockers / Decisions Pending @@ -126,7 +141,7 @@ report exported. CLI and web inspector. | `@ocpp-debugkit/core` | in progress (package config finalized, ready for downstream) | 0.0.0 | | `@ocpp-debugkit/scenarios` | in progress (5 scenarios + registry) | 0.0.0 | | `@ocpp-debugkit/reporter` | in progress (Markdown generator) | 0.0.0 | -| `@ocpp-debugkit/cli` | not started | — | +| `@ocpp-debugkit/cli` | in progress (inspect + report + scenario) | 0.0.0 | | `@ocpp-debugkit/replay` | not started | — | | `@ocpp-debugkit/react` | not started | — | | `apps/web` | not started | — | diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..8f8bac0 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,54 @@ +{ + "name": "@ocpp-debugkit/cli", + "version": "0.0.0", + "description": "Command-line interface for OCPP DebugKit — inspect traces, generate reports, run scenarios.", + "license": "Apache-2.0", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "bin": { + "ocpp-debugkit": "./dist/index.js" + }, + "files": [ + "dist", + "README.md", + "NOTICE" + ], + "keywords": [ + "ocpp", + "ocpp1.6", + "ev-charging", + "cli", + "debugging", + "trace-inspector" + ], + "repository": { + "type": "git", + "url": "https://github.com/ocpp-debugkit/ocpp-debugkit.git", + "directory": "packages/cli" + }, + "homepage": "https://github.com/ocpp-debugkit/ocpp-debugkit/tree/main/packages/cli", + "bugs": { + "url": "https://github.com/ocpp-debugkit/ocpp-debugkit/issues" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "clean": "rm -rf dist .turbo" + }, + "dependencies": { + "@ocpp-debugkit/core": "workspace:*", + "@ocpp-debugkit/scenarios": "workspace:*", + "@ocpp-debugkit/reporter": "workspace:*", + "commander": "^13.0.0" + }, + "devDependencies": { + "typescript": "^5.7.0", + "vitest": "^3.0.0", + "execa": "^9.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/cli/src/cli.test.ts b/packages/cli/src/cli.test.ts new file mode 100644 index 0000000..12d36c9 --- /dev/null +++ b/packages/cli/src/cli.test.ts @@ -0,0 +1,174 @@ +import { describe, it, expect } from 'vitest'; +import { execa } from 'execa'; +import { writeFileSync, mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { fixtures } from '@ocpp-debugkit/core'; + +const CLI_PATH = join(process.cwd(), 'packages/cli/dist/index.js'); + +// Helper: run the CLI with given args +async function runCli( + ...args: string[] +): Promise<{ stdout: string; stderr: string; exitCode: number }> { + try { + const result = await execa('node', [CLI_PATH, ...args]); + return { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode ?? 0 }; + } catch (e) { + const error = e as { stdout?: string; stderr?: string; exitCode?: number }; + return { + stdout: error.stdout ?? '', + stderr: error.stderr ?? '', + exitCode: error.exitCode ?? 1, + }; + } +} + +// Helper: write a trace to a temp file +function writeTempTrace(trace: unknown): string { + const dir = mkdtempSync(join(tmpdir(), 'ocpp-test-')); + const filePath = join(dir, 'trace.json'); + writeFileSync(filePath, JSON.stringify(trace), 'utf8'); + return filePath; +} + +describe('CLI integration', () => { + describe('--version', () => { + it('outputs version', async () => { + const result = await runCli('--version'); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('0.0.0'); + }); + }); + + describe('--help', () => { + it('shows help with all commands', async () => { + const result = await runCli('--help'); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('inspect'); + expect(result.stdout).toContain('report'); + expect(result.stdout).toContain('scenario'); + }); + }); + + describe('inspect', () => { + it('inspects a normal session trace', async () => { + const filePath = writeTempTrace(fixtures.normalSession); + const result = await runCli('inspect', filePath); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Trace Inspection'); + expect(result.stdout).toContain('Sessions: 1'); + expect(result.stdout).toContain('No failures detected'); + }); + + it('inspects a failed-auth trace and shows failures', async () => { + const filePath = writeTempTrace(fixtures.failedAuth); + const result = await runCli('inspect', filePath); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('FAILED_AUTHORIZATION'); + }); + + it('inspects a connector-fault trace and shows failures', async () => { + const filePath = writeTempTrace(fixtures.connectorFault); + const result = await runCli('inspect', filePath); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('CONNECTOR_FAULT'); + }); + + it('handles missing file gracefully', async () => { + const result = await runCli('inspect', '/nonexistent/file.json'); + expect(result.exitCode).not.toBe(0); + }); + + it('handles invalid JSON gracefully', async () => { + const dir = mkdtempSync(join(tmpdir(), 'ocpp-test-')); + const filePath = join(dir, 'bad.json'); + writeFileSync(filePath, '{ invalid json }', 'utf8'); + const result = await runCli('inspect', filePath); + expect(result.exitCode).not.toBe(0); + }); + }); + + describe('report', () => { + it('generates a Markdown report to stdout', async () => { + const filePath = writeTempTrace(fixtures.normalSession); + const result = await runCli('report', filePath); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('OCPP DebugKit — Trace Analysis Report'); + expect(result.stdout).toContain('## Session Overview'); + expect(result.stdout).toContain('## Failures'); + }); + + it('generates a report with failures', async () => { + const filePath = writeTempTrace(fixtures.failedAuth); + const result = await runCli('report', filePath); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('FAILED_AUTHORIZATION'); + expect(result.stdout).toContain('## Suggested Next Steps'); + }); + + it('writes report to a file with --output', async () => { + const filePath = writeTempTrace(fixtures.normalSession); + const dir = mkdtempSync(join(tmpdir(), 'ocpp-test-')); + const outputPath = join(dir, 'report.md'); + const result = await runCli('report', filePath, '--output', outputPath); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Report written to'); + }); + }); + + describe('scenario list', () => { + it('lists all available scenarios', async () => { + const result = await runCli('scenario', 'list'); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('normal-session'); + expect(result.stdout).toContain('failed-auth'); + expect(result.stdout).toContain('connector-fault'); + expect(result.stdout).toContain('station-offline'); + expect(result.stdout).toContain('unexpected-stop-reason'); + }); + }); + + describe('scenario run', () => { + it('runs normal-session scenario and passes', async () => { + const result = await runCli('scenario', 'run', 'normal-session'); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Running Scenario: normal-session'); + expect(result.stdout).toContain('No failures expected, none detected'); + expect(result.stdout).toContain('PASS'); + }); + + it('runs failed-auth scenario and detects FAILED_AUTHORIZATION', async () => { + const result = await runCli('scenario', 'run', 'failed-auth'); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('FAILED_AUTHORIZATION'); + expect(result.stdout).toContain('PASS'); + }); + + it('runs connector-fault scenario and detects CONNECTOR_FAULT', async () => { + const result = await runCli('scenario', 'run', 'connector-fault'); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('CONNECTOR_FAULT'); + expect(result.stdout).toContain('PASS'); + }); + + it('runs station-offline scenario and detects STATION_OFFLINE_DURING_SESSION', async () => { + const result = await runCli('scenario', 'run', 'station-offline'); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('STATION_OFFLINE_DURING_SESSION'); + expect(result.stdout).toContain('PASS'); + }); + + it('runs unexpected-stop-reason scenario (no failures)', async () => { + const result = await runCli('scenario', 'run', 'unexpected-stop-reason'); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('No failures expected, none detected'); + expect(result.stdout).toContain('PASS'); + }); + + it('handles unknown scenario name', async () => { + const result = await runCli('scenario', 'run', 'nonexistent'); + expect(result.exitCode).not.toBe(0); + }); + }); +}); diff --git a/packages/cli/src/commands/inspect.ts b/packages/cli/src/commands/inspect.ts new file mode 100644 index 0000000..6e60907 --- /dev/null +++ b/packages/cli/src/commands/inspect.ts @@ -0,0 +1,82 @@ +/** + * `ocpp-debugkit inspect ` — parse and analyze an OCPP trace file. + */ + +import { + parseTrace, + buildSessionTimeline, + detectFailures, + summarizeSessions, + ParseError, +} from '@ocpp-debugkit/core'; +import { CliError, readTraceFile } from '../utils.js'; + +export interface InspectOptions { + format: string; +} + +export async function inspectCommand(file: string, _options: InspectOptions): Promise { + const content = readTraceFile(file); + + let result; + try { + result = parseTrace(content); + } catch (e) { + if (e instanceof ParseError) { + throw new CliError(e.message); + } + if (e instanceof Error) { + throw new CliError(e.message); + } + throw new CliError('Unknown error during parsing'); + } + + const sessions = buildSessionTimeline(result.events); + const failures = detectFailures(result.events, sessions); + const summaries = summarizeSessions(sessions, failures); + + // Output to stdout + console.log(''); + console.log('═'.repeat(60)); + console.log(' OCPP DebugKit — Trace Inspection'); + console.log('═'.repeat(60)); + console.log(''); + + console.log(` Events: ${result.events.length}`); + console.log(` Sessions: ${sessions.length}`); + console.log(` Failures: ${failures.length}`); + console.log(` Warnings: ${result.warnings.length}`); + console.log(''); + + if (result.warnings.length > 0) { + console.log('── Parse Warnings ──────────────────────────────────────'); + for (const w of result.warnings) { + console.log(` ⚠ ${w.message}`); + } + console.log(''); + } + + console.log('── Sessions ────────────────────────────────────────────'); + for (const summary of summaries) { + const duration = + summary.durationMs !== null ? `${(summary.durationMs / 1000).toFixed(1)}s` : 'Unknown'; + console.log( + ` ${summary.sessionId} | station=${summary.stationId} | conn=${summary.connectorId ?? '-'} | tx=${summary.transactionId ?? '-'} | ${summary.status} | ${duration} | ${summary.eventCount} events | ${summary.failureCount} failures`, + ); + } + console.log(''); + + if (failures.length > 0) { + console.log('── Failures ────────────────────────────────────────────'); + for (const failure of failures) { + console.log(` [${failure.severity.toUpperCase()}] ${failure.code}`); + console.log(` ${failure.description}`); + console.log(` Events: ${failure.eventIds.join(', ')}`); + } + console.log(''); + } else { + console.log('── Failures ────────────────────────────────────────────'); + console.log(' No failures detected. ✅'); + console.log(''); + } +} diff --git a/packages/cli/src/commands/report.ts b/packages/cli/src/commands/report.ts new file mode 100644 index 0000000..8bd2990 --- /dev/null +++ b/packages/cli/src/commands/report.ts @@ -0,0 +1,77 @@ +/** + * `ocpp-debugkit report ` — generate a report from an OCPP trace file. + */ + +import { + parseTrace, + buildSessionTimeline, + detectFailures, + summarizeSessions, + ParseError, + type Trace, +} from '@ocpp-debugkit/core'; +import { generateMarkdownReport } from '@ocpp-debugkit/reporter'; +import { CliError, readTraceFile } from '../utils.js'; + +export interface ReportOptions { + format: string; + output?: string; +} + +export async function reportCommand(file: string, options: ReportOptions): Promise { + const content = readTraceFile(file); + + // Parse the trace to get events + let result; + try { + result = parseTrace(content); + } catch (e) { + if (e instanceof ParseError) { + throw new CliError(e.message); + } + if (e instanceof Error) { + throw new CliError(e.message); + } + throw new CliError('Unknown error during parsing'); + } + + // Also extract metadata from the original JSON (if JSON Object format) + let metadata: + | { + traceId?: string; + stationId?: string; + ocppVersion?: string; + source?: string; + description?: string; + } + | undefined; + try { + const parsed = JSON.parse(content); + if (parsed && typeof parsed === 'object' && 'metadata' in parsed) { + metadata = (parsed as Trace).metadata; + } + } catch { + // JSONL or bare array — no metadata + } + + const sessions = buildSessionTimeline(result.events); + const failures = detectFailures(result.events, sessions); + const summaries = summarizeSessions(sessions, failures); + + const report = generateMarkdownReport({ + events: result.events, + sessions, + failures, + summaries, + warnings: result.warnings, + metadata, + }); + + if (options.output) { + const { writeFileSync } = await import('node:fs'); + writeFileSync(options.output, report, 'utf8'); + console.log(`Report written to ${options.output}`); + } else { + console.log(report); + } +} diff --git a/packages/cli/src/commands/scenario.ts b/packages/cli/src/commands/scenario.ts new file mode 100644 index 0000000..3f2a660 --- /dev/null +++ b/packages/cli/src/commands/scenario.ts @@ -0,0 +1,120 @@ +/** + * `ocpp-debugkit scenario list` and `ocpp-debugkit scenario run ` 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. + */ + +import { parseTrace, buildSessionTimeline, detectFailures, ParseError } from '@ocpp-debugkit/core'; +import { scenarioNames, getScenario } from '@ocpp-debugkit/scenarios'; +import { CliError } from '../utils.js'; + +export function scenarioListCommand(): void { + console.log(''); + console.log('═'.repeat(60)); + console.log(' Available Scenarios'); + console.log('═'.repeat(60)); + console.log(''); + + for (const name of scenarioNames) { + const scenario = getScenario(name); + if (scenario) { + console.log(` ${name}`); + console.log(` ${scenario.description}`); + if (scenario.expectedFailures.length > 0) { + console.log(` Expected failures: ${scenario.expectedFailures.join(', ')}`); + } else { + console.log(` Expected failures: none`); + } + console.log(''); + } + } + + console.log('Run with: ocpp-debugkit scenario run '); + console.log(''); +} + +export async function scenarioRunCommand(name: string): Promise { + const scenario = getScenario(name); + + if (!scenario) { + throw new CliError( + `Scenario "${name}" not found. Run "ocpp-debugkit scenario list" to see available scenarios.`, + ); + } + + console.log(''); + console.log('═'.repeat(60)); + console.log(` Running Scenario: ${scenario.name}`); + console.log('═'.repeat(60)); + console.log(` ${scenario.description}`); + console.log(''); + + const traceContent = JSON.stringify(scenario.trace); + + let result; + try { + result = parseTrace(traceContent); + } catch (e) { + if (e instanceof ParseError) { + throw new CliError(e.message); + } + if (e instanceof Error) { + throw new CliError(e.message); + } + throw new CliError('Unknown error during parsing'); + } + + const sessions = buildSessionTimeline(result.events); + const failures = detectFailures(result.events, sessions); + + // Compare detected vs expected failures + const detectedCodes = new Set(failures.map((f) => f.code)); + const expectedCodes = new Set(scenario.expectedFailures); + + const correctlyDetected = [...expectedCodes].filter((c) => detectedCodes.has(c)); + const unexpectedFailures = [...detectedCodes].filter((c) => !expectedCodes.has(c)); + const missedFailures = [...expectedCodes].filter((c) => !detectedCodes.has(c)); + + console.log('── Results ─────────────────────────────────────────────'); + console.log(` Events: ${result.events.length}`); + console.log(` Sessions: ${sessions.length}`); + console.log(` Failures: ${failures.length}`); + console.log(''); + + if (correctlyDetected.length > 0) { + console.log(` ✅ Correctly detected: ${correctlyDetected.join(', ')}`); + } + if (unexpectedFailures.length > 0) { + console.log(` ⚠ Unexpected failures: ${unexpectedFailures.join(', ')}`); + } + if (missedFailures.length > 0) { + console.log(` ❌ Missed failures: ${missedFailures.join(', ')}`); + } + if ( + correctlyDetected.length === 0 && + unexpectedFailures.length === 0 && + missedFailures.length === 0 + ) { + console.log(' ✅ No failures expected, none detected.'); + } + console.log(''); + + if (failures.length > 0) { + console.log('── Failure Details ────────────────────────────────────'); + for (const failure of failures) { + console.log(` [${failure.severity.toUpperCase()}] ${failure.code}`); + console.log(` ${failure.description}`); + } + console.log(''); + } + + // Exit code: 0 if all expected failures detected and no unexpected ones + if (missedFailures.length > 0 || unexpectedFailures.length > 0) { + console.log(' Result: FAIL ❌'); + } else { + console.log(' Result: PASS ✅'); + } + console.log(''); +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 0000000..b6aec3a --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,55 @@ +#!/usr/bin/env node +/** + * OCPP DebugKit CLI — command-line interface for inspecting OCPP traces, + * generating reports, and running scenarios. + */ + +import { Command } from 'commander'; +import { inspectCommand } from './commands/inspect.js'; +import { reportCommand } from './commands/report.js'; +import { scenarioListCommand, scenarioRunCommand } from './commands/scenario.js'; + +const program = new Command(); + +program + .name('ocpp-debugkit') + .description('Debug OCPP charging sessions — inspect traces, generate reports, run scenarios.') + .version('0.0.0'); + +// inspect +program + .command('inspect ') + .description('Parse and analyze an OCPP trace file') + .option('--format ', 'Output format (text)', 'text') + .action(async (file: string, options: { format: string }) => { + await inspectCommand(file, options); + }); + +// report +program + .command('report ') + .description('Generate a report from an OCPP trace file') + .option('-f, --format ', 'Report format (markdown)', 'markdown') + .option('-o, --output ', 'Write report to file (default: stdout)') + .action(async (file: string, options: { format: string; output?: string }) => { + await reportCommand(file, options); + }); + +// scenario +const scenarioCmd = program.command('scenario').description('Run predefined scenarios'); + +scenarioCmd + .command('list') + .description('List all available scenarios') + .action(() => { + scenarioListCommand(); + }); + +scenarioCmd + .command('run ') + .description('Run a scenario through the analysis engine') + .action(async (name: string) => { + await scenarioRunCommand(name); + }); + +program.parse(); diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts new file mode 100644 index 0000000..284f518 --- /dev/null +++ b/packages/cli/src/utils.ts @@ -0,0 +1,50 @@ +/** + * Shared utilities for CLI commands. + */ + +import { readFileSync, statSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { MAX_INPUT_SIZE_BYTES } from '@ocpp-debugkit/core'; + +/** Maximum file size for trace input (10 MB). */ +const MAX_FILE_SIZE = MAX_INPUT_SIZE_BYTES; + +/** Error thrown when a file is invalid or unreadable. */ +export class CliError extends Error { + constructor(message: string) { + super(message); + this.name = 'CliError'; + } +} + +/** + * Read a trace file safely with path validation and size limits. + * @throws {CliError} if the file is missing, too large, or unreadable. + */ +export function readTraceFile(filePath: string): string { + // Resolve and validate the path (prevent path traversal in edge cases) + const resolved = resolve(filePath); + + let stats; + try { + stats = statSync(resolved); + } catch { + throw new CliError(`File not found: ${filePath}`); + } + + if (!stats.isFile()) { + throw new CliError(`Not a file: ${filePath}`); + } + + if (stats.size > MAX_FILE_SIZE) { + throw new CliError( + `File size (${stats.size} bytes) exceeds the maximum allowed size (${MAX_FILE_SIZE} bytes).`, + ); + } + + try { + return readFileSync(resolved, 'utf8'); + } catch { + throw new CliError(`Failed to read file: ${filePath}`); + } +} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 0000000..dd1cdcb --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/core/src/__fixtures__/connector-fault.json b/packages/core/src/__fixtures__/connector-fault.json deleted file mode 100644 index 452325e..0000000 --- a/packages/core/src/__fixtures__/connector-fault.json +++ /dev/null @@ -1,230 +0,0 @@ -{ - "traceId": "fixture-connector-fault", - "metadata": { - "stationId": "CS-SYNTHETIC-003", - "ocppVersion": "1.6", - "source": "synthetic-fixture", - "description": "Connector fault during active session: station boots, transaction starts, connector faults mid-charging, transaction stops with fault reason. Expects CONNECTOR_FAULT failure." - }, - "events": [ - { - "timestamp": "2024-01-15T12:00:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-001", - "BootNotification", - { - "chargePointVendor": "SyntheticVendor", - "chargePointModel": "SM-100", - "chargePointSerialNumber": "CS-SYNTHETIC-003", - "firmwareVersion": "1.0.0" - } - ] - }, - { - "timestamp": "2024-01-15T12:00:00.500Z", - "direction": "CSMS_TO_CS", - "message": [ - 3, - "msg-001", - { - "currentTime": "2024-01-15T12:00:00.500Z", - "interval": 300, - "status": "Accepted" - } - ] - }, - { - "timestamp": "2024-01-15T12:01:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-002", - "StatusNotification", - { - "connectorId": 0, - "status": "Available", - "errorCode": "NoError" - } - ] - }, - { - "timestamp": "2024-01-15T12:01:00.500Z", - "direction": "CSMS_TO_CS", - "message": [3, "msg-002", {}] - }, - { - "timestamp": "2024-01-15T12:02:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-003", - "StatusNotification", - { - "connectorId": 1, - "status": "Preparing", - "errorCode": "NoError" - } - ] - }, - { - "timestamp": "2024-01-15T12:02:00.500Z", - "direction": "CSMS_TO_CS", - "message": [3, "msg-003", {}] - }, - { - "timestamp": "2024-01-15T12:02:15.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-004", - "Authorize", - { - "idTag": "SYNTHETIC-TAG-002" - } - ] - }, - { - "timestamp": "2024-01-15T12:02:15.500Z", - "direction": "CSMS_TO_CS", - "message": [ - 3, - "msg-004", - { - "idTagInfo": { - "status": "Accepted", - "expiryDate": "2024-12-31T23:59:59.000Z" - } - } - ] - }, - { - "timestamp": "2024-01-15T12:02:30.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-005", - "StartTransaction", - { - "connectorId": 1, - "idTag": "SYNTHETIC-TAG-002", - "meterStart": 0, - "timestamp": "2024-01-15T12:02:30.000Z" - } - ] - }, - { - "timestamp": "2024-01-15T12:02:30.500Z", - "direction": "CSMS_TO_CS", - "message": [ - 3, - "msg-005", - { - "transactionId": 100002, - "idTagInfo": { - "status": "Accepted" - } - } - ] - }, - { - "timestamp": "2024-01-15T12:02:31.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-006", - "StatusNotification", - { - "connectorId": 1, - "status": "Charging", - "errorCode": "NoError" - } - ] - }, - { - "timestamp": "2024-01-15T12:02:31.500Z", - "direction": "CSMS_TO_CS", - "message": [3, "msg-006", {}] - }, - { - "timestamp": "2024-01-15T12:15:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-007", - "MeterValues", - { - "connectorId": 1, - "transactionId": 100002, - "meterValue": [ - { - "timestamp": "2024-01-15T12:15:00.000Z", - "sampledValue": [ - { - "value": "3500", - "measurand": "Energy.Active.Import.Register", - "unit": "Wh" - } - ] - } - ] - } - ] - }, - { - "timestamp": "2024-01-15T12:15:00.500Z", - "direction": "CSMS_TO_CS", - "message": [3, "msg-007", {}] - }, - { - "timestamp": "2024-01-15T12:18:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-008", - "StatusNotification", - { - "connectorId": 1, - "status": "Faulted", - "errorCode": "ConnectorLockFailure", - "info": "Connector lock mechanism failure detected" - } - ] - }, - { - "timestamp": "2024-01-15T12:18:00.500Z", - "direction": "CSMS_TO_CS", - "message": [3, "msg-008", {}] - }, - { - "timestamp": "2024-01-15T12:18:05.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-009", - "StopTransaction", - { - "transactionId": 100002, - "idTag": "SYNTHETIC-TAG-002", - "meterStop": 3500, - "timestamp": "2024-01-15T12:18:05.000Z", - "reason": "Faulted" - } - ] - }, - { - "timestamp": "2024-01-15T12:18:05.500Z", - "direction": "CSMS_TO_CS", - "message": [ - 3, - "msg-009", - { - "idTagInfo": { - "status": "Accepted" - } - } - ] - } - ] -} diff --git a/packages/core/src/__fixtures__/connector-fault.ts b/packages/core/src/__fixtures__/connector-fault.ts new file mode 100644 index 0000000..d1ede1a --- /dev/null +++ b/packages/core/src/__fixtures__/connector-fault.ts @@ -0,0 +1,231 @@ +export default { + traceId: 'fixture-connector-fault', + metadata: { + stationId: 'CS-SYNTHETIC-003', + ocppVersion: '1.6', + source: 'synthetic-fixture', + description: + 'Connector fault during active session: station boots, transaction starts, connector faults mid-charging, transaction stops with fault reason. Expects CONNECTOR_FAULT failure.', + }, + events: [ + { + timestamp: '2024-01-15T12:00:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-001', + 'BootNotification', + { + chargePointVendor: 'SyntheticVendor', + chargePointModel: 'SM-100', + chargePointSerialNumber: 'CS-SYNTHETIC-003', + firmwareVersion: '1.0.0', + }, + ], + }, + { + timestamp: '2024-01-15T12:00:00.500Z', + direction: 'CSMS_TO_CS', + message: [ + 3, + 'msg-001', + { + currentTime: '2024-01-15T12:00:00.500Z', + interval: 300, + status: 'Accepted', + }, + ], + }, + { + timestamp: '2024-01-15T12:01:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-002', + 'StatusNotification', + { + connectorId: 0, + status: 'Available', + errorCode: 'NoError', + }, + ], + }, + { + timestamp: '2024-01-15T12:01:00.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-002', {}], + }, + { + timestamp: '2024-01-15T12:02:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-003', + 'StatusNotification', + { + connectorId: 1, + status: 'Preparing', + errorCode: 'NoError', + }, + ], + }, + { + timestamp: '2024-01-15T12:02:00.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-003', {}], + }, + { + timestamp: '2024-01-15T12:02:15.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-004', + 'Authorize', + { + idTag: 'SYNTHETIC-TAG-002', + }, + ], + }, + { + timestamp: '2024-01-15T12:02:15.500Z', + direction: 'CSMS_TO_CS', + message: [ + 3, + 'msg-004', + { + idTagInfo: { + status: 'Accepted', + expiryDate: '2024-12-31T23:59:59.000Z', + }, + }, + ], + }, + { + timestamp: '2024-01-15T12:02:30.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-005', + 'StartTransaction', + { + connectorId: 1, + idTag: 'SYNTHETIC-TAG-002', + meterStart: 0, + timestamp: '2024-01-15T12:02:30.000Z', + }, + ], + }, + { + timestamp: '2024-01-15T12:02:30.500Z', + direction: 'CSMS_TO_CS', + message: [ + 3, + 'msg-005', + { + transactionId: 100002, + idTagInfo: { + status: 'Accepted', + }, + }, + ], + }, + { + timestamp: '2024-01-15T12:02:31.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-006', + 'StatusNotification', + { + connectorId: 1, + status: 'Charging', + errorCode: 'NoError', + }, + ], + }, + { + timestamp: '2024-01-15T12:02:31.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-006', {}], + }, + { + timestamp: '2024-01-15T12:15:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-007', + 'MeterValues', + { + connectorId: 1, + transactionId: 100002, + meterValue: [ + { + timestamp: '2024-01-15T12:15:00.000Z', + sampledValue: [ + { + value: '3500', + measurand: 'Energy.Active.Import.Register', + unit: 'Wh', + }, + ], + }, + ], + }, + ], + }, + { + timestamp: '2024-01-15T12:15:00.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-007', {}], + }, + { + timestamp: '2024-01-15T12:18:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-008', + 'StatusNotification', + { + connectorId: 1, + status: 'Faulted', + errorCode: 'ConnectorLockFailure', + info: 'Connector lock mechanism failure detected', + }, + ], + }, + { + timestamp: '2024-01-15T12:18:00.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-008', {}], + }, + { + timestamp: '2024-01-15T12:18:05.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-009', + 'StopTransaction', + { + transactionId: 100002, + idTag: 'SYNTHETIC-TAG-002', + meterStop: 3500, + timestamp: '2024-01-15T12:18:05.000Z', + reason: 'Faulted', + }, + ], + }, + { + timestamp: '2024-01-15T12:18:05.500Z', + direction: 'CSMS_TO_CS', + message: [ + 3, + 'msg-009', + { + idTagInfo: { + status: 'Accepted', + }, + }, + ], + }, + ], +}; diff --git a/packages/core/src/__fixtures__/failed-auth.json b/packages/core/src/__fixtures__/failed-auth.json deleted file mode 100644 index 87a6d45..0000000 --- a/packages/core/src/__fixtures__/failed-auth.json +++ /dev/null @@ -1,172 +0,0 @@ -{ - "traceId": "fixture-failed-auth", - "metadata": { - "stationId": "CS-SYNTHETIC-002", - "ocppVersion": "1.6", - "source": "synthetic-fixture", - "description": "Failed authorization: station boots, connector prepares, idTag is rejected by CSMS. StartTransaction is not attempted. Expects FAILED_AUTHORIZATION failure." - }, - "events": [ - { - "timestamp": "2024-01-15T11:00:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-001", - "BootNotification", - { - "chargePointVendor": "SyntheticVendor", - "chargePointModel": "SM-100", - "chargePointSerialNumber": "CS-SYNTHETIC-002", - "firmwareVersion": "1.0.0" - } - ] - }, - { - "timestamp": "2024-01-15T11:00:00.500Z", - "direction": "CSMS_TO_CS", - "message": [ - 3, - "msg-001", - { - "currentTime": "2024-01-15T11:00:00.500Z", - "interval": 300, - "status": "Accepted" - } - ] - }, - { - "timestamp": "2024-01-15T11:01:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-002", - "StatusNotification", - { - "connectorId": 0, - "status": "Available", - "errorCode": "NoError" - } - ] - }, - { - "timestamp": "2024-01-15T11:01:00.500Z", - "direction": "CSMS_TO_CS", - "message": [3, "msg-002", {}] - }, - { - "timestamp": "2024-01-15T11:02:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-003", - "StatusNotification", - { - "connectorId": 1, - "status": "Preparing", - "errorCode": "NoError" - } - ] - }, - { - "timestamp": "2024-01-15T11:02:00.500Z", - "direction": "CSMS_TO_CS", - "message": [3, "msg-003", {}] - }, - { - "timestamp": "2024-01-15T11:02:15.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-004", - "Authorize", - { - "idTag": "SYNTHETIC-TAG-INVALID" - } - ] - }, - { - "timestamp": "2024-01-15T11:02:15.500Z", - "direction": "CSMS_TO_CS", - "message": [ - 3, - "msg-004", - { - "idTagInfo": { - "status": "Invalid" - } - } - ] - }, - { - "timestamp": "2024-01-15T11:02:20.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-005", - "Authorize", - { - "idTag": "SYNTHETIC-TAG-INVALID" - } - ] - }, - { - "timestamp": "2024-01-15T11:02:20.500Z", - "direction": "CSMS_TO_CS", - "message": [ - 3, - "msg-005", - { - "idTagInfo": { - "status": "Invalid" - } - } - ] - }, - { - "timestamp": "2024-01-15T11:02:30.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-006", - "Authorize", - { - "idTag": "SYNTHETIC-TAG-INVALID" - } - ] - }, - { - "timestamp": "2024-01-15T11:02:30.500Z", - "direction": "CSMS_TO_CS", - "message": [ - 3, - "msg-006", - { - "idTagInfo": { - "status": "Invalid" - } - } - ] - }, - { - "timestamp": "2024-01-15T11:02:40.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-007", - "StatusNotification", - { - "connectorId": 1, - "status": "Faulted", - "errorCode": "OtherError", - "info": "Authorization failed after 3 attempts" - } - ] - }, - { - "timestamp": "2024-01-15T11:02:40.500Z", - "direction": "CSMS_TO_CS", - "message": [3, "msg-007", {}] - } - ] -} diff --git a/packages/core/src/__fixtures__/failed-auth.ts b/packages/core/src/__fixtures__/failed-auth.ts new file mode 100644 index 0000000..5c03f8b --- /dev/null +++ b/packages/core/src/__fixtures__/failed-auth.ts @@ -0,0 +1,173 @@ +export default { + traceId: 'fixture-failed-auth', + metadata: { + stationId: 'CS-SYNTHETIC-002', + ocppVersion: '1.6', + source: 'synthetic-fixture', + description: + 'Failed authorization: station boots, connector prepares, idTag is rejected by CSMS. StartTransaction is not attempted. Expects FAILED_AUTHORIZATION failure.', + }, + events: [ + { + timestamp: '2024-01-15T11:00:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-001', + 'BootNotification', + { + chargePointVendor: 'SyntheticVendor', + chargePointModel: 'SM-100', + chargePointSerialNumber: 'CS-SYNTHETIC-002', + firmwareVersion: '1.0.0', + }, + ], + }, + { + timestamp: '2024-01-15T11:00:00.500Z', + direction: 'CSMS_TO_CS', + message: [ + 3, + 'msg-001', + { + currentTime: '2024-01-15T11:00:00.500Z', + interval: 300, + status: 'Accepted', + }, + ], + }, + { + timestamp: '2024-01-15T11:01:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-002', + 'StatusNotification', + { + connectorId: 0, + status: 'Available', + errorCode: 'NoError', + }, + ], + }, + { + timestamp: '2024-01-15T11:01:00.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-002', {}], + }, + { + timestamp: '2024-01-15T11:02:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-003', + 'StatusNotification', + { + connectorId: 1, + status: 'Preparing', + errorCode: 'NoError', + }, + ], + }, + { + timestamp: '2024-01-15T11:02:00.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-003', {}], + }, + { + timestamp: '2024-01-15T11:02:15.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-004', + 'Authorize', + { + idTag: 'SYNTHETIC-TAG-INVALID', + }, + ], + }, + { + timestamp: '2024-01-15T11:02:15.500Z', + direction: 'CSMS_TO_CS', + message: [ + 3, + 'msg-004', + { + idTagInfo: { + status: 'Invalid', + }, + }, + ], + }, + { + timestamp: '2024-01-15T11:02:20.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-005', + 'Authorize', + { + idTag: 'SYNTHETIC-TAG-INVALID', + }, + ], + }, + { + timestamp: '2024-01-15T11:02:20.500Z', + direction: 'CSMS_TO_CS', + message: [ + 3, + 'msg-005', + { + idTagInfo: { + status: 'Invalid', + }, + }, + ], + }, + { + timestamp: '2024-01-15T11:02:30.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-006', + 'Authorize', + { + idTag: 'SYNTHETIC-TAG-INVALID', + }, + ], + }, + { + timestamp: '2024-01-15T11:02:30.500Z', + direction: 'CSMS_TO_CS', + message: [ + 3, + 'msg-006', + { + idTagInfo: { + status: 'Invalid', + }, + }, + ], + }, + { + timestamp: '2024-01-15T11:02:40.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-007', + 'StatusNotification', + { + connectorId: 1, + status: 'Faulted', + errorCode: 'OtherError', + info: 'Authorization failed after 3 attempts', + }, + ], + }, + { + timestamp: '2024-01-15T11:02:40.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-007', {}], + }, + ], +}; diff --git a/packages/core/src/__fixtures__/normal-session.json b/packages/core/src/__fixtures__/normal-session.json deleted file mode 100644 index ab79bd2..0000000 --- a/packages/core/src/__fixtures__/normal-session.json +++ /dev/null @@ -1,260 +0,0 @@ -{ - "traceId": "fixture-normal-session", - "metadata": { - "stationId": "CS-SYNTHETIC-001", - "ocppVersion": "1.6", - "source": "synthetic-fixture", - "description": "Normal charging session: boot → authorize → start transaction → meter values → stop transaction. No failures expected." - }, - "events": [ - { - "timestamp": "2024-01-15T10:00:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-001", - "BootNotification", - { - "chargePointVendor": "SyntheticVendor", - "chargePointModel": "SM-100", - "chargePointSerialNumber": "CS-SYNTHETIC-001", - "firmwareVersion": "1.0.0" - } - ] - }, - { - "timestamp": "2024-01-15T10:00:00.500Z", - "direction": "CSMS_TO_CS", - "message": [ - 3, - "msg-001", - { - "currentTime": "2024-01-15T10:00:00.500Z", - "interval": 300, - "status": "Accepted" - } - ] - }, - { - "timestamp": "2024-01-15T10:01:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-002", - "StatusNotification", - { - "connectorId": 0, - "status": "Available", - "errorCode": "NoError" - } - ] - }, - { - "timestamp": "2024-01-15T10:01:00.500Z", - "direction": "CSMS_TO_CS", - "message": [3, "msg-002", {}] - }, - { - "timestamp": "2024-01-15T10:02:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-003", - "StatusNotification", - { - "connectorId": 1, - "status": "Preparing", - "errorCode": "NoError" - } - ] - }, - { - "timestamp": "2024-01-15T10:02:00.500Z", - "direction": "CSMS_TO_CS", - "message": [3, "msg-003", {}] - }, - { - "timestamp": "2024-01-15T10:02:15.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-004", - "Authorize", - { - "idTag": "SYNTHETIC-TAG-001" - } - ] - }, - { - "timestamp": "2024-01-15T10:02:15.500Z", - "direction": "CSMS_TO_CS", - "message": [ - 3, - "msg-004", - { - "idTagInfo": { - "status": "Accepted", - "expiryDate": "2024-12-31T23:59:59.000Z", - "parentIdTag": "SYNTHETIC-PARENT-001" - } - } - ] - }, - { - "timestamp": "2024-01-15T10:02:30.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-005", - "StartTransaction", - { - "connectorId": 1, - "idTag": "SYNTHETIC-TAG-001", - "meterStart": 0, - "timestamp": "2024-01-15T10:02:30.000Z" - } - ] - }, - { - "timestamp": "2024-01-15T10:02:30.500Z", - "direction": "CSMS_TO_CS", - "message": [ - 3, - "msg-005", - { - "transactionId": 100001, - "idTagInfo": { - "status": "Accepted" - } - } - ] - }, - { - "timestamp": "2024-01-15T10:02:31.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-006", - "StatusNotification", - { - "connectorId": 1, - "status": "Charging", - "errorCode": "NoError" - } - ] - }, - { - "timestamp": "2024-01-15T10:02:31.500Z", - "direction": "CSMS_TO_CS", - "message": [3, "msg-006", {}] - }, - { - "timestamp": "2024-01-15T10:15:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-007", - "MeterValues", - { - "connectorId": 1, - "transactionId": 100001, - "meterValue": [ - { - "timestamp": "2024-01-15T10:15:00.000Z", - "sampledValue": [ - { - "value": "5000", - "measurand": "Energy.Active.Import.Register", - "unit": "Wh" - } - ] - } - ] - } - ] - }, - { - "timestamp": "2024-01-15T10:15:00.500Z", - "direction": "CSMS_TO_CS", - "message": [3, "msg-007", {}] - }, - { - "timestamp": "2024-01-15T10:30:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-008", - "MeterValues", - { - "connectorId": 1, - "transactionId": 100001, - "meterValue": [ - { - "timestamp": "2024-01-15T10:30:00.000Z", - "sampledValue": [ - { - "value": "10000", - "measurand": "Energy.Active.Import.Register", - "unit": "Wh" - } - ] - } - ] - } - ] - }, - { - "timestamp": "2024-01-15T10:30:00.500Z", - "direction": "CSMS_TO_CS", - "message": [3, "msg-008", {}] - }, - { - "timestamp": "2024-01-15T10:35:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-009", - "StopTransaction", - { - "transactionId": 100001, - "idTag": "SYNTHETIC-TAG-001", - "meterStop": 10000, - "timestamp": "2024-01-15T10:35:00.000Z", - "reason": "EVDisconnected" - } - ] - }, - { - "timestamp": "2024-01-15T10:35:00.500Z", - "direction": "CSMS_TO_CS", - "message": [ - 3, - "msg-009", - { - "idTagInfo": { - "status": "Accepted" - } - } - ] - }, - { - "timestamp": "2024-01-15T10:35:01.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-010", - "StatusNotification", - { - "connectorId": 1, - "status": "Available", - "errorCode": "NoError" - } - ] - }, - { - "timestamp": "2024-01-15T10:35:01.500Z", - "direction": "CSMS_TO_CS", - "message": [3, "msg-010", {}] - } - ] -} diff --git a/packages/core/src/__fixtures__/normal-session.ts b/packages/core/src/__fixtures__/normal-session.ts new file mode 100644 index 0000000..53db47a --- /dev/null +++ b/packages/core/src/__fixtures__/normal-session.ts @@ -0,0 +1,261 @@ +export default { + traceId: 'fixture-normal-session', + metadata: { + stationId: 'CS-SYNTHETIC-001', + ocppVersion: '1.6', + source: 'synthetic-fixture', + description: + 'Normal charging session: boot → authorize → start transaction → meter values → stop transaction. No failures expected.', + }, + events: [ + { + timestamp: '2024-01-15T10:00:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-001', + 'BootNotification', + { + chargePointVendor: 'SyntheticVendor', + chargePointModel: 'SM-100', + chargePointSerialNumber: 'CS-SYNTHETIC-001', + firmwareVersion: '1.0.0', + }, + ], + }, + { + timestamp: '2024-01-15T10:00:00.500Z', + direction: 'CSMS_TO_CS', + message: [ + 3, + 'msg-001', + { + currentTime: '2024-01-15T10:00:00.500Z', + interval: 300, + status: 'Accepted', + }, + ], + }, + { + timestamp: '2024-01-15T10:01:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-002', + 'StatusNotification', + { + connectorId: 0, + status: 'Available', + errorCode: 'NoError', + }, + ], + }, + { + timestamp: '2024-01-15T10:01:00.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-002', {}], + }, + { + timestamp: '2024-01-15T10:02:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-003', + 'StatusNotification', + { + connectorId: 1, + status: 'Preparing', + errorCode: 'NoError', + }, + ], + }, + { + timestamp: '2024-01-15T10:02:00.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-003', {}], + }, + { + timestamp: '2024-01-15T10:02:15.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-004', + 'Authorize', + { + idTag: 'SYNTHETIC-TAG-001', + }, + ], + }, + { + timestamp: '2024-01-15T10:02:15.500Z', + direction: 'CSMS_TO_CS', + message: [ + 3, + 'msg-004', + { + idTagInfo: { + status: 'Accepted', + expiryDate: '2024-12-31T23:59:59.000Z', + parentIdTag: 'SYNTHETIC-PARENT-001', + }, + }, + ], + }, + { + timestamp: '2024-01-15T10:02:30.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-005', + 'StartTransaction', + { + connectorId: 1, + idTag: 'SYNTHETIC-TAG-001', + meterStart: 0, + timestamp: '2024-01-15T10:02:30.000Z', + }, + ], + }, + { + timestamp: '2024-01-15T10:02:30.500Z', + direction: 'CSMS_TO_CS', + message: [ + 3, + 'msg-005', + { + transactionId: 100001, + idTagInfo: { + status: 'Accepted', + }, + }, + ], + }, + { + timestamp: '2024-01-15T10:02:31.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-006', + 'StatusNotification', + { + connectorId: 1, + status: 'Charging', + errorCode: 'NoError', + }, + ], + }, + { + timestamp: '2024-01-15T10:02:31.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-006', {}], + }, + { + timestamp: '2024-01-15T10:15:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-007', + 'MeterValues', + { + connectorId: 1, + transactionId: 100001, + meterValue: [ + { + timestamp: '2024-01-15T10:15:00.000Z', + sampledValue: [ + { + value: '5000', + measurand: 'Energy.Active.Import.Register', + unit: 'Wh', + }, + ], + }, + ], + }, + ], + }, + { + timestamp: '2024-01-15T10:15:00.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-007', {}], + }, + { + timestamp: '2024-01-15T10:30:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-008', + 'MeterValues', + { + connectorId: 1, + transactionId: 100001, + meterValue: [ + { + timestamp: '2024-01-15T10:30:00.000Z', + sampledValue: [ + { + value: '10000', + measurand: 'Energy.Active.Import.Register', + unit: 'Wh', + }, + ], + }, + ], + }, + ], + }, + { + timestamp: '2024-01-15T10:30:00.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-008', {}], + }, + { + timestamp: '2024-01-15T10:35:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-009', + 'StopTransaction', + { + transactionId: 100001, + idTag: 'SYNTHETIC-TAG-001', + meterStop: 10000, + timestamp: '2024-01-15T10:35:00.000Z', + reason: 'EVDisconnected', + }, + ], + }, + { + timestamp: '2024-01-15T10:35:00.500Z', + direction: 'CSMS_TO_CS', + message: [ + 3, + 'msg-009', + { + idTagInfo: { + status: 'Accepted', + }, + }, + ], + }, + { + timestamp: '2024-01-15T10:35:01.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-010', + 'StatusNotification', + { + connectorId: 1, + status: 'Available', + errorCode: 'NoError', + }, + ], + }, + { + timestamp: '2024-01-15T10:35:01.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-010', {}], + }, + ], +}; diff --git a/packages/core/src/fixtures/index.ts b/packages/core/src/fixtures/index.ts index 3819975..61f4aa1 100644 --- a/packages/core/src/fixtures/index.ts +++ b/packages/core/src/fixtures/index.ts @@ -7,9 +7,9 @@ * @see docs/trace-format-spec.md */ -import normalSession from '../__fixtures__/normal-session.json'; -import failedAuth from '../__fixtures__/failed-auth.json'; -import connectorFault from '../__fixtures__/connector-fault.json'; +import normalSession from '../__fixtures__/normal-session.js'; +import failedAuth from '../__fixtures__/failed-auth.js'; +import connectorFault from '../__fixtures__/connector-fault.js'; export { normalSession, failedAuth, connectorFault }; diff --git a/packages/scenarios/src/__scenarios__/station-offline.json b/packages/scenarios/src/__scenarios__/station-offline.json deleted file mode 100644 index c56af41..0000000 --- a/packages/scenarios/src/__scenarios__/station-offline.json +++ /dev/null @@ -1,167 +0,0 @@ -{ - "name": "station-offline", - "description": "Station goes offline during active session: transaction starts but no StopTransaction follows. Expects STATION_OFFLINE_DURING_SESSION failure.", - "trace": { - "traceId": "scenario-station-offline", - "metadata": { - "stationId": "CS-SYNTHETIC-004", - "ocppVersion": "1.6", - "source": "synthetic-scenario", - "description": "Station offline during active session — StartTransaction with no StopTransaction." - }, - "events": [ - { - "timestamp": "2024-01-15T14:00:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-001", - "BootNotification", - { - "chargePointVendor": "SyntheticVendor", - "chargePointModel": "SM-100", - "chargePointSerialNumber": "CS-SYNTHETIC-004", - "firmwareVersion": "1.0.0" - } - ] - }, - { - "timestamp": "2024-01-15T14:00:00.500Z", - "direction": "CSMS_TO_CS", - "message": [ - 3, - "msg-001", - { - "currentTime": "2024-01-15T14:00:00.500Z", - "interval": 300, - "status": "Accepted" - } - ] - }, - { - "timestamp": "2024-01-15T14:01:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-002", - "StatusNotification", - { - "connectorId": 0, - "status": "Available", - "errorCode": "NoError" - } - ] - }, - { - "timestamp": "2024-01-15T14:01:00.500Z", - "direction": "CSMS_TO_CS", - "message": [3, "msg-002", {}] - }, - { - "timestamp": "2024-01-15T14:02:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-003", - "Authorize", - { - "idTag": "SYNTHETIC-TAG-004" - } - ] - }, - { - "timestamp": "2024-01-15T14:02:00.500Z", - "direction": "CSMS_TO_CS", - "message": [ - 3, - "msg-003", - { - "idTagInfo": { - "status": "Accepted", - "expiryDate": "2024-12-31T23:59:59.000Z" - } - } - ] - }, - { - "timestamp": "2024-01-15T14:02:30.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-004", - "StartTransaction", - { - "connectorId": 1, - "idTag": "SYNTHETIC-TAG-004", - "meterStart": 0, - "timestamp": "2024-01-15T14:02:30.000Z" - } - ] - }, - { - "timestamp": "2024-01-15T14:02:30.500Z", - "direction": "CSMS_TO_CS", - "message": [ - 3, - "msg-004", - { - "transactionId": 100004, - "idTagInfo": { - "status": "Accepted" - } - } - ] - }, - { - "timestamp": "2024-01-15T14:02:31.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-005", - "StatusNotification", - { - "connectorId": 1, - "status": "Charging", - "errorCode": "NoError" - } - ] - }, - { - "timestamp": "2024-01-15T14:02:31.500Z", - "direction": "CSMS_TO_CS", - "message": [3, "msg-005", {}] - }, - { - "timestamp": "2024-01-15T14:15:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-006", - "MeterValues", - { - "connectorId": 1, - "transactionId": 100004, - "meterValue": [ - { - "timestamp": "2024-01-15T14:15:00.000Z", - "sampledValue": [ - { - "value": "2000", - "measurand": "Energy.Active.Import.Register", - "unit": "Wh" - } - ] - } - ] - } - ] - }, - { - "timestamp": "2024-01-15T14:15:00.500Z", - "direction": "CSMS_TO_CS", - "message": [3, "msg-006", {}] - } - ] - }, - "expectedFailures": ["STATION_OFFLINE_DURING_SESSION"] -} diff --git a/packages/scenarios/src/__scenarios__/station-offline.ts b/packages/scenarios/src/__scenarios__/station-offline.ts new file mode 100644 index 0000000..4f3c622 --- /dev/null +++ b/packages/scenarios/src/__scenarios__/station-offline.ts @@ -0,0 +1,169 @@ +export default { + name: 'station-offline', + description: + 'Station goes offline during active session: transaction starts but no StopTransaction follows. Expects STATION_OFFLINE_DURING_SESSION failure.', + trace: { + traceId: 'scenario-station-offline', + metadata: { + stationId: 'CS-SYNTHETIC-004', + ocppVersion: '1.6', + source: 'synthetic-scenario', + description: + 'Station offline during active session — StartTransaction with no StopTransaction.', + }, + events: [ + { + timestamp: '2024-01-15T14:00:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-001', + 'BootNotification', + { + chargePointVendor: 'SyntheticVendor', + chargePointModel: 'SM-100', + chargePointSerialNumber: 'CS-SYNTHETIC-004', + firmwareVersion: '1.0.0', + }, + ], + }, + { + timestamp: '2024-01-15T14:00:00.500Z', + direction: 'CSMS_TO_CS', + message: [ + 3, + 'msg-001', + { + currentTime: '2024-01-15T14:00:00.500Z', + interval: 300, + status: 'Accepted', + }, + ], + }, + { + timestamp: '2024-01-15T14:01:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-002', + 'StatusNotification', + { + connectorId: 0, + status: 'Available', + errorCode: 'NoError', + }, + ], + }, + { + timestamp: '2024-01-15T14:01:00.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-002', {}], + }, + { + timestamp: '2024-01-15T14:02:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-003', + 'Authorize', + { + idTag: 'SYNTHETIC-TAG-004', + }, + ], + }, + { + timestamp: '2024-01-15T14:02:00.500Z', + direction: 'CSMS_TO_CS', + message: [ + 3, + 'msg-003', + { + idTagInfo: { + status: 'Accepted', + expiryDate: '2024-12-31T23:59:59.000Z', + }, + }, + ], + }, + { + timestamp: '2024-01-15T14:02:30.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-004', + 'StartTransaction', + { + connectorId: 1, + idTag: 'SYNTHETIC-TAG-004', + meterStart: 0, + timestamp: '2024-01-15T14:02:30.000Z', + }, + ], + }, + { + timestamp: '2024-01-15T14:02:30.500Z', + direction: 'CSMS_TO_CS', + message: [ + 3, + 'msg-004', + { + transactionId: 100004, + idTagInfo: { + status: 'Accepted', + }, + }, + ], + }, + { + timestamp: '2024-01-15T14:02:31.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-005', + 'StatusNotification', + { + connectorId: 1, + status: 'Charging', + errorCode: 'NoError', + }, + ], + }, + { + timestamp: '2024-01-15T14:02:31.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-005', {}], + }, + { + timestamp: '2024-01-15T14:15:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-006', + 'MeterValues', + { + connectorId: 1, + transactionId: 100004, + meterValue: [ + { + timestamp: '2024-01-15T14:15:00.000Z', + sampledValue: [ + { + value: '2000', + measurand: 'Energy.Active.Import.Register', + unit: 'Wh', + }, + ], + }, + ], + }, + ], + }, + { + timestamp: '2024-01-15T14:15:00.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-006', {}], + }, + ], + }, + expectedFailures: ['STATION_OFFLINE_DURING_SESSION'], +}; diff --git a/packages/scenarios/src/__scenarios__/unexpected-stop-reason.json b/packages/scenarios/src/__scenarios__/unexpected-stop-reason.json deleted file mode 100644 index e8f5cd1..0000000 --- a/packages/scenarios/src/__scenarios__/unexpected-stop-reason.json +++ /dev/null @@ -1,215 +0,0 @@ -{ - "name": "unexpected-stop-reason", - "description": "Stop transaction with unexpected stop reason (EVDisconnected replaced by Other). Parser and timeline-only fixture — no v0.1 detection rule matches this pattern. Validates that the parser and timeline builder handle stop events with error payloads correctly.", - "trace": { - "traceId": "scenario-unexpected-stop-reason", - "metadata": { - "stationId": "CS-SYNTHETIC-005", - "ocppVersion": "1.6", - "source": "synthetic-scenario", - "description": "Stop transaction with unexpected stop reason — parser/timeline validation only." - }, - "events": [ - { - "timestamp": "2024-01-15T16:00:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-001", - "BootNotification", - { - "chargePointVendor": "SyntheticVendor", - "chargePointModel": "SM-100", - "chargePointSerialNumber": "CS-SYNTHETIC-005", - "firmwareVersion": "1.0.0" - } - ] - }, - { - "timestamp": "2024-01-15T16:00:00.500Z", - "direction": "CSMS_TO_CS", - "message": [ - 3, - "msg-001", - { - "currentTime": "2024-01-15T16:00:00.500Z", - "interval": 300, - "status": "Accepted" - } - ] - }, - { - "timestamp": "2024-01-15T16:01:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-002", - "StatusNotification", - { - "connectorId": 0, - "status": "Available", - "errorCode": "NoError" - } - ] - }, - { - "timestamp": "2024-01-15T16:01:00.500Z", - "direction": "CSMS_TO_CS", - "message": [3, "msg-002", {}] - }, - { - "timestamp": "2024-01-15T16:02:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-003", - "Authorize", - { - "idTag": "SYNTHETIC-TAG-005" - } - ] - }, - { - "timestamp": "2024-01-15T16:02:00.500Z", - "direction": "CSMS_TO_CS", - "message": [ - 3, - "msg-003", - { - "idTagInfo": { - "status": "Accepted", - "expiryDate": "2024-12-31T23:59:59.000Z" - } - } - ] - }, - { - "timestamp": "2024-01-15T16:02:30.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-004", - "StartTransaction", - { - "connectorId": 1, - "idTag": "SYNTHETIC-TAG-005", - "meterStart": 0, - "timestamp": "2024-01-15T16:02:30.000Z" - } - ] - }, - { - "timestamp": "2024-01-15T16:02:30.500Z", - "direction": "CSMS_TO_CS", - "message": [ - 3, - "msg-004", - { - "transactionId": 100005, - "idTagInfo": { - "status": "Accepted" - } - } - ] - }, - { - "timestamp": "2024-01-15T16:02:31.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-005", - "StatusNotification", - { - "connectorId": 1, - "status": "Charging", - "errorCode": "NoError" - } - ] - }, - { - "timestamp": "2024-01-15T16:02:31.500Z", - "direction": "CSMS_TO_CS", - "message": [3, "msg-005", {}] - }, - { - "timestamp": "2024-01-15T16:20:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-006", - "MeterValues", - { - "connectorId": 1, - "transactionId": 100005, - "meterValue": [ - { - "timestamp": "2024-01-15T16:20:00.000Z", - "sampledValue": [ - { - "value": "4000", - "measurand": "Energy.Active.Import.Register", - "unit": "Wh" - } - ] - } - ] - } - ] - }, - { - "timestamp": "2024-01-15T16:20:00.500Z", - "direction": "CSMS_TO_CS", - "message": [3, "msg-006", {}] - }, - { - "timestamp": "2024-01-15T16:25:00.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-007", - "StopTransaction", - { - "transactionId": 100005, - "idTag": "SYNTHETIC-TAG-005", - "meterStop": 4000, - "timestamp": "2024-01-15T16:25:00.000Z", - "reason": "Other" - } - ] - }, - { - "timestamp": "2024-01-15T16:25:00.500Z", - "direction": "CSMS_TO_CS", - "message": [ - 3, - "msg-007", - { - "idTagInfo": { - "status": "Accepted" - } - } - ] - }, - { - "timestamp": "2024-01-15T16:25:01.000Z", - "direction": "CS_TO_CSMS", - "message": [ - 2, - "msg-008", - "StatusNotification", - { - "connectorId": 1, - "status": "Available", - "errorCode": "NoError" - } - ] - }, - { - "timestamp": "2024-01-15T16:25:01.500Z", - "direction": "CSMS_TO_CS", - "message": [3, "msg-008", {}] - } - ] - }, - "expectedFailures": [] -} diff --git a/packages/scenarios/src/__scenarios__/unexpected-stop-reason.ts b/packages/scenarios/src/__scenarios__/unexpected-stop-reason.ts new file mode 100644 index 0000000..eec8e33 --- /dev/null +++ b/packages/scenarios/src/__scenarios__/unexpected-stop-reason.ts @@ -0,0 +1,217 @@ +export default { + name: 'unexpected-stop-reason', + description: + 'Stop transaction with unexpected stop reason (EVDisconnected replaced by Other). Parser and timeline-only fixture — no v0.1 detection rule matches this pattern. Validates that the parser and timeline builder handle stop events with error payloads correctly.', + trace: { + traceId: 'scenario-unexpected-stop-reason', + metadata: { + stationId: 'CS-SYNTHETIC-005', + ocppVersion: '1.6', + source: 'synthetic-scenario', + description: + 'Stop transaction with unexpected stop reason — parser/timeline validation only.', + }, + events: [ + { + timestamp: '2024-01-15T16:00:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-001', + 'BootNotification', + { + chargePointVendor: 'SyntheticVendor', + chargePointModel: 'SM-100', + chargePointSerialNumber: 'CS-SYNTHETIC-005', + firmwareVersion: '1.0.0', + }, + ], + }, + { + timestamp: '2024-01-15T16:00:00.500Z', + direction: 'CSMS_TO_CS', + message: [ + 3, + 'msg-001', + { + currentTime: '2024-01-15T16:00:00.500Z', + interval: 300, + status: 'Accepted', + }, + ], + }, + { + timestamp: '2024-01-15T16:01:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-002', + 'StatusNotification', + { + connectorId: 0, + status: 'Available', + errorCode: 'NoError', + }, + ], + }, + { + timestamp: '2024-01-15T16:01:00.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-002', {}], + }, + { + timestamp: '2024-01-15T16:02:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-003', + 'Authorize', + { + idTag: 'SYNTHETIC-TAG-005', + }, + ], + }, + { + timestamp: '2024-01-15T16:02:00.500Z', + direction: 'CSMS_TO_CS', + message: [ + 3, + 'msg-003', + { + idTagInfo: { + status: 'Accepted', + expiryDate: '2024-12-31T23:59:59.000Z', + }, + }, + ], + }, + { + timestamp: '2024-01-15T16:02:30.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-004', + 'StartTransaction', + { + connectorId: 1, + idTag: 'SYNTHETIC-TAG-005', + meterStart: 0, + timestamp: '2024-01-15T16:02:30.000Z', + }, + ], + }, + { + timestamp: '2024-01-15T16:02:30.500Z', + direction: 'CSMS_TO_CS', + message: [ + 3, + 'msg-004', + { + transactionId: 100005, + idTagInfo: { + status: 'Accepted', + }, + }, + ], + }, + { + timestamp: '2024-01-15T16:02:31.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-005', + 'StatusNotification', + { + connectorId: 1, + status: 'Charging', + errorCode: 'NoError', + }, + ], + }, + { + timestamp: '2024-01-15T16:02:31.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-005', {}], + }, + { + timestamp: '2024-01-15T16:20:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-006', + 'MeterValues', + { + connectorId: 1, + transactionId: 100005, + meterValue: [ + { + timestamp: '2024-01-15T16:20:00.000Z', + sampledValue: [ + { + value: '4000', + measurand: 'Energy.Active.Import.Register', + unit: 'Wh', + }, + ], + }, + ], + }, + ], + }, + { + timestamp: '2024-01-15T16:20:00.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-006', {}], + }, + { + timestamp: '2024-01-15T16:25:00.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-007', + 'StopTransaction', + { + transactionId: 100005, + idTag: 'SYNTHETIC-TAG-005', + meterStop: 4000, + timestamp: '2024-01-15T16:25:00.000Z', + reason: 'Other', + }, + ], + }, + { + timestamp: '2024-01-15T16:25:00.500Z', + direction: 'CSMS_TO_CS', + message: [ + 3, + 'msg-007', + { + idTagInfo: { + status: 'Accepted', + }, + }, + ], + }, + { + timestamp: '2024-01-15T16:25:01.000Z', + direction: 'CS_TO_CSMS', + message: [ + 2, + 'msg-008', + 'StatusNotification', + { + connectorId: 1, + status: 'Available', + errorCode: 'NoError', + }, + ], + }, + { + timestamp: '2024-01-15T16:25:01.500Z', + direction: 'CSMS_TO_CS', + message: [3, 'msg-008', {}], + }, + ], + }, + expectedFailures: [], +}; diff --git a/packages/scenarios/src/index.ts b/packages/scenarios/src/index.ts index 950de26..fa3ba21 100644 --- a/packages/scenarios/src/index.ts +++ b/packages/scenarios/src/index.ts @@ -8,8 +8,8 @@ import type { Scenario, Trace } from '@ocpp-debugkit/core'; import { fixtures } from '@ocpp-debugkit/core'; -import stationOffline from './__scenarios__/station-offline.json'; -import unexpectedStopReason from './__scenarios__/unexpected-stop-reason.json'; +import stationOffline from './__scenarios__/station-offline.js'; +import unexpectedStopReason from './__scenarios__/unexpected-stop-reason.js'; // --------------------------------------------------------------------------- // Scenarios derived from core fixtures diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1170c7a..f9d5e32 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,6 +36,31 @@ importers: specifier: ^3.0.0 version: 3.2.7(@types/node@22.20.0) + packages/cli: + dependencies: + '@ocpp-debugkit/core': + specifier: workspace:* + version: link:../core + '@ocpp-debugkit/reporter': + specifier: workspace:* + version: link:../reporter + '@ocpp-debugkit/scenarios': + specifier: workspace:* + version: link:../scenarios + commander: + specifier: ^13.0.0 + version: 13.1.0 + devDependencies: + execa: + specifier: ^9.0.0 + version: 9.6.1 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vitest: + specifier: ^3.0.0 + version: 3.2.7(@types/node@22.20.0) + packages/core: dependencies: zod: @@ -505,6 +530,13 @@ packages: cpu: [x64] os: [win32] + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + '@turbo/darwin-64@2.10.4': resolution: {integrity: sha512-m1MUEI4MJ69r5CwfMYxmHi0H0rrgiYCBOp0tgBZ9x/YVvOb5uu/lRIDyDwdtH054R2yWeQaIigUGu6aCX9f8cA==} cpu: [x64] @@ -732,6 +764,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -833,6 +869,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + expect-type@1.4.0: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} @@ -865,6 +905,10 @@ packages: picomatch: optional: true + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -901,6 +945,10 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -928,6 +976,10 @@ packages: resolution: {integrity: sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA==} hasBin: true + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + iconv-lite@0.7.3: resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} engines: {node: '>=0.10.0'} @@ -960,10 +1012,22 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + is-subdir@1.2.0: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} engines: {node: '>=4'} + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + is-windows@1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} @@ -1051,6 +1115,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1093,6 +1161,10 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1101,6 +1173,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -1145,6 +1221,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1230,6 +1310,10 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -1300,6 +1384,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -1398,6 +1486,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} @@ -1800,6 +1892,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + '@turbo/darwin-64@2.10.4': optional: true @@ -2047,6 +2143,8 @@ snapshots: color-name@1.1.4: {} + commander@13.1.0: {} + concat-map@0.0.1: {} cross-spawn@7.0.6: @@ -2181,6 +2279,21 @@ snapshots: esutils@2.0.3: {} + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + expect-type@1.4.0: {} extendable-error@0.1.7: {} @@ -2207,6 +2320,10 @@ snapshots: optionalDependencies: picomatch: 4.0.5 + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -2247,6 +2364,11 @@ snapshots: fsevents@2.3.3: optional: true + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -2272,6 +2394,8 @@ snapshots: human-id@4.2.0: {} + human-signals@8.0.1: {} + iconv-lite@0.7.3: dependencies: safer-buffer: 2.1.2 @@ -2295,10 +2419,16 @@ snapshots: is-number@7.0.0: {} + is-plain-obj@4.1.0: {} + + is-stream@4.0.1: {} + is-subdir@1.2.0: dependencies: better-path-resolve: 1.0.0 + is-unicode-supported@2.1.0: {} + is-windows@1.0.2: {} isexe@2.0.0: {} @@ -2374,6 +2504,11 @@ snapshots: natural-compare@1.4.0: {} + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -2417,10 +2552,14 @@ snapshots: dependencies: callsites: 3.1.0 + parse-ms@4.0.0: {} + path-exists@4.0.0: {} path-key@3.1.1: {} + path-key@4.0.0: {} + path-type@4.0.0: {} pathe@2.0.3: {} @@ -2447,6 +2586,10 @@ snapshots: prettier@3.9.4: {} + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + punycode@2.3.1: {} quansync@0.2.11: {} @@ -2536,6 +2679,8 @@ snapshots: strip-bom@3.0.0: {} + strip-final-newline@4.0.0: {} + strip-json-comments@3.1.1: {} strip-literal@3.1.0: @@ -2599,6 +2744,8 @@ snapshots: undici-types@6.21.0: {} + unicorn-magic@0.3.0: {} + universalify@0.1.2: {} uri-js@4.4.1: @@ -2692,4 +2839,6 @@ snapshots: yocto-queue@0.1.0: {} + yoctocolors@2.1.2: {} + zod@4.4.3: {}