@ocpp-debugkit/toolkit 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +234 -12
- package/dist/cli/commands/anonymize.d.ts +15 -0
- package/dist/cli/commands/anonymize.d.ts.map +1 -0
- package/dist/cli/commands/anonymize.js +104 -0
- package/dist/cli/commands/anonymize.js.map +1 -0
- package/dist/cli/commands/ci.d.ts +10 -0
- package/dist/cli/commands/ci.d.ts.map +1 -0
- package/dist/cli/commands/ci.js +96 -0
- package/dist/cli/commands/ci.js.map +1 -0
- package/dist/cli/commands/diff.d.ts +10 -0
- package/dist/cli/commands/diff.d.ts.map +1 -0
- package/dist/cli/commands/diff.js +71 -0
- package/dist/cli/commands/diff.js.map +1 -0
- package/dist/cli/commands/report.d.ts.map +1 -1
- package/dist/cli/commands/report.js +6 -3
- package/dist/cli/commands/report.js.map +1 -1
- package/dist/cli/commands/scenario.d.ts +11 -3
- package/dist/cli/commands/scenario.d.ts.map +1 -1
- package/dist/cli/commands/scenario.js +41 -4
- package/dist/cli/commands/scenario.js.map +1 -1
- package/dist/cli/index.js +41 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/core/__fixtures__/connector-fault.d.ts +6 -0
- package/dist/core/__fixtures__/connector-fault.d.ts.map +1 -1
- package/dist/core/__fixtures__/connector-fault.js +11 -1
- package/dist/core/__fixtures__/connector-fault.js.map +1 -1
- package/dist/core/__fixtures__/normal-session.d.ts +6 -0
- package/dist/core/__fixtures__/normal-session.d.ts.map +1 -1
- package/dist/core/__fixtures__/normal-session.js +10 -0
- package/dist/core/__fixtures__/normal-session.js.map +1 -1
- package/dist/core/assertions.d.ts +36 -0
- package/dist/core/assertions.d.ts.map +1 -0
- package/dist/core/assertions.js +335 -0
- package/dist/core/assertions.js.map +1 -0
- package/dist/core/detection.d.ts +20 -3
- package/dist/core/detection.d.ts.map +1 -1
- package/dist/core/detection.js +624 -6
- package/dist/core/detection.js.map +1 -1
- package/dist/core/diff.d.ts +68 -0
- package/dist/core/diff.d.ts.map +1 -0
- package/dist/core/diff.js +199 -0
- package/dist/core/diff.js.map +1 -0
- package/dist/core/fixtures/index.d.ts +12 -0
- package/dist/core/fixtures/index.d.ts.map +1 -1
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +4 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/types.d.ts +77 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/react/components.d.ts +9 -45
- package/dist/react/components.d.ts.map +1 -1
- package/dist/react/components.js +120 -38
- package/dist/react/components.js.map +1 -1
- package/dist/reporter/html.d.ts +25 -0
- package/dist/reporter/html.d.ts.map +1 -0
- package/dist/reporter/html.js +396 -0
- package/dist/reporter/html.js.map +1 -0
- package/dist/reporter/index.d.ts +1 -0
- package/dist/reporter/index.d.ts.map +1 -1
- package/dist/reporter/index.js +1 -0
- package/dist/reporter/index.js.map +1 -1
- package/dist/scenarios/__scenarios__/diagnostics-failure.d.ts +58 -0
- package/dist/scenarios/__scenarios__/diagnostics-failure.d.ts.map +1 -0
- package/dist/scenarios/__scenarios__/diagnostics-failure.js +91 -0
- package/dist/scenarios/__scenarios__/diagnostics-failure.js.map +1 -0
- package/dist/scenarios/__scenarios__/heartbeat-irregular.d.ts +51 -0
- package/dist/scenarios/__scenarios__/heartbeat-irregular.d.ts.map +1 -0
- package/dist/scenarios/__scenarios__/heartbeat-irregular.js +81 -0
- package/dist/scenarios/__scenarios__/heartbeat-irregular.js.map +1 -0
- package/dist/scenarios/__scenarios__/invalid-stop-reason.d.ts +118 -0
- package/dist/scenarios/__scenarios__/invalid-stop-reason.d.ts.map +1 -0
- package/dist/scenarios/__scenarios__/invalid-stop-reason.js +226 -0
- package/dist/scenarios/__scenarios__/invalid-stop-reason.js.map +1 -0
- package/dist/scenarios/__scenarios__/meter-anomaly.d.ts +115 -0
- package/dist/scenarios/__scenarios__/meter-anomaly.d.ts.map +1 -0
- package/dist/scenarios/__scenarios__/meter-anomaly.js +152 -0
- package/dist/scenarios/__scenarios__/meter-anomaly.js.map +1 -0
- package/dist/scenarios/__scenarios__/meter-value-gap.d.ts +103 -0
- package/dist/scenarios/__scenarios__/meter-value-gap.d.ts.map +1 -0
- package/dist/scenarios/__scenarios__/meter-value-gap.js +196 -0
- package/dist/scenarios/__scenarios__/meter-value-gap.js.map +1 -0
- package/dist/scenarios/__scenarios__/short-session.d.ts +111 -0
- package/dist/scenarios/__scenarios__/short-session.d.ts.map +1 -0
- package/dist/scenarios/__scenarios__/short-session.js +129 -0
- package/dist/scenarios/__scenarios__/short-session.js.map +1 -0
- package/dist/scenarios/__scenarios__/slow-csms-response.d.ts +52 -0
- package/dist/scenarios/__scenarios__/slow-csms-response.d.ts.map +1 -0
- package/dist/scenarios/__scenarios__/slow-csms-response.js +61 -0
- package/dist/scenarios/__scenarios__/slow-csms-response.js.map +1 -0
- package/dist/scenarios/__scenarios__/station-offline.d.ts +6 -0
- package/dist/scenarios/__scenarios__/station-offline.d.ts.map +1 -1
- package/dist/scenarios/__scenarios__/station-offline.js +10 -0
- package/dist/scenarios/__scenarios__/station-offline.js.map +1 -1
- package/dist/scenarios/__scenarios__/status-transition-violation.d.ts +52 -0
- package/dist/scenarios/__scenarios__/status-transition-violation.d.ts.map +1 -0
- package/dist/scenarios/__scenarios__/status-transition-violation.js +112 -0
- package/dist/scenarios/__scenarios__/status-transition-violation.js.map +1 -0
- package/dist/scenarios/__scenarios__/unexpected-start.d.ts +80 -0
- package/dist/scenarios/__scenarios__/unexpected-start.d.ts.map +1 -0
- package/dist/scenarios/__scenarios__/unexpected-start.js +124 -0
- package/dist/scenarios/__scenarios__/unexpected-start.js.map +1 -0
- package/dist/scenarios/__scenarios__/unexpected-stop-reason.d.ts +6 -0
- package/dist/scenarios/__scenarios__/unexpected-stop-reason.d.ts.map +1 -1
- package/dist/scenarios/__scenarios__/unexpected-stop-reason.js +10 -0
- package/dist/scenarios/__scenarios__/unexpected-stop-reason.js.map +1 -1
- package/dist/scenarios/__scenarios__/unresponsive-csms.d.ts +53 -0
- package/dist/scenarios/__scenarios__/unresponsive-csms.d.ts.map +1 -0
- package/dist/scenarios/__scenarios__/unresponsive-csms.js +53 -0
- package/dist/scenarios/__scenarios__/unresponsive-csms.js.map +1 -0
- package/dist/scenarios/compare.d.ts +39 -0
- package/dist/scenarios/compare.d.ts.map +1 -0
- package/dist/scenarios/compare.js +77 -0
- package/dist/scenarios/compare.js.map +1 -0
- package/dist/scenarios/index.d.ts +15 -3
- package/dist/scenarios/index.d.ts.map +1 -1
- package/dist/scenarios/index.js +45 -4
- package/dist/scenarios/index.js.map +1 -1
- package/package.json +1 -1
package/dist/core/detection.js
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Failure detection — analyzes events and sessions for known failure patterns.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 15 detection rules (v0.1 + v0.2 + v0.3):
|
|
5
|
+
*
|
|
6
|
+
* v0.1:
|
|
5
7
|
* 1. FAILED_AUTHORIZATION — Authorize response with idTagInfo.status = "Invalid"
|
|
6
8
|
* 2. CONNECTOR_FAULT — StatusNotification with status = "Faulted" during active session
|
|
7
|
-
* 3. STATION_OFFLINE_DURING_SESSION — session has StartTransaction but no StopTransaction
|
|
8
|
-
*
|
|
9
|
+
* 3. STATION_OFFLINE_DURING_SESSION — session has StartTransaction but no StopTransaction
|
|
10
|
+
*
|
|
11
|
+
* v0.2:
|
|
12
|
+
* 4. TIMEOUT_NO_HEARTBEAT — no Heartbeat within expected interval after BootNotification
|
|
13
|
+
* 5. METER_VALUE_GAP — no MeterValues during an active transaction
|
|
14
|
+
* 6. INVALID_STOP_REASON — StopTransaction with unexpected/invalid stop reason
|
|
15
|
+
* 7. UNEXPECTED_START — StartTransaction without preceding BootNotification or Authorize
|
|
16
|
+
* 8. STATUS_TRANSITION_VIOLATION — illegal connector status transition
|
|
17
|
+
* 9. DIAGNOSTICS_FAILURE — DiagnosticsStatusNotification indicating failure
|
|
18
|
+
* 10. FIRMWARE_UPDATE_FAILURE — FirmwareStatusNotification indicating failure
|
|
19
|
+
*
|
|
20
|
+
* v0.3:
|
|
21
|
+
* 11. SUSPICIOUS_SESSION_DURATION — session < 60s or > 24h
|
|
22
|
+
* 12. SLOW_RESPONSE — gap between Call and matching CallResult/CallError > 10s
|
|
23
|
+
* 13. HEARTBEAT_INTERVAL_VIOLATION — heartbeat intervals deviate >50% from expected
|
|
24
|
+
* 14. METER_VALUE_ANOMALY — non-monotonic or negative meter readings
|
|
25
|
+
* 15. UNRESPONSIVE_CSMS — Call with no matching CallResult or CallError
|
|
9
26
|
*
|
|
10
27
|
* @see ADR-0003
|
|
11
28
|
*/
|
|
@@ -33,11 +50,103 @@ const SUGGESTED_STEPS = {
|
|
|
33
50
|
'Check if the station firmware has a known stability issue',
|
|
34
51
|
'Investigate if maintenance was performed on the station',
|
|
35
52
|
],
|
|
53
|
+
TIMEOUT_NO_HEARTBEAT: [
|
|
54
|
+
'Check the station network connectivity',
|
|
55
|
+
'Verify the WebSocket connection is stable',
|
|
56
|
+
'Review the station heartbeat interval configuration',
|
|
57
|
+
'Check if the station has rebooted or lost power',
|
|
58
|
+
'Inspect the CSMS for connection acceptance issues',
|
|
59
|
+
],
|
|
60
|
+
METER_VALUE_GAP: [
|
|
61
|
+
'Verify the meter is functioning correctly',
|
|
62
|
+
'Check the meter value reporting interval configuration',
|
|
63
|
+
'Inspect the OCPP connection stability during the session',
|
|
64
|
+
'Review station logs for meter communication errors',
|
|
65
|
+
'Consider hardware replacement if meter is faulty',
|
|
66
|
+
],
|
|
67
|
+
INVALID_STOP_REASON: [
|
|
68
|
+
'Review the StopTransaction payload for the stop reason',
|
|
69
|
+
'Check if the stop reason is within the OCPP 1.6 specification',
|
|
70
|
+
'Investigate why the station used a non-standard reason',
|
|
71
|
+
'Review station firmware for stop reason mapping bugs',
|
|
72
|
+
],
|
|
73
|
+
UNEXPECTED_START: [
|
|
74
|
+
'Verify the station performed BootNotification before starting a transaction',
|
|
75
|
+
'Check if authorization was properly completed before StartTransaction',
|
|
76
|
+
'Review the station startup sequence and timing',
|
|
77
|
+
'Inspect the CSMS for delayed or missing responses',
|
|
78
|
+
],
|
|
79
|
+
STATUS_TRANSITION_VIOLATION: [
|
|
80
|
+
'Review the connector status transition sequence',
|
|
81
|
+
'Check if the station firmware follows the OCPP status model correctly',
|
|
82
|
+
'Verify no manual overrides triggered invalid transitions',
|
|
83
|
+
'Inspect the connector status history for anomalies',
|
|
84
|
+
],
|
|
85
|
+
DIAGNOSTICS_FAILURE: [
|
|
86
|
+
'Review the DiagnosticsStatusNotification payload for the specific status',
|
|
87
|
+
'Check the station diagnostic logs for detailed error information',
|
|
88
|
+
'Verify the station hardware diagnostics are passing',
|
|
89
|
+
'Contact hardware vendor if diagnostics indicate hardware failure',
|
|
90
|
+
],
|
|
91
|
+
FIRMWARE_UPDATE_FAILURE: [
|
|
92
|
+
'Review the FirmwareStatusNotification payload for the specific status',
|
|
93
|
+
'Check if the firmware image was corrupted or incomplete',
|
|
94
|
+
'Verify the station has sufficient storage for the firmware update',
|
|
95
|
+
'Retry the firmware update after addressing the failure cause',
|
|
96
|
+
'Contact the firmware provider if the image is defective',
|
|
97
|
+
],
|
|
98
|
+
SUSPICIOUS_SESSION_DURATION: [
|
|
99
|
+
'Review the session duration in the trace timeline',
|
|
100
|
+
'For very short sessions: check if the transaction was aborted or authorization failed mid-session',
|
|
101
|
+
'For very long sessions: check if the station forgot to send StopTransaction',
|
|
102
|
+
'Verify the station clock is synchronized (NTP)',
|
|
103
|
+
'Check if the session spans a maintenance window or firmware update',
|
|
104
|
+
],
|
|
105
|
+
SLOW_RESPONSE: [
|
|
106
|
+
'Check the CSMS processing time for the affected message type',
|
|
107
|
+
'Review system load and database performance on the CSMS',
|
|
108
|
+
'Verify network latency between station and CSMS',
|
|
109
|
+
'Check if the CSMS is running a long-running synchronous operation',
|
|
110
|
+
'Review CSMS logs for the specific message ID',
|
|
111
|
+
],
|
|
112
|
+
HEARTBEAT_INTERVAL_VIOLATION: [
|
|
113
|
+
'Verify the heartbeat interval configured on the station matches the BootNotification response',
|
|
114
|
+
'Check for network instability causing delayed heartbeats',
|
|
115
|
+
'Review the station clock synchronization (NTP)',
|
|
116
|
+
'Check if the station firmware has a known heartbeat timing bug',
|
|
117
|
+
'Inspect the WebSocket connection stability',
|
|
118
|
+
],
|
|
119
|
+
METER_VALUE_ANOMALY: [
|
|
120
|
+
'Verify the meter is functioning correctly and is properly calibrated',
|
|
121
|
+
'Check for meter communication errors or data corruption',
|
|
122
|
+
'Review the meter value sampling and reporting configuration',
|
|
123
|
+
'Inspect for potential tampering or hardware malfunction',
|
|
124
|
+
'Contact the meter vendor if the issue persists',
|
|
125
|
+
],
|
|
126
|
+
UNRESPONSIVE_CSMS: [
|
|
127
|
+
'Check if the CSMS was online and accepting connections during the session',
|
|
128
|
+
'Verify the WebSocket connection was stable at the time of the unanswered Call',
|
|
129
|
+
'Review CSMS logs for the specific message ID',
|
|
130
|
+
'Check if the CSMS crashed or restarted during the session',
|
|
131
|
+
'Inspect the network path between station and CSMS for packet loss',
|
|
132
|
+
],
|
|
36
133
|
};
|
|
37
134
|
const SEVERITY = {
|
|
38
135
|
FAILED_AUTHORIZATION: 'warning',
|
|
39
136
|
CONNECTOR_FAULT: 'critical',
|
|
40
137
|
STATION_OFFLINE_DURING_SESSION: 'critical',
|
|
138
|
+
TIMEOUT_NO_HEARTBEAT: 'warning',
|
|
139
|
+
METER_VALUE_GAP: 'warning',
|
|
140
|
+
INVALID_STOP_REASON: 'info',
|
|
141
|
+
UNEXPECTED_START: 'warning',
|
|
142
|
+
STATUS_TRANSITION_VIOLATION: 'warning',
|
|
143
|
+
DIAGNOSTICS_FAILURE: 'critical',
|
|
144
|
+
FIRMWARE_UPDATE_FAILURE: 'warning',
|
|
145
|
+
SUSPICIOUS_SESSION_DURATION: 'warning',
|
|
146
|
+
SLOW_RESPONSE: 'warning',
|
|
147
|
+
HEARTBEAT_INTERVAL_VIOLATION: 'info',
|
|
148
|
+
METER_VALUE_ANOMALY: 'warning',
|
|
149
|
+
UNRESPONSIVE_CSMS: 'critical',
|
|
41
150
|
};
|
|
42
151
|
// ---------------------------------------------------------------------------
|
|
43
152
|
// Payload extraction helpers
|
|
@@ -215,12 +324,521 @@ function detectStationOfflineDuringSession(_events, sessions) {
|
|
|
215
324
|
*/
|
|
216
325
|
export function detectFailures(events, sessions) {
|
|
217
326
|
const failures = [];
|
|
218
|
-
//
|
|
327
|
+
// v0.1 rules
|
|
219
328
|
failures.push(...detectFailedAuthorization(events));
|
|
220
|
-
// Rule 2: Connector fault during active session
|
|
221
329
|
failures.push(...detectConnectorFault(events));
|
|
222
|
-
// Rule 3: Station offline during session
|
|
223
330
|
failures.push(...detectStationOfflineDuringSession(events, sessions));
|
|
331
|
+
// v0.2 rules
|
|
332
|
+
failures.push(...detectTimeoutNoHeartbeat(events));
|
|
333
|
+
failures.push(...detectMeterValueGap(events, sessions));
|
|
334
|
+
failures.push(...detectInvalidStopReason(events));
|
|
335
|
+
failures.push(...detectUnexpectedStart(events));
|
|
336
|
+
failures.push(...detectStatusTransitionViolation(events));
|
|
337
|
+
failures.push(...detectDiagnosticsFailure(events));
|
|
338
|
+
failures.push(...detectFirmwareUpdateFailure(events));
|
|
339
|
+
// v0.3 rules
|
|
340
|
+
failures.push(...detectSuspiciousSessionDuration(events, sessions));
|
|
341
|
+
failures.push(...detectSlowResponse(events));
|
|
342
|
+
failures.push(...detectHeartbeatIntervalViolation(events));
|
|
343
|
+
failures.push(...detectMeterValueAnomaly(events, sessions));
|
|
344
|
+
failures.push(...detectUnresponsiveCsms(events));
|
|
345
|
+
return failures;
|
|
346
|
+
}
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
// v0.2 detection rules
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
/** Default heartbeat interval: 60 seconds (OCPP 1.6 default). */
|
|
351
|
+
const DEFAULT_HEARTBEAT_INTERVAL_MS = 60_000;
|
|
352
|
+
/** Valid OCPP 1.6 StopTransaction reasons. */
|
|
353
|
+
const VALID_STOP_REASONS = new Set([
|
|
354
|
+
'EmergencyStop',
|
|
355
|
+
'EVDisconnected',
|
|
356
|
+
'HardReset',
|
|
357
|
+
'Local',
|
|
358
|
+
'Other',
|
|
359
|
+
'PowerLoss',
|
|
360
|
+
'Reboot',
|
|
361
|
+
'Remote',
|
|
362
|
+
'SoftReset',
|
|
363
|
+
'UnlockCommand',
|
|
364
|
+
'DeAuthorized',
|
|
365
|
+
]);
|
|
366
|
+
/** Valid connector statuses per OCPP 1.6. */
|
|
367
|
+
const VALID_CONNECTOR_STATUSES = new Set([
|
|
368
|
+
'Available',
|
|
369
|
+
'Preparing',
|
|
370
|
+
'Charging',
|
|
371
|
+
'SuspendedEVSE',
|
|
372
|
+
'SuspendedEV',
|
|
373
|
+
'Finishing',
|
|
374
|
+
'Reserved',
|
|
375
|
+
'Unavailable',
|
|
376
|
+
'Faulted',
|
|
377
|
+
]);
|
|
378
|
+
/**
|
|
379
|
+
* Rule 4: TIMEOUT_NO_HEARTBEAT
|
|
380
|
+
* Detects when a station sends no Heartbeat within the expected interval
|
|
381
|
+
* after BootNotification. Uses 2x the default interval as the threshold.
|
|
382
|
+
*/
|
|
383
|
+
function detectTimeoutNoHeartbeat(events) {
|
|
384
|
+
const failures = [];
|
|
385
|
+
const bootIndex = events.findIndex((e) => e.messageType === 'Call' && e.action === 'BootNotification');
|
|
386
|
+
if (bootIndex === -1)
|
|
387
|
+
return failures;
|
|
388
|
+
const bootEvent = events[bootIndex];
|
|
389
|
+
if (!bootEvent || bootEvent.timestamp === null)
|
|
390
|
+
return failures;
|
|
391
|
+
// Extract heartbeat interval from BootNotification response if present
|
|
392
|
+
let intervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
393
|
+
const bootResponse = events.find((e) => e.messageType === 'CallResult' &&
|
|
394
|
+
e.messageId === bootEvent.messageId &&
|
|
395
|
+
typeof e.payload?.interval === 'number');
|
|
396
|
+
if (bootResponse) {
|
|
397
|
+
const interval = bootResponse.payload.interval;
|
|
398
|
+
intervalMs = interval * 1000;
|
|
399
|
+
}
|
|
400
|
+
// Look for any Heartbeat within 2x the expected interval after BootNotification
|
|
401
|
+
const threshold = bootEvent.timestamp + intervalMs * 2;
|
|
402
|
+
// Only flag if the trace has events beyond the heartbeat threshold
|
|
403
|
+
// (if the trace ends before the first heartbeat is due, we can't know
|
|
404
|
+
// if the station would have sent one)
|
|
405
|
+
const hasEventsBeyondThreshold = events.some((e) => e.timestamp !== null && e.timestamp > threshold);
|
|
406
|
+
if (!hasEventsBeyondThreshold)
|
|
407
|
+
return failures;
|
|
408
|
+
const hasHeartbeat = events.some((e) => e.messageType === 'Call' &&
|
|
409
|
+
e.action === 'Heartbeat' &&
|
|
410
|
+
e.timestamp !== null &&
|
|
411
|
+
e.timestamp <= threshold);
|
|
412
|
+
if (!hasHeartbeat) {
|
|
413
|
+
failures.push({
|
|
414
|
+
code: 'TIMEOUT_NO_HEARTBEAT',
|
|
415
|
+
description: `No Heartbeat received within ${(intervalMs * 2) / 1000}s of BootNotification (expected every ${intervalMs / 1000}s)`,
|
|
416
|
+
severity: SEVERITY.TIMEOUT_NO_HEARTBEAT,
|
|
417
|
+
eventIds: [bootEvent.id],
|
|
418
|
+
suggestedSteps: SUGGESTED_STEPS.TIMEOUT_NO_HEARTBEAT,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
return failures;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Rule 5: METER_VALUE_GAP
|
|
425
|
+
* Detects active transactions (StartTransaction to StopTransaction) where
|
|
426
|
+
* no MeterValuesRequest was sent.
|
|
427
|
+
*/
|
|
428
|
+
function detectMeterValueGap(_events, sessions) {
|
|
429
|
+
const failures = [];
|
|
430
|
+
for (const session of sessions) {
|
|
431
|
+
if (session.transactionId === null)
|
|
432
|
+
continue;
|
|
433
|
+
const hasStart = session.events.some((e) => e.messageType === 'Call' && e.action === 'StartTransaction');
|
|
434
|
+
const hasStop = session.events.some((e) => e.messageType === 'Call' && e.action === 'StopTransaction');
|
|
435
|
+
if (!hasStart || !hasStop)
|
|
436
|
+
continue;
|
|
437
|
+
const hasMeterValues = session.events.some((e) => e.messageType === 'Call' && e.action === 'MeterValues');
|
|
438
|
+
if (!hasMeterValues) {
|
|
439
|
+
const startEvent = session.events.find((e) => e.messageType === 'Call' && e.action === 'StartTransaction');
|
|
440
|
+
failures.push({
|
|
441
|
+
code: 'METER_VALUE_GAP',
|
|
442
|
+
description: `Session ${session.sessionId} (transaction ${session.transactionId}) has StartTransaction and StopTransaction but no MeterValues — metering data is missing`,
|
|
443
|
+
severity: SEVERITY.METER_VALUE_GAP,
|
|
444
|
+
eventIds: startEvent ? [startEvent.id] : [],
|
|
445
|
+
suggestedSteps: SUGGESTED_STEPS.METER_VALUE_GAP,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return failures;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Rule 6: INVALID_STOP_REASON
|
|
453
|
+
* Detects StopTransaction with a stop reason not in the OCPP 1.6 specification.
|
|
454
|
+
*/
|
|
455
|
+
function detectInvalidStopReason(events) {
|
|
456
|
+
const failures = [];
|
|
457
|
+
for (const event of events) {
|
|
458
|
+
if (event.messageType !== 'Call' || event.action !== 'StopTransaction')
|
|
459
|
+
continue;
|
|
460
|
+
const payload = event.payload;
|
|
461
|
+
const reason = payload?.reason;
|
|
462
|
+
if (typeof reason === 'string' && !VALID_STOP_REASONS.has(reason)) {
|
|
463
|
+
failures.push({
|
|
464
|
+
code: 'INVALID_STOP_REASON',
|
|
465
|
+
description: `StopTransaction has invalid stop reason "${reason}" — not a valid OCPP 1.6 reason code (messageId: ${event.messageId})`,
|
|
466
|
+
severity: SEVERITY.INVALID_STOP_REASON,
|
|
467
|
+
eventIds: [event.id],
|
|
468
|
+
suggestedSteps: SUGGESTED_STEPS.INVALID_STOP_REASON,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return failures;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Rule 7: UNEXPECTED_START
|
|
476
|
+
* Detects StartTransaction without a preceding BootNotification or Authorize.
|
|
477
|
+
*/
|
|
478
|
+
function detectUnexpectedStart(events) {
|
|
479
|
+
const failures = [];
|
|
480
|
+
for (let i = 0; i < events.length; i++) {
|
|
481
|
+
const event = events[i];
|
|
482
|
+
if (!event)
|
|
483
|
+
continue;
|
|
484
|
+
if (event.messageType !== 'Call' || event.action !== 'StartTransaction')
|
|
485
|
+
continue;
|
|
486
|
+
// Check for BootNotification or Authorize before this StartTransaction
|
|
487
|
+
const hasBoot = events
|
|
488
|
+
.slice(0, i)
|
|
489
|
+
.some((e) => e.messageType === 'Call' && e.action === 'BootNotification');
|
|
490
|
+
const hasAuthorize = events
|
|
491
|
+
.slice(0, i)
|
|
492
|
+
.some((e) => e.messageType === 'Call' && e.action === 'Authorize');
|
|
493
|
+
if (!hasBoot && !hasAuthorize) {
|
|
494
|
+
failures.push({
|
|
495
|
+
code: 'UNEXPECTED_START',
|
|
496
|
+
description: `StartTransaction at event ${event.id} without preceding BootNotification or Authorize — transaction started without proper initialization (messageId: ${event.messageId})`,
|
|
497
|
+
severity: SEVERITY.UNEXPECTED_START,
|
|
498
|
+
eventIds: [event.id],
|
|
499
|
+
suggestedSteps: SUGGESTED_STEPS.UNEXPECTED_START,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return failures;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Rule 8: STATUS_TRANSITION_VIOLATION
|
|
507
|
+
* Detects illegal connector status transitions.
|
|
508
|
+
* Valid transitions are based on the OCPP 1.6 connector state model.
|
|
509
|
+
*/
|
|
510
|
+
const VALID_TRANSITIONS = {
|
|
511
|
+
Available: new Set(['Preparing', 'Charging', 'Reserved', 'Unavailable', 'Faulted']),
|
|
512
|
+
Preparing: new Set(['Charging', 'Available', 'SuspendedEVSE', 'Faulted', 'Unavailable']),
|
|
513
|
+
Charging: new Set(['SuspendedEVSE', 'SuspendedEV', 'Finishing', 'Available', 'Faulted']),
|
|
514
|
+
SuspendedEVSE: new Set(['Charging', 'Finishing', 'Available', 'Faulted']),
|
|
515
|
+
SuspendedEV: new Set(['Charging', 'Finishing', 'Available', 'Faulted']),
|
|
516
|
+
Finishing: new Set(['Available', 'Reserved', 'Faulted']),
|
|
517
|
+
Reserved: new Set(['Available', 'Unavailable', 'Faulted']),
|
|
518
|
+
Unavailable: new Set(['Available', 'Faulted']),
|
|
519
|
+
Faulted: new Set(['Unavailable', 'Available']),
|
|
520
|
+
};
|
|
521
|
+
function detectStatusTransitionViolation(events) {
|
|
522
|
+
const failures = [];
|
|
523
|
+
// Collect all StatusNotification statuses in order
|
|
524
|
+
const statusEvents = events.filter((e) => e.messageType === 'Call' && e.action === 'StatusNotification');
|
|
525
|
+
let prevStatus = null;
|
|
526
|
+
let prevEvent = null;
|
|
527
|
+
for (const event of statusEvents) {
|
|
528
|
+
const payload = event.payload;
|
|
529
|
+
const status = payload?.status;
|
|
530
|
+
if (typeof status !== 'string' || !VALID_CONNECTOR_STATUSES.has(status))
|
|
531
|
+
continue;
|
|
532
|
+
if (prevStatus !== null && prevEvent !== null) {
|
|
533
|
+
const allowed = VALID_TRANSITIONS[prevStatus];
|
|
534
|
+
if (allowed && !allowed.has(status)) {
|
|
535
|
+
failures.push({
|
|
536
|
+
code: 'STATUS_TRANSITION_VIOLATION',
|
|
537
|
+
description: `Connector status transition from "${prevStatus}" to "${status}" is not a valid OCPP 1.6 transition (messageId: ${event.messageId})`,
|
|
538
|
+
severity: SEVERITY.STATUS_TRANSITION_VIOLATION,
|
|
539
|
+
eventIds: [prevEvent.id, event.id],
|
|
540
|
+
suggestedSteps: SUGGESTED_STEPS.STATUS_TRANSITION_VIOLATION,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
prevStatus = status;
|
|
545
|
+
prevEvent = event;
|
|
546
|
+
}
|
|
547
|
+
return failures;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Rule 9: DIAGNOSTICS_FAILURE
|
|
551
|
+
* Detects DiagnosticsStatusNotification with a failure status.
|
|
552
|
+
*/
|
|
553
|
+
const DIAGNOSTICS_FAILURE_STATUSES = new Set([
|
|
554
|
+
'Idle',
|
|
555
|
+
'Uploaded',
|
|
556
|
+
'UploadFailed',
|
|
557
|
+
'DiagnosisFailed',
|
|
558
|
+
'NotImplemented',
|
|
559
|
+
]);
|
|
560
|
+
function detectDiagnosticsFailure(events) {
|
|
561
|
+
const failures = [];
|
|
562
|
+
for (const event of events) {
|
|
563
|
+
if (event.messageType !== 'Call' || event.action !== 'DiagnosticsStatusNotification')
|
|
564
|
+
continue;
|
|
565
|
+
const payload = event.payload;
|
|
566
|
+
const status = payload?.status;
|
|
567
|
+
if (typeof status === 'string' && DIAGNOSTICS_FAILURE_STATUSES.has(status)) {
|
|
568
|
+
// Only report actual failures, not successful diagnostics
|
|
569
|
+
if (status === 'UploadFailed' || status === 'DiagnosisFailed') {
|
|
570
|
+
failures.push({
|
|
571
|
+
code: 'DIAGNOSTICS_FAILURE',
|
|
572
|
+
description: `DiagnosticsStatusNotification reported failure status "${status}" (messageId: ${event.messageId})`,
|
|
573
|
+
severity: SEVERITY.DIAGNOSTICS_FAILURE,
|
|
574
|
+
eventIds: [event.id],
|
|
575
|
+
suggestedSteps: SUGGESTED_STEPS.DIAGNOSTICS_FAILURE,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return failures;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Rule 10: FIRMWARE_UPDATE_FAILURE
|
|
584
|
+
* Detects FirmwareStatusNotification indicating a firmware update failure.
|
|
585
|
+
*/
|
|
586
|
+
const FIRMWARE_FAILURE_STATUSES = new Set([
|
|
587
|
+
'DownloadFailed',
|
|
588
|
+
'DownloadPaused',
|
|
589
|
+
'InstallFailed',
|
|
590
|
+
'InstallRebootingFailed',
|
|
591
|
+
]);
|
|
592
|
+
function detectFirmwareUpdateFailure(events) {
|
|
593
|
+
const failures = [];
|
|
594
|
+
for (const event of events) {
|
|
595
|
+
if (event.messageType !== 'Call' || event.action !== 'FirmwareStatusNotification')
|
|
596
|
+
continue;
|
|
597
|
+
const payload = event.payload;
|
|
598
|
+
const status = payload?.status;
|
|
599
|
+
if (typeof status === 'string' && FIRMWARE_FAILURE_STATUSES.has(status)) {
|
|
600
|
+
failures.push({
|
|
601
|
+
code: 'FIRMWARE_UPDATE_FAILURE',
|
|
602
|
+
description: `FirmwareStatusNotification reported failure status "${status}" (messageId: ${event.messageId})`,
|
|
603
|
+
severity: SEVERITY.FIRMWARE_UPDATE_FAILURE,
|
|
604
|
+
eventIds: [event.id],
|
|
605
|
+
suggestedSteps: SUGGESTED_STEPS.FIRMWARE_UPDATE_FAILURE,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return failures;
|
|
610
|
+
}
|
|
611
|
+
// ---------------------------------------------------------------------------
|
|
612
|
+
// v0.3 detection rules
|
|
613
|
+
// ---------------------------------------------------------------------------
|
|
614
|
+
/** Minimum session duration to not be suspicious: 60 seconds. */
|
|
615
|
+
const MIN_SESSION_DURATION_MS = 60_000;
|
|
616
|
+
/** Maximum session duration to not be suspicious: 24 hours. */
|
|
617
|
+
const MAX_SESSION_DURATION_MS = 24 * 60 * 60 * 1000;
|
|
618
|
+
/** Threshold for slow response: 10 seconds. */
|
|
619
|
+
const SLOW_RESPONSE_THRESHOLD_MS = 10_000;
|
|
620
|
+
/** Deviation threshold for heartbeat interval violation: 50%. */
|
|
621
|
+
const HEARTBEAT_DEVIATION_THRESHOLD = 0.5;
|
|
622
|
+
/**
|
|
623
|
+
* Rule 11: SUSPICIOUS_SESSION_DURATION
|
|
624
|
+
* Detects sessions that are suspiciously short (< 60s) or long (> 24h).
|
|
625
|
+
*/
|
|
626
|
+
function detectSuspiciousSessionDuration(_events, sessions) {
|
|
627
|
+
const failures = [];
|
|
628
|
+
for (const session of sessions) {
|
|
629
|
+
if (session.transactionId === null)
|
|
630
|
+
continue;
|
|
631
|
+
if (session.startTime === null || session.endTime === null)
|
|
632
|
+
continue;
|
|
633
|
+
const durationMs = session.endTime - session.startTime;
|
|
634
|
+
if (durationMs < MIN_SESSION_DURATION_MS) {
|
|
635
|
+
failures.push({
|
|
636
|
+
code: 'SUSPICIOUS_SESSION_DURATION',
|
|
637
|
+
description: `Session ${session.sessionId} (transaction ${session.transactionId}) lasted only ${durationMs}ms — suspiciously short session may indicate aborted start or authorization failure`,
|
|
638
|
+
severity: SEVERITY.SUSPICIOUS_SESSION_DURATION,
|
|
639
|
+
eventIds: session.events
|
|
640
|
+
.filter((e) => e.messageType === 'Call' &&
|
|
641
|
+
(e.action === 'StartTransaction' || e.action === 'StopTransaction'))
|
|
642
|
+
.map((e) => e.id),
|
|
643
|
+
suggestedSteps: SUGGESTED_STEPS.SUSPICIOUS_SESSION_DURATION,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
else if (durationMs > MAX_SESSION_DURATION_MS) {
|
|
647
|
+
failures.push({
|
|
648
|
+
code: 'SUSPICIOUS_SESSION_DURATION',
|
|
649
|
+
description: `Session ${session.sessionId} (transaction ${session.transactionId}) lasted ${Math.round(durationMs / (60 * 60 * 1000))} hours — suspiciously long session may indicate forgotten charging or missing StopTransaction`,
|
|
650
|
+
severity: SEVERITY.SUSPICIOUS_SESSION_DURATION,
|
|
651
|
+
eventIds: session.events
|
|
652
|
+
.filter((e) => e.messageType === 'Call' &&
|
|
653
|
+
(e.action === 'StartTransaction' || e.action === 'StopTransaction'))
|
|
654
|
+
.map((e) => e.id),
|
|
655
|
+
suggestedSteps: SUGGESTED_STEPS.SUSPICIOUS_SESSION_DURATION,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return failures;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Rule 12: SLOW_RESPONSE
|
|
663
|
+
* Detects when the gap between a Call and its matching CallResult/CallError
|
|
664
|
+
* exceeds the threshold (default 10s).
|
|
665
|
+
*/
|
|
666
|
+
function detectSlowResponse(events) {
|
|
667
|
+
const failures = [];
|
|
668
|
+
// Build a map of messageId → CallResult/CallError timestamps
|
|
669
|
+
const responseMap = new Map();
|
|
670
|
+
for (const event of events) {
|
|
671
|
+
if ((event.messageType === 'CallResult' || event.messageType === 'CallError') &&
|
|
672
|
+
event.timestamp !== null) {
|
|
673
|
+
responseMap.set(event.messageId, event.timestamp);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
for (const call of events) {
|
|
677
|
+
if (call.messageType !== 'Call' || call.timestamp === null)
|
|
678
|
+
continue;
|
|
679
|
+
const responseTime = responseMap.get(call.messageId);
|
|
680
|
+
if (responseTime === undefined)
|
|
681
|
+
continue; // No matching response — handled by UNRESPONSIVE_CSMS
|
|
682
|
+
const gapMs = responseTime - call.timestamp;
|
|
683
|
+
if (gapMs > SLOW_RESPONSE_THRESHOLD_MS) {
|
|
684
|
+
failures.push({
|
|
685
|
+
code: 'SLOW_RESPONSE',
|
|
686
|
+
description: `Response to ${call.action} (messageId: ${call.messageId}) took ${Math.round(gapMs / 1000)}s — exceeds ${SLOW_RESPONSE_THRESHOLD_MS / 1000}s threshold`,
|
|
687
|
+
severity: SEVERITY.SLOW_RESPONSE,
|
|
688
|
+
eventIds: [call.id],
|
|
689
|
+
suggestedSteps: SUGGESTED_STEPS.SLOW_RESPONSE,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return failures;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Rule 13: HEARTBEAT_INTERVAL_VIOLATION
|
|
697
|
+
* Detects when heartbeat intervals deviate >50% from the expected interval
|
|
698
|
+
* (from BootNotification response, or default 60s).
|
|
699
|
+
*/
|
|
700
|
+
function detectHeartbeatIntervalViolation(events) {
|
|
701
|
+
const failures = [];
|
|
702
|
+
// Extract expected heartbeat interval from BootNotification response
|
|
703
|
+
let expectedIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
704
|
+
const bootIndex = events.findIndex((e) => e.messageType === 'Call' && e.action === 'BootNotification');
|
|
705
|
+
if (bootIndex !== -1) {
|
|
706
|
+
const bootEvent = events[bootIndex];
|
|
707
|
+
if (bootEvent) {
|
|
708
|
+
const bootResponse = events.find((e) => e.messageType === 'CallResult' &&
|
|
709
|
+
e.messageId === bootEvent.messageId &&
|
|
710
|
+
typeof e.payload?.interval === 'number');
|
|
711
|
+
if (bootResponse) {
|
|
712
|
+
const interval = bootResponse.payload.interval;
|
|
713
|
+
expectedIntervalMs = interval * 1000;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
// Collect heartbeat timestamps in order
|
|
718
|
+
const heartbeatTimestamps = events
|
|
719
|
+
.filter((e) => e.messageType === 'Call' && e.action === 'Heartbeat' && e.timestamp !== null)
|
|
720
|
+
.map((e) => ({ id: e.id, timestamp: e.timestamp }));
|
|
721
|
+
if (heartbeatTimestamps.length < 2)
|
|
722
|
+
return failures;
|
|
723
|
+
for (let i = 1; i < heartbeatTimestamps.length; i++) {
|
|
724
|
+
const prev = heartbeatTimestamps[i - 1];
|
|
725
|
+
const curr = heartbeatTimestamps[i];
|
|
726
|
+
if (!prev || !curr)
|
|
727
|
+
continue;
|
|
728
|
+
const actualGap = curr.timestamp - prev.timestamp;
|
|
729
|
+
const deviation = Math.abs(actualGap - expectedIntervalMs) / expectedIntervalMs;
|
|
730
|
+
if (deviation > HEARTBEAT_DEVIATION_THRESHOLD) {
|
|
731
|
+
failures.push({
|
|
732
|
+
code: 'HEARTBEAT_INTERVAL_VIOLATION',
|
|
733
|
+
description: `Heartbeat interval deviation: ${Math.round(actualGap / 1000)}s between heartbeats, expected ~${Math.round(expectedIntervalMs / 1000)}s (${Math.round(deviation * 100)}% deviation)`,
|
|
734
|
+
severity: SEVERITY.HEARTBEAT_INTERVAL_VIOLATION,
|
|
735
|
+
eventIds: [prev.id, curr.id],
|
|
736
|
+
suggestedSteps: SUGGESTED_STEPS.HEARTBEAT_INTERVAL_VIOLATION,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return failures;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Rule 14: METER_VALUE_ANOMALY
|
|
744
|
+
* Detects non-monotonic (decreasing) or negative meter readings during
|
|
745
|
+
* an active transaction.
|
|
746
|
+
*/
|
|
747
|
+
function detectMeterValueAnomaly(_events, sessions) {
|
|
748
|
+
const failures = [];
|
|
749
|
+
for (const session of sessions) {
|
|
750
|
+
if (session.transactionId === null)
|
|
751
|
+
continue;
|
|
752
|
+
// Collect meter values from MeterValues calls in order
|
|
753
|
+
const meterEvents = session.events.filter((e) => e.messageType === 'Call' && e.action === 'MeterValues');
|
|
754
|
+
if (meterEvents.length === 0)
|
|
755
|
+
continue;
|
|
756
|
+
// Extract numeric meter values from the payload
|
|
757
|
+
// OCPP 1.6 MeterValues: { connectorId, transactionId, meterValue: [{ timestamp, sampledValue: [{ value, ... }] }] }
|
|
758
|
+
const readings = [];
|
|
759
|
+
for (const event of meterEvents) {
|
|
760
|
+
const payload = event.payload;
|
|
761
|
+
const meterValues = payload?.meterValue;
|
|
762
|
+
if (!Array.isArray(meterValues))
|
|
763
|
+
continue;
|
|
764
|
+
for (const mv of meterValues) {
|
|
765
|
+
const sampledValues = mv?.sampledValue;
|
|
766
|
+
if (!Array.isArray(sampledValues))
|
|
767
|
+
continue;
|
|
768
|
+
for (const sv of sampledValues) {
|
|
769
|
+
const rawValue = sv?.value;
|
|
770
|
+
if (typeof rawValue === 'string') {
|
|
771
|
+
const numValue = Number.parseFloat(rawValue);
|
|
772
|
+
if (!Number.isNaN(numValue)) {
|
|
773
|
+
readings.push({ eventId: event.id, value: numValue });
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
else if (typeof rawValue === 'number') {
|
|
777
|
+
readings.push({ eventId: event.id, value: rawValue });
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
if (readings.length === 0)
|
|
783
|
+
continue;
|
|
784
|
+
// Check for negative values
|
|
785
|
+
for (const reading of readings) {
|
|
786
|
+
if (reading.value < 0) {
|
|
787
|
+
failures.push({
|
|
788
|
+
code: 'METER_VALUE_ANOMALY',
|
|
789
|
+
description: `Negative meter value detected: ${reading.value} in session ${session.sessionId} (transaction ${session.transactionId})`,
|
|
790
|
+
severity: SEVERITY.METER_VALUE_ANOMALY,
|
|
791
|
+
eventIds: [reading.eventId],
|
|
792
|
+
suggestedSteps: SUGGESTED_STEPS.METER_VALUE_ANOMALY,
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
// Check for non-monotonic (decreasing) values
|
|
797
|
+
for (let i = 1; i < readings.length; i++) {
|
|
798
|
+
const prev = readings[i - 1];
|
|
799
|
+
const curr = readings[i];
|
|
800
|
+
if (!prev || !curr)
|
|
801
|
+
continue;
|
|
802
|
+
if (curr.value < prev.value) {
|
|
803
|
+
failures.push({
|
|
804
|
+
code: 'METER_VALUE_ANOMALY',
|
|
805
|
+
description: `Non-monotonic meter reading: value decreased from ${prev.value} to ${curr.value} in session ${session.sessionId} (transaction ${session.transactionId})`,
|
|
806
|
+
severity: SEVERITY.METER_VALUE_ANOMALY,
|
|
807
|
+
eventIds: [prev.eventId, curr.eventId],
|
|
808
|
+
suggestedSteps: SUGGESTED_STEPS.METER_VALUE_ANOMALY,
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return failures;
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Rule 15: UNRESPONSIVE_CSMS
|
|
817
|
+
* Detects Call messages that have no matching CallResult or CallError
|
|
818
|
+
* (the CSMS never responded).
|
|
819
|
+
*/
|
|
820
|
+
function detectUnresponsiveCsms(events) {
|
|
821
|
+
const failures = [];
|
|
822
|
+
// Build a set of messageIds that have responses
|
|
823
|
+
const respondedMessageIds = new Set();
|
|
824
|
+
for (const event of events) {
|
|
825
|
+
if (event.messageType === 'CallResult' || event.messageType === 'CallError') {
|
|
826
|
+
respondedMessageIds.add(event.messageId);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
for (const call of events) {
|
|
830
|
+
if (call.messageType !== 'Call')
|
|
831
|
+
continue;
|
|
832
|
+
if (!respondedMessageIds.has(call.messageId)) {
|
|
833
|
+
failures.push({
|
|
834
|
+
code: 'UNRESPONSIVE_CSMS',
|
|
835
|
+
description: `No response received for ${call.action} Call (messageId: ${call.messageId}) — CSMS did not respond with CallResult or CallError`,
|
|
836
|
+
severity: SEVERITY.UNRESPONSIVE_CSMS,
|
|
837
|
+
eventIds: [call.id],
|
|
838
|
+
suggestedSteps: SUGGESTED_STEPS.UNRESPONSIVE_CSMS,
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
}
|
|
224
842
|
return failures;
|
|
225
843
|
}
|
|
226
844
|
//# sourceMappingURL=detection.js.map
|