FAILURE MAP
← Case archive

FA-93206 / EV charging session scheduling / Open access

Connector session state machine: abrupt unplug classification · case 01

Unplugging a suspended vehicle is not flagged as an abrupt termination.

Verified by executionVariant 1 · 7 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

Only the Charging state counts toward abrupt unplugs.

VERIFIED REPAIR

Count unplugs from Charging or SuspendedEV as abrupt.

Unsuccessful approach: Counting everything except Preparing also flags orderly unplugs from Finishing.

Case contract

Chronological [t, event] pairs drive states Available, Preparing, Charging, SuspendedEV, Finishing. plug: Available->Preparing. auth: Preparing->Charging. ev_pause/ev_resume toggle Charging/SuspendedEV. stop: Charging or SuspendedEV->Finishing. unplug: any non-Available state ->Available, counted abrupt from Charging or SuspendedEV. Before handling each event, a Preparing connector whose plug is more than timeout seconds old returns to Available (one timeout). Invalid events are ignored. Return [state, seconds spent in Charging, timeouts, abrupt].

Why this case matters

Depot, workplace and public EV chargers schedule sessions against prices, circuit limits and departure deadlines; a wrong decision silently strands a driver or overloads a feeder.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(events, timeout):
    state = 'Available'
    plug_t = None
    since = None
    charged = 0
    timeouts = 0
    abrupt = 0
    for t, ev in events:
        if state == 'Preparing' and t - plug_t > timeout:
            state = 'Available'
            timeouts += 1
        if ev == 'plug' and state == 'Available':
            state = 'Preparing'
            plug_t = t
        elif ev == 'auth' and state == 'Preparing':
            state = 'Charging'
            since = t
        elif ev == 'ev_pause' and state == 'Charging':
            charged += t - since
            state = 'SuspendedEV'
        elif ev == 'ev_resume' and state == 'SuspendedEV':
            state = 'Charging'
            since = t
        elif ev == 'stop' and state in ('Charging', 'SuspendedEV'):
            if state == 'Charging':
                charged += t - since
            state = 'Finishing'
        elif ev == 'unplug' and state != 'Available':
            if state == 'Charging':
                charged += t - since
            if state == 'Charging':
                abrupt += 1
            state = 'Available'
    return [state, charged, timeouts, abrupt]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: authorised exactly at timeout', [[[0, 'plug'], [60, 'auth'], [100, 'stop']], 60],
   ['Finishing', 40, 0, 0]],
  ['normal: pause and resume',
   [[[0, 'plug'], [5, 'auth'], [65, 'ev_pause'], [125, 'ev_resume'], [185, 'stop'],
     [190, 'unplug']],
    60],
   ['Available', 120, 0, 0]],
  ['boundary: unplug while charging', [[[0, 'plug'], [10, 'auth'], [70, 'unplug']], 60],
   ['Available', 60, 0, 1]],
  ['regression: abrupt unplug classification',
   [[[60, 'ev_pause'], [360, 'ev_pause'], [660, 'ev_pause'], [720, 'auth'], [781, 'plug'],
     [791, 'auth'], [1091, 'ev_pause'], [1391, 'auth'], [1691, 'unplug']],
    90],
   ['Available', 300, 0, 1]],
  ['regression: abrupt unplug classification (partial repair)',
   [[[5, 'plug'], [10, 'auth'], [71, 'stop'], [76, 'unplug'], [137, 'ev_resume'], [142, 'plug'],
     [142, 'plug'], [202, 'plug'], [322, 'ev_resume'], [352, 'auth']],
    30],
   ['Available', 61, 2, 0]],
  ['control 1',
   [[[5, 'plug'], [5, 'plug'], [15, 'plug'], [315, 'auth'], [376, 'ev_resume'], [496, 'auth'],
     [796, 'auth'], [1096, 'unplug']],
    60],
   ['Available', 0, 1, 0]],
  ['control 2', [[[30, 'unplug'], [91, 'auth'], [96, 'unplug'], [106, 'ev_resume']], 90],
   ['Available', 0, 0, 0]]],
 [['boundary: authorised exactly at timeout', [[[0, 'plug'], [60, 'auth'], [100, 'stop']], 60],
   ['Finishing', 40, 0, 0]],
  ['normal: pause and resume',
   [[[0, 'plug'], [5, 'auth'], [65, 'ev_pause'], [125, 'ev_resume'], [185, 'stop'],
     [190, 'unplug']],
    60],
   ['Available', 120, 0, 0]],
  ['boundary: unplug while charging', [[[0, 'plug'], [10, 'auth'], [70, 'unplug']], 60],
   ['Available', 60, 0, 1]],
  ['regression: abrupt unplug classification',
   [[[60, 'ev_pause'], [360, 'ev_pause'], [660, 'ev_pause'], [720, 'auth'], [781, 'plug'],
     [791, 'auth'], [1091, 'ev_pause'], [1391, 'auth'], [1691, 'unplug']],
    90],
   ['Available', 300, 0, 1]],
  ['regression: abrupt unplug classification (partial repair)',
   [[[5, 'plug'], [10, 'auth'], [71, 'stop'], [76, 'unplug'], [137, 'ev_resume'], [142, 'plug'],
     [142, 'plug'], [202, 'plug'], [322, 'ev_resume'], [352, 'auth']],
    30],
   ['Available', 61, 2, 0]],
  ['control 1', [[[120, 'auth'], [240, 'ev_resume'], [540, 'unplug']], 60], ['Available', 0, 0, 0]],
  ['control 2', [[[60, 'unplug'], [180, 'stop'], [240, 'stop'], [270, 'unplug']], 60],
   ['Available', 0, 0, 0]]],
 [['boundary: authorised exactly at timeout', [[[0, 'plug'], [60, 'auth'], [100, 'stop']], 60],
   ['Finishing', 40, 0, 0]],
  ['normal: pause and resume',
   [[[0, 'plug'], [5, 'auth'], [65, 'ev_pause'], [125, 'ev_resume'], [185, 'stop'],
     [190, 'unplug']],
    60],
   ['Available', 120, 0, 0]],
  ['boundary: unplug while charging', [[[0, 'plug'], [10, 'auth'], [70, 'unplug']], 60],
   ['Available', 60, 0, 1]],
  ['regression: abrupt unplug classification',
   [[[60, 'ev_pause'], [360, 'ev_pause'], [660, 'ev_pause'], [720, 'auth'], [781, 'plug'],
     [791, 'auth'], [1091, 'ev_pause'], [1391, 'auth'], [1691, 'unplug']],
    90],
   ['Available', 300, 0, 1]],
  ['regression: abrupt unplug classification (partial repair)',
   [[[60, 'plug'], [121, 'auth'], [241, 'auth'], [251, 'stop'], [371, 'unplug'], [376, 'ev_pause'],
     [406, 'unplug']],
    90],
   ['Available', 130, 0, 0]],
  ['control 1', [[[0, 'unplug'], [10, 'ev_pause'], [10, 'plug']], 60], ['Preparing', 0, 0, 0]],
  ['control 2', [[[300, 'plug'], [360, 'ev_pause'], [421, 'ev_pause']], 30],
   ['Available', 0, 1, 0]]],
 [['boundary: authorised exactly at timeout', [[[0, 'plug'], [60, 'auth'], [100, 'stop']], 60],
   ['Finishing', 40, 0, 0]],
  ['normal: pause and resume',
   [[[0, 'plug'], [5, 'auth'], [65, 'ev_pause'], [125, 'ev_resume'], [185, 'stop'],
     [190, 'unplug']],
    60],
   ['Available', 120, 0, 0]],
  ['boundary: unplug while charging', [[[0, 'plug'], [10, 'auth'], [70, 'unplug']], 60],
   ['Available', 60, 0, 1]],
  ['regression: abrupt unplug classification',
   [[[30, 'plug'], [60, 'auth'], [120, 'ev_resume'], [180, 'ev_resume'], [185, 'plug'],
     [190, 'plug'], [250, 'ev_pause'], [370, 'unplug']],
    30],
   ['Available', 190, 0, 1]],
  ['regression: abrupt unplug classification (partial repair)',
   [[[61, 'plug'], [91, 'auth'], [101, 'stop'], [401, 'ev_resume'], [701, 'plug'], [701, 'plug'],
     [706, 'unplug'], [706, 'ev_pause'], [736, 'auth']],
    90],
   ['Available', 10, 0, 0]],
  ['control 1',
   [[[61, 'auth'], [91, 'unplug'], [211, 'unplug'], [221, 'ev_resume'], [281, 'plug'],
     [311, 'ev_pause'], [321, 'unplug'], [326, 'plug']],
    30],
   ['Preparing', 0, 1, 0]],
  ['control 2',
   [[[300, 'plug'], [600, 'ev_resume'], [900, 'ev_resume'], [961, 'unplug'], [966, 'auth'],
     [971, 'unplug'], [1271, 'auth'], [1276, 'ev_pause']],
    60],
   ['Available', 0, 1, 0]]],
 [['boundary: authorised exactly at timeout', [[[0, 'plug'], [60, 'auth'], [100, 'stop']], 60],
   ['Finishing', 40, 0, 0]],
  ['normal: pause and resume',
   [[[0, 'plug'], [5, 'auth'], [65, 'ev_pause'], [125, 'ev_resume'], [185, 'stop'],
     [190, 'unplug']],
    60],
   ['Available', 120, 0, 0]],
  ['boundary: unplug while charging', [[[0, 'plug'], [10, 'auth'], [70, 'unplug']], 60],
   ['Available', 60, 0, 1]],
  ['regression: abrupt unplug classification',
   [[[10, 'plug'], [130, 'auth'], [250, 'plug'], [250, 'auth'], [250, 'ev_pause'], [550, 'unplug'],
     [555, 'auth'], [565, 'auth']],
    60],
   ['Available', 0, 1, 1]],
  ['regression: abrupt unplug classification (partial repair)',
   [[[30, 'plug'], [150, 'stop'], [211, 'plug'], [272, 'auth'], [392, 'stop'], [453, 'plug'],
     [753, 'unplug']],
    90],
   ['Available', 120, 1, 0]],
  ['control 1', [[[61, 'unplug'], [361, 'auth'], [421, 'unplug'], [426, 'ev_pause']], 60],
   ['Available', 0, 0, 0]],
  ['control 2',
   [[[5, 'ev_resume'], [305, 'unplug'], [605, 'plug'], [665, 'ev_resume'], [670, 'ev_resume']], 60],
   ['Available', 0, 1, 0]]]]
for label, args, expected in fixtures[N-1]:
    check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
boundary: authorised exactly at timeout['Finishing', 40, 0, 0]['Finishing', 40, 0, 0]Passed
normal: pause and resume['Available', 120, 0, 0]['Available', 120, 0, 0]Passed
boundary: unplug while charging['Available', 60, 0, 1]['Available', 60, 0, 1]Passed
regression: abrupt unplug classification['Available', 300, 0, 0]['Available', 300, 0, 1]Failed
regression: abrupt unplug classification (partial repair)['Available', 61, 2, 0]['Available', 61, 2, 0]Passed
control 1['Available', 0, 1, 0]['Available', 0, 1, 0]Passed
control 2['Available', 0, 0, 0]['Available', 0, 0, 0]Passed

SHA-256 / 50a3eda0cad78e69afaab16551ceff26bfe06b3ed5952e249b027615ff8ed866

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(events, timeout):
    state = 'Available'
    plug_t = None
    since = None
    charged = 0
    timeouts = 0
    abrupt = 0
    for t, ev in events:
        if state == 'Preparing' and t - plug_t > timeout:
            state = 'Available'
            timeouts += 1
        if ev == 'plug' and state == 'Available':
            state = 'Preparing'
            plug_t = t
        elif ev == 'auth' and state == 'Preparing':
            state = 'Charging'
            since = t
        elif ev == 'ev_pause' and state == 'Charging':
            charged += t - since
            state = 'SuspendedEV'
        elif ev == 'ev_resume' and state == 'SuspendedEV':
            state = 'Charging'
            since = t
        elif ev == 'stop' and state in ('Charging', 'SuspendedEV'):
            if state == 'Charging':
                charged += t - since
            state = 'Finishing'
        elif ev == 'unplug' and state != 'Available':
            if state == 'Charging':
                charged += t - since
            if state != 'Preparing':
                abrupt += 1
            state = 'Available'
    return [state, charged, timeouts, abrupt]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: authorised exactly at timeout', [[[0, 'plug'], [60, 'auth'], [100, 'stop']], 60],
   ['Finishing', 40, 0, 0]],
  ['normal: pause and resume',
   [[[0, 'plug'], [5, 'auth'], [65, 'ev_pause'], [125, 'ev_resume'], [185, 'stop'],
     [190, 'unplug']],
    60],
   ['Available', 120, 0, 0]],
  ['boundary: unplug while charging', [[[0, 'plug'], [10, 'auth'], [70, 'unplug']], 60],
   ['Available', 60, 0, 1]],
  ['regression: abrupt unplug classification',
   [[[60, 'ev_pause'], [360, 'ev_pause'], [660, 'ev_pause'], [720, 'auth'], [781, 'plug'],
     [791, 'auth'], [1091, 'ev_pause'], [1391, 'auth'], [1691, 'unplug']],
    90],
   ['Available', 300, 0, 1]],
  ['regression: abrupt unplug classification (partial repair)',
   [[[5, 'plug'], [10, 'auth'], [71, 'stop'], [76, 'unplug'], [137, 'ev_resume'], [142, 'plug'],
     [142, 'plug'], [202, 'plug'], [322, 'ev_resume'], [352, 'auth']],
    30],
   ['Available', 61, 2, 0]],
  ['control 1',
   [[[5, 'plug'], [5, 'plug'], [15, 'plug'], [315, 'auth'], [376, 'ev_resume'], [496, 'auth'],
     [796, 'auth'], [1096, 'unplug']],
    60],
   ['Available', 0, 1, 0]],
  ['control 2', [[[30, 'unplug'], [91, 'auth'], [96, 'unplug'], [106, 'ev_resume']], 90],
   ['Available', 0, 0, 0]]],
 [['boundary: authorised exactly at timeout', [[[0, 'plug'], [60, 'auth'], [100, 'stop']], 60],
   ['Finishing', 40, 0, 0]],
  ['normal: pause and resume',
   [[[0, 'plug'], [5, 'auth'], [65, 'ev_pause'], [125, 'ev_resume'], [185, 'stop'],
     [190, 'unplug']],
    60],
   ['Available', 120, 0, 0]],
  ['boundary: unplug while charging', [[[0, 'plug'], [10, 'auth'], [70, 'unplug']], 60],
   ['Available', 60, 0, 1]],
  ['regression: abrupt unplug classification',
   [[[60, 'ev_pause'], [360, 'ev_pause'], [660, 'ev_pause'], [720, 'auth'], [781, 'plug'],
     [791, 'auth'], [1091, 'ev_pause'], [1391, 'auth'], [1691, 'unplug']],
    90],
   ['Available', 300, 0, 1]],
  ['regression: abrupt unplug classification (partial repair)',
   [[[5, 'plug'], [10, 'auth'], [71, 'stop'], [76, 'unplug'], [137, 'ev_resume'], [142, 'plug'],
     [142, 'plug'], [202, 'plug'], [322, 'ev_resume'], [352, 'auth']],
    30],
   ['Available', 61, 2, 0]],
  ['control 1', [[[120, 'auth'], [240, 'ev_resume'], [540, 'unplug']], 60], ['Available', 0, 0, 0]],
  ['control 2', [[[60, 'unplug'], [180, 'stop'], [240, 'stop'], [270, 'unplug']], 60],
   ['Available', 0, 0, 0]]],
 [['boundary: authorised exactly at timeout', [[[0, 'plug'], [60, 'auth'], [100, 'stop']], 60],
   ['Finishing', 40, 0, 0]],
  ['normal: pause and resume',
   [[[0, 'plug'], [5, 'auth'], [65, 'ev_pause'], [125, 'ev_resume'], [185, 'stop'],
     [190, 'unplug']],
    60],
   ['Available', 120, 0, 0]],
  ['boundary: unplug while charging', [[[0, 'plug'], [10, 'auth'], [70, 'unplug']], 60],
   ['Available', 60, 0, 1]],
  ['regression: abrupt unplug classification',
   [[[60, 'ev_pause'], [360, 'ev_pause'], [660, 'ev_pause'], [720, 'auth'], [781, 'plug'],
     [791, 'auth'], [1091, 'ev_pause'], [1391, 'auth'], [1691, 'unplug']],
    90],
   ['Available', 300, 0, 1]],
  ['regression: abrupt unplug classification (partial repair)',
   [[[60, 'plug'], [121, 'auth'], [241, 'auth'], [251, 'stop'], [371, 'unplug'], [376, 'ev_pause'],
     [406, 'unplug']],
    90],
   ['Available', 130, 0, 0]],
  ['control 1', [[[0, 'unplug'], [10, 'ev_pause'], [10, 'plug']], 60], ['Preparing', 0, 0, 0]],
  ['control 2', [[[300, 'plug'], [360, 'ev_pause'], [421, 'ev_pause']], 30],
   ['Available', 0, 1, 0]]],
 [['boundary: authorised exactly at timeout', [[[0, 'plug'], [60, 'auth'], [100, 'stop']], 60],
   ['Finishing', 40, 0, 0]],
  ['normal: pause and resume',
   [[[0, 'plug'], [5, 'auth'], [65, 'ev_pause'], [125, 'ev_resume'], [185, 'stop'],
     [190, 'unplug']],
    60],
   ['Available', 120, 0, 0]],
  ['boundary: unplug while charging', [[[0, 'plug'], [10, 'auth'], [70, 'unplug']], 60],
   ['Available', 60, 0, 1]],
  ['regression: abrupt unplug classification',
   [[[30, 'plug'], [60, 'auth'], [120, 'ev_resume'], [180, 'ev_resume'], [185, 'plug'],
     [190, 'plug'], [250, 'ev_pause'], [370, 'unplug']],
    30],
   ['Available', 190, 0, 1]],
  ['regression: abrupt unplug classification (partial repair)',
   [[[61, 'plug'], [91, 'auth'], [101, 'stop'], [401, 'ev_resume'], [701, 'plug'], [701, 'plug'],
     [706, 'unplug'], [706, 'ev_pause'], [736, 'auth']],
    90],
   ['Available', 10, 0, 0]],
  ['control 1',
   [[[61, 'auth'], [91, 'unplug'], [211, 'unplug'], [221, 'ev_resume'], [281, 'plug'],
     [311, 'ev_pause'], [321, 'unplug'], [326, 'plug']],
    30],
   ['Preparing', 0, 1, 0]],
  ['control 2',
   [[[300, 'plug'], [600, 'ev_resume'], [900, 'ev_resume'], [961, 'unplug'], [966, 'auth'],
     [971, 'unplug'], [1271, 'auth'], [1276, 'ev_pause']],
    60],
   ['Available', 0, 1, 0]]],
 [['boundary: authorised exactly at timeout', [[[0, 'plug'], [60, 'auth'], [100, 'stop']], 60],
   ['Finishing', 40, 0, 0]],
  ['normal: pause and resume',
   [[[0, 'plug'], [5, 'auth'], [65, 'ev_pause'], [125, 'ev_resume'], [185, 'stop'],
     [190, 'unplug']],
    60],
   ['Available', 120, 0, 0]],
  ['boundary: unplug while charging', [[[0, 'plug'], [10, 'auth'], [70, 'unplug']], 60],
   ['Available', 60, 0, 1]],
  ['regression: abrupt unplug classification',
   [[[10, 'plug'], [130, 'auth'], [250, 'plug'], [250, 'auth'], [250, 'ev_pause'], [550, 'unplug'],
     [555, 'auth'], [565, 'auth']],
    60],
   ['Available', 0, 1, 1]],
  ['regression: abrupt unplug classification (partial repair)',
   [[[30, 'plug'], [150, 'stop'], [211, 'plug'], [272, 'auth'], [392, 'stop'], [453, 'plug'],
     [753, 'unplug']],
    90],
   ['Available', 120, 1, 0]],
  ['control 1', [[[61, 'unplug'], [361, 'auth'], [421, 'unplug'], [426, 'ev_pause']], 60],
   ['Available', 0, 0, 0]],
  ['control 2',
   [[[5, 'ev_resume'], [305, 'unplug'], [605, 'plug'], [665, 'ev_resume'], [670, 'ev_resume']], 60],
   ['Available', 0, 1, 0]]]]
for label, args, expected in fixtures[N-1]:
    check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
boundary: authorised exactly at timeout['Finishing', 40, 0, 0]['Finishing', 40, 0, 0]Passed
normal: pause and resume['Available', 120, 0, 1]['Available', 120, 0, 0]Failed
boundary: unplug while charging['Available', 60, 0, 1]['Available', 60, 0, 1]Passed
regression: abrupt unplug classification['Available', 300, 0, 1]['Available', 300, 0, 1]Passed
regression: abrupt unplug classification (partial repair)['Available', 61, 2, 1]['Available', 61, 2, 0]Failed
control 1['Available', 0, 1, 0]['Available', 0, 1, 0]Passed
control 2['Available', 0, 0, 0]['Available', 0, 0, 0]Passed

SHA-256 / ba6cef1f8301b3fbc148b6536e352a5d0d18450a3b74bfe30187c026662e0967

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(events, timeout):
    state = 'Available'
    plug_t = None
    since = None
    charged = 0
    timeouts = 0
    abrupt = 0
    for t, ev in events:
        if state == 'Preparing' and t - plug_t > timeout:
            state = 'Available'
            timeouts += 1
        if ev == 'plug' and state == 'Available':
            state = 'Preparing'
            plug_t = t
        elif ev == 'auth' and state == 'Preparing':
            state = 'Charging'
            since = t
        elif ev == 'ev_pause' and state == 'Charging':
            charged += t - since
            state = 'SuspendedEV'
        elif ev == 'ev_resume' and state == 'SuspendedEV':
            state = 'Charging'
            since = t
        elif ev == 'stop' and state in ('Charging', 'SuspendedEV'):
            if state == 'Charging':
                charged += t - since
            state = 'Finishing'
        elif ev == 'unplug' and state != 'Available':
            if state == 'Charging':
                charged += t - since
            if state in ('Charging', 'SuspendedEV'):
                abrupt += 1
            state = 'Available'
    return [state, charged, timeouts, abrupt]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: authorised exactly at timeout', [[[0, 'plug'], [60, 'auth'], [100, 'stop']], 60],
   ['Finishing', 40, 0, 0]],
  ['normal: pause and resume',
   [[[0, 'plug'], [5, 'auth'], [65, 'ev_pause'], [125, 'ev_resume'], [185, 'stop'],
     [190, 'unplug']],
    60],
   ['Available', 120, 0, 0]],
  ['boundary: unplug while charging', [[[0, 'plug'], [10, 'auth'], [70, 'unplug']], 60],
   ['Available', 60, 0, 1]],
  ['regression: abrupt unplug classification',
   [[[60, 'ev_pause'], [360, 'ev_pause'], [660, 'ev_pause'], [720, 'auth'], [781, 'plug'],
     [791, 'auth'], [1091, 'ev_pause'], [1391, 'auth'], [1691, 'unplug']],
    90],
   ['Available', 300, 0, 1]],
  ['regression: abrupt unplug classification (partial repair)',
   [[[5, 'plug'], [10, 'auth'], [71, 'stop'], [76, 'unplug'], [137, 'ev_resume'], [142, 'plug'],
     [142, 'plug'], [202, 'plug'], [322, 'ev_resume'], [352, 'auth']],
    30],
   ['Available', 61, 2, 0]],
  ['control 1',
   [[[5, 'plug'], [5, 'plug'], [15, 'plug'], [315, 'auth'], [376, 'ev_resume'], [496, 'auth'],
     [796, 'auth'], [1096, 'unplug']],
    60],
   ['Available', 0, 1, 0]],
  ['control 2', [[[30, 'unplug'], [91, 'auth'], [96, 'unplug'], [106, 'ev_resume']], 90],
   ['Available', 0, 0, 0]]],
 [['boundary: authorised exactly at timeout', [[[0, 'plug'], [60, 'auth'], [100, 'stop']], 60],
   ['Finishing', 40, 0, 0]],
  ['normal: pause and resume',
   [[[0, 'plug'], [5, 'auth'], [65, 'ev_pause'], [125, 'ev_resume'], [185, 'stop'],
     [190, 'unplug']],
    60],
   ['Available', 120, 0, 0]],
  ['boundary: unplug while charging', [[[0, 'plug'], [10, 'auth'], [70, 'unplug']], 60],
   ['Available', 60, 0, 1]],
  ['regression: abrupt unplug classification',
   [[[60, 'ev_pause'], [360, 'ev_pause'], [660, 'ev_pause'], [720, 'auth'], [781, 'plug'],
     [791, 'auth'], [1091, 'ev_pause'], [1391, 'auth'], [1691, 'unplug']],
    90],
   ['Available', 300, 0, 1]],
  ['regression: abrupt unplug classification (partial repair)',
   [[[5, 'plug'], [10, 'auth'], [71, 'stop'], [76, 'unplug'], [137, 'ev_resume'], [142, 'plug'],
     [142, 'plug'], [202, 'plug'], [322, 'ev_resume'], [352, 'auth']],
    30],
   ['Available', 61, 2, 0]],
  ['control 1', [[[120, 'auth'], [240, 'ev_resume'], [540, 'unplug']], 60], ['Available', 0, 0, 0]],
  ['control 2', [[[60, 'unplug'], [180, 'stop'], [240, 'stop'], [270, 'unplug']], 60],
   ['Available', 0, 0, 0]]],
 [['boundary: authorised exactly at timeout', [[[0, 'plug'], [60, 'auth'], [100, 'stop']], 60],
   ['Finishing', 40, 0, 0]],
  ['normal: pause and resume',
   [[[0, 'plug'], [5, 'auth'], [65, 'ev_pause'], [125, 'ev_resume'], [185, 'stop'],
     [190, 'unplug']],
    60],
   ['Available', 120, 0, 0]],
  ['boundary: unplug while charging', [[[0, 'plug'], [10, 'auth'], [70, 'unplug']], 60],
   ['Available', 60, 0, 1]],
  ['regression: abrupt unplug classification',
   [[[60, 'ev_pause'], [360, 'ev_pause'], [660, 'ev_pause'], [720, 'auth'], [781, 'plug'],
     [791, 'auth'], [1091, 'ev_pause'], [1391, 'auth'], [1691, 'unplug']],
    90],
   ['Available', 300, 0, 1]],
  ['regression: abrupt unplug classification (partial repair)',
   [[[60, 'plug'], [121, 'auth'], [241, 'auth'], [251, 'stop'], [371, 'unplug'], [376, 'ev_pause'],
     [406, 'unplug']],
    90],
   ['Available', 130, 0, 0]],
  ['control 1', [[[0, 'unplug'], [10, 'ev_pause'], [10, 'plug']], 60], ['Preparing', 0, 0, 0]],
  ['control 2', [[[300, 'plug'], [360, 'ev_pause'], [421, 'ev_pause']], 30],
   ['Available', 0, 1, 0]]],
 [['boundary: authorised exactly at timeout', [[[0, 'plug'], [60, 'auth'], [100, 'stop']], 60],
   ['Finishing', 40, 0, 0]],
  ['normal: pause and resume',
   [[[0, 'plug'], [5, 'auth'], [65, 'ev_pause'], [125, 'ev_resume'], [185, 'stop'],
     [190, 'unplug']],
    60],
   ['Available', 120, 0, 0]],
  ['boundary: unplug while charging', [[[0, 'plug'], [10, 'auth'], [70, 'unplug']], 60],
   ['Available', 60, 0, 1]],
  ['regression: abrupt unplug classification',
   [[[30, 'plug'], [60, 'auth'], [120, 'ev_resume'], [180, 'ev_resume'], [185, 'plug'],
     [190, 'plug'], [250, 'ev_pause'], [370, 'unplug']],
    30],
   ['Available', 190, 0, 1]],
  ['regression: abrupt unplug classification (partial repair)',
   [[[61, 'plug'], [91, 'auth'], [101, 'stop'], [401, 'ev_resume'], [701, 'plug'], [701, 'plug'],
     [706, 'unplug'], [706, 'ev_pause'], [736, 'auth']],
    90],
   ['Available', 10, 0, 0]],
  ['control 1',
   [[[61, 'auth'], [91, 'unplug'], [211, 'unplug'], [221, 'ev_resume'], [281, 'plug'],
     [311, 'ev_pause'], [321, 'unplug'], [326, 'plug']],
    30],
   ['Preparing', 0, 1, 0]],
  ['control 2',
   [[[300, 'plug'], [600, 'ev_resume'], [900, 'ev_resume'], [961, 'unplug'], [966, 'auth'],
     [971, 'unplug'], [1271, 'auth'], [1276, 'ev_pause']],
    60],
   ['Available', 0, 1, 0]]],
 [['boundary: authorised exactly at timeout', [[[0, 'plug'], [60, 'auth'], [100, 'stop']], 60],
   ['Finishing', 40, 0, 0]],
  ['normal: pause and resume',
   [[[0, 'plug'], [5, 'auth'], [65, 'ev_pause'], [125, 'ev_resume'], [185, 'stop'],
     [190, 'unplug']],
    60],
   ['Available', 120, 0, 0]],
  ['boundary: unplug while charging', [[[0, 'plug'], [10, 'auth'], [70, 'unplug']], 60],
   ['Available', 60, 0, 1]],
  ['regression: abrupt unplug classification',
   [[[10, 'plug'], [130, 'auth'], [250, 'plug'], [250, 'auth'], [250, 'ev_pause'], [550, 'unplug'],
     [555, 'auth'], [565, 'auth']],
    60],
   ['Available', 0, 1, 1]],
  ['regression: abrupt unplug classification (partial repair)',
   [[[30, 'plug'], [150, 'stop'], [211, 'plug'], [272, 'auth'], [392, 'stop'], [453, 'plug'],
     [753, 'unplug']],
    90],
   ['Available', 120, 1, 0]],
  ['control 1', [[[61, 'unplug'], [361, 'auth'], [421, 'unplug'], [426, 'ev_pause']], 60],
   ['Available', 0, 0, 0]],
  ['control 2',
   [[[5, 'ev_resume'], [305, 'unplug'], [605, 'plug'], [665, 'ev_resume'], [670, 'ev_resume']], 60],
   ['Available', 0, 1, 0]]]]
for label, args, expected in fixtures[N-1]:
    check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
boundary: authorised exactly at timeout['Finishing', 40, 0, 0]['Finishing', 40, 0, 0]Passed
normal: pause and resume['Available', 120, 0, 0]['Available', 120, 0, 0]Passed
boundary: unplug while charging['Available', 60, 0, 1]['Available', 60, 0, 1]Passed
regression: abrupt unplug classification['Available', 300, 0, 1]['Available', 300, 0, 1]Passed
regression: abrupt unplug classification (partial repair)['Available', 61, 2, 0]['Available', 61, 2, 0]Passed
control 1['Available', 0, 1, 0]['Available', 0, 1, 0]Passed
control 2['Available', 0, 0, 0]['Available', 0, 0, 0]Passed

SHA-256 / 22ae23738dbb06c2e846ce19706509b785eeefeab78cafdc1b1c78e4ed3f0a93

Verification & scope

Deterministic stipulated toy contract for teaching; no claim of conformance with any standard, vendor protocol or production controller. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.

Observations recorded using Python 3.12.14 at 2026-09-29T14:51:52.987400+00:00.

Case digest / 753284df03c6a4e7569d2a89827a2ac9c7f4874392efb50de68004d27231036d