FA-93221 / EV charging session scheduling / Open access
Connector session state machine: resume restarts interval · case 01
Charging time after a resume includes the paused period.
ROOT CAUSE
Resuming does not restart the charging interval clock.
VERIFIED REPAIR
Set since = t on resume.
Unsuccessful approach: Restarting from the plug time counts preparation and pause time again.
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'
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: resume restarts interval',
[[[0, 'unplug'], [5, 'unplug'], [5, 'plug'], [35, 'auth'], [95, 'ev_pause'], [100, 'ev_resume'],
[130, 'plug'], [160, 'unplug'], [160, 'plug'], [160, 'plug']],
60],
['Preparing', 120, 0, 1]],
['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]],
['control 3', [[[0, 'ev_resume'], [10, 'plug'], [70, 'stop'], [100, 'stop']], 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: resume restarts interval',
[[[0, 'unplug'], [5, 'unplug'], [5, 'plug'], [35, 'auth'], [95, 'ev_pause'], [100, 'ev_resume'],
[130, 'plug'], [160, 'unplug'], [160, 'plug'], [160, 'plug']],
60],
['Preparing', 120, 0, 1]],
['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]],
['control 3', [[[61, 'auth'], [66, 'unplug'], [127, 'plug'], [132, 'ev_resume']], 60],
['Preparing', 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: resume restarts interval',
[[[0, 'unplug'], [5, 'unplug'], [5, 'plug'], [35, 'auth'], [95, 'ev_pause'], [100, 'ev_resume'],
[130, 'plug'], [160, 'unplug'], [160, 'plug'], [160, 'plug']],
60],
['Preparing', 120, 0, 1]],
['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]],
['control 3', [[[5, 'auth'], [65, 'unplug'], [75, 'plug'], [75, 'unplug']], 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: resume restarts interval',
[[[0, 'unplug'], [5, 'unplug'], [5, 'plug'], [35, 'auth'], [95, 'ev_pause'], [100, 'ev_resume'],
[130, 'plug'], [160, 'unplug'], [160, 'plug'], [160, 'plug']],
60],
['Preparing', 120, 0, 1]],
['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]],
['control 3', [[[30, 'auth'], [60, 'ev_pause'], [120, 'auth'], [125, '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: resume restarts interval',
[[[0, 'unplug'], [5, 'unplug'], [5, 'plug'], [35, 'auth'], [95, 'ev_pause'], [100, 'ev_resume'],
[130, 'plug'], [160, 'unplug'], [160, 'plug'], [160, 'plug']],
60],
['Preparing', 120, 0, 1]],
['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]],
['control 3',
[[[30, 'plug'], [330, 'ev_resume'], [340, 'unplug'], [340, 'ev_pause'], [350, 'plug'],
[650, 'unplug'], [650, 'plug'], [680, 'plug']],
90],
['Preparing', 0, 2, 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', 240, 0, 0] | ['Available', 120, 0, 0] | Failed |
| boundary: unplug while charging | ['Available', 60, 0, 1] | ['Available', 60, 0, 1] | Passed |
| regression: resume restarts interval | ['Preparing', 185, 0, 1] | ['Preparing', 120, 0, 1] | Failed |
| control 1 | ['Available', 0, 1, 0] | ['Available', 0, 1, 0] | Passed |
| control 2 | ['Available', 0, 0, 0] | ['Available', 0, 0, 0] | Passed |
| control 3 | ['Available', 0, 1, 0] | ['Available', 0, 1, 0] | Passed |
SHA-256 / dc40e07e92fda2da954be11c6be31e09055b390696ee869ae22adb4526ea8592
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 = plug_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: resume restarts interval',
[[[0, 'unplug'], [5, 'unplug'], [5, 'plug'], [35, 'auth'], [95, 'ev_pause'], [100, 'ev_resume'],
[130, 'plug'], [160, 'unplug'], [160, 'plug'], [160, 'plug']],
60],
['Preparing', 120, 0, 1]],
['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]],
['control 3', [[[0, 'ev_resume'], [10, 'plug'], [70, 'stop'], [100, 'stop']], 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: resume restarts interval',
[[[0, 'unplug'], [5, 'unplug'], [5, 'plug'], [35, 'auth'], [95, 'ev_pause'], [100, 'ev_resume'],
[130, 'plug'], [160, 'unplug'], [160, 'plug'], [160, 'plug']],
60],
['Preparing', 120, 0, 1]],
['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]],
['control 3', [[[61, 'auth'], [66, 'unplug'], [127, 'plug'], [132, 'ev_resume']], 60],
['Preparing', 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: resume restarts interval',
[[[0, 'unplug'], [5, 'unplug'], [5, 'plug'], [35, 'auth'], [95, 'ev_pause'], [100, 'ev_resume'],
[130, 'plug'], [160, 'unplug'], [160, 'plug'], [160, 'plug']],
60],
['Preparing', 120, 0, 1]],
['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]],
['control 3', [[[5, 'auth'], [65, 'unplug'], [75, 'plug'], [75, 'unplug']], 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: resume restarts interval',
[[[0, 'unplug'], [5, 'unplug'], [5, 'plug'], [35, 'auth'], [95, 'ev_pause'], [100, 'ev_resume'],
[130, 'plug'], [160, 'unplug'], [160, 'plug'], [160, 'plug']],
60],
['Preparing', 120, 0, 1]],
['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]],
['control 3', [[[30, 'auth'], [60, 'ev_pause'], [120, 'auth'], [125, '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: resume restarts interval',
[[[0, 'unplug'], [5, 'unplug'], [5, 'plug'], [35, 'auth'], [95, 'ev_pause'], [100, 'ev_resume'],
[130, 'plug'], [160, 'unplug'], [160, 'plug'], [160, 'plug']],
60],
['Preparing', 120, 0, 1]],
['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]],
['control 3',
[[[30, 'plug'], [330, 'ev_resume'], [340, 'unplug'], [340, 'ev_pause'], [350, 'plug'],
[650, 'unplug'], [650, 'plug'], [680, 'plug']],
90],
['Preparing', 0, 2, 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', 245, 0, 0] | ['Available', 120, 0, 0] | Failed |
| boundary: unplug while charging | ['Available', 60, 0, 1] | ['Available', 60, 0, 1] | Passed |
| regression: resume restarts interval | ['Preparing', 215, 0, 1] | ['Preparing', 120, 0, 1] | Failed |
| control 1 | ['Available', 0, 1, 0] | ['Available', 0, 1, 0] | Passed |
| control 2 | ['Available', 0, 0, 0] | ['Available', 0, 0, 0] | Passed |
| control 3 | ['Available', 0, 1, 0] | ['Available', 0, 1, 0] | Passed |
SHA-256 / dbff01bf8f41abbdb0584a7615f61e330d9d2a58c30dbaaa38647d78e7b7329a
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: resume restarts interval',
[[[0, 'unplug'], [5, 'unplug'], [5, 'plug'], [35, 'auth'], [95, 'ev_pause'], [100, 'ev_resume'],
[130, 'plug'], [160, 'unplug'], [160, 'plug'], [160, 'plug']],
60],
['Preparing', 120, 0, 1]],
['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]],
['control 3', [[[0, 'ev_resume'], [10, 'plug'], [70, 'stop'], [100, 'stop']], 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: resume restarts interval',
[[[0, 'unplug'], [5, 'unplug'], [5, 'plug'], [35, 'auth'], [95, 'ev_pause'], [100, 'ev_resume'],
[130, 'plug'], [160, 'unplug'], [160, 'plug'], [160, 'plug']],
60],
['Preparing', 120, 0, 1]],
['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]],
['control 3', [[[61, 'auth'], [66, 'unplug'], [127, 'plug'], [132, 'ev_resume']], 60],
['Preparing', 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: resume restarts interval',
[[[0, 'unplug'], [5, 'unplug'], [5, 'plug'], [35, 'auth'], [95, 'ev_pause'], [100, 'ev_resume'],
[130, 'plug'], [160, 'unplug'], [160, 'plug'], [160, 'plug']],
60],
['Preparing', 120, 0, 1]],
['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]],
['control 3', [[[5, 'auth'], [65, 'unplug'], [75, 'plug'], [75, 'unplug']], 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: resume restarts interval',
[[[0, 'unplug'], [5, 'unplug'], [5, 'plug'], [35, 'auth'], [95, 'ev_pause'], [100, 'ev_resume'],
[130, 'plug'], [160, 'unplug'], [160, 'plug'], [160, 'plug']],
60],
['Preparing', 120, 0, 1]],
['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]],
['control 3', [[[30, 'auth'], [60, 'ev_pause'], [120, 'auth'], [125, '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: resume restarts interval',
[[[0, 'unplug'], [5, 'unplug'], [5, 'plug'], [35, 'auth'], [95, 'ev_pause'], [100, 'ev_resume'],
[130, 'plug'], [160, 'unplug'], [160, 'plug'], [160, 'plug']],
60],
['Preparing', 120, 0, 1]],
['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]],
['control 3',
[[[30, 'plug'], [330, 'ev_resume'], [340, 'unplug'], [340, 'ev_pause'], [350, 'plug'],
[650, 'unplug'], [650, 'plug'], [680, 'plug']],
90],
['Preparing', 0, 2, 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: resume restarts interval | ['Preparing', 120, 0, 1] | ['Preparing', 120, 0, 1] | Passed |
| control 1 | ['Available', 0, 1, 0] | ['Available', 0, 1, 0] | Passed |
| control 2 | ['Available', 0, 0, 0] | ['Available', 0, 0, 0] | Passed |
| control 3 | ['Available', 0, 1, 0] | ['Available', 0, 1, 0] | Passed |
SHA-256 / 731000db84248586a15c81adce38b97fb9ccc9e964d5fd7f50a80679bf12ff4c
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:53.117864+00:00.
Case digest / a3b5984ff901067ccd1681ff7c654f225a3da32de3eff9478f94ebfe377bc210