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.
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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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