- TimeBox model with explicit recoverable BoxState state machine - Store layer: time_boxes schema, CRUD, active-box recovery - BoxController: start/pause/resume/interrupt/complete/expire with elapsed math - CLI group (start/status/pause/resume/interrupt/complete/abandon/recover/list/focus) - Live ADHD-friendly focus REPL with single-key commands, calm UX, no-shame quit - Interruptions logged with timestamp + reason for pattern recognition - Non-interactive helpers + auto-resume on entry - 65 tests green, strict vertical RED-GREEN TDD
110 lines
4.0 KiB
Python
110 lines
4.0 KiB
Python
import pytest
|
|
|
|
from agent_todos.timebox import BoxState, TimeBox
|
|
|
|
|
|
def test_timebox_default_state_is_open():
|
|
tb = TimeBox(id=None, todo_id=None, duration_seconds=1500)
|
|
assert tb.state == BoxState.open
|
|
assert tb.accumulated_seconds == 0
|
|
assert tb.last_started_at is None
|
|
assert tb.interruptions == []
|
|
|
|
|
|
def test_transitions_from_open():
|
|
tb = TimeBox(id=None, todo_id=None, duration_seconds=600)
|
|
# open can go to running (start) or be abandoned immediately
|
|
assert tb.can(BoxState.running) is True
|
|
assert tb.can(BoxState.abandoned) is True
|
|
assert tb.can(BoxState.completed) is False # nothing was worked on
|
|
assert tb.can(BoxState.expired) is False
|
|
|
|
|
|
def test_transitions_from_running():
|
|
tb = TimeBox(id=None, todo_id=None, duration_seconds=600, state=BoxState.running)
|
|
assert tb.can(BoxState.paused) is True
|
|
assert tb.can(BoxState.interrupted) is True
|
|
assert tb.can(BoxState.completed) is True
|
|
assert tb.can(BoxState.abandoned) is True
|
|
assert tb.can(BoxState.expired) is True
|
|
assert tb.can(BoxState.open) is False # cannot rewind to fresh
|
|
assert tb.can(BoxState.running) is False # no self-loops
|
|
|
|
|
|
def test_transitions_from_paused():
|
|
tb = TimeBox(id=None, todo_id=None, duration_seconds=600, state=BoxState.paused)
|
|
assert tb.can(BoxState.running) is True # resume
|
|
assert tb.can(BoxState.interrupted) is True
|
|
assert tb.can(BoxState.completed) is True
|
|
assert tb.can(BoxState.abandoned) is True
|
|
assert tb.can(BoxState.expired) is True
|
|
assert tb.can(BoxState.open) is False
|
|
|
|
|
|
def test_transitions_from_interrupted():
|
|
# interrupted is paused-with-a-reason: still recoverable to running
|
|
tb = TimeBox(id=None, todo_id=None, duration_seconds=600, state=BoxState.interrupted)
|
|
assert tb.can(BoxState.running) is True
|
|
assert tb.can(BoxState.completed) is True
|
|
assert tb.can(BoxState.abandoned) is True
|
|
assert tb.can(BoxState.expired) is True
|
|
assert tb.can(BoxState.open) is False
|
|
|
|
|
|
def test_terminal_states_accept_no_transitions():
|
|
for terminal in (BoxState.completed, BoxState.abandoned, BoxState.expired):
|
|
tb = TimeBox(id=None, todo_id=None, duration_seconds=600, state=terminal)
|
|
for s in BoxState:
|
|
assert tb.can(s) is False
|
|
|
|
|
|
def test_elapsed_math_paused_uses_accumulated_only():
|
|
tb = TimeBox(
|
|
id=None,
|
|
todo_id=None,
|
|
duration_seconds=600,
|
|
state=BoxState.paused,
|
|
accumulated_seconds=120,
|
|
)
|
|
assert tb.elapsed_seconds() == 120
|
|
assert tb.remaining_seconds() == 480
|
|
|
|
|
|
def test_remaining_is_floor_zero():
|
|
tb = TimeBox(
|
|
id=None,
|
|
todo_id=None,
|
|
duration_seconds=600,
|
|
state=BoxState.paused,
|
|
accumulated_seconds=1200,
|
|
)
|
|
assert tb.remaining_seconds() == 0
|
|
|
|
|
|
def test_percent_progress_clamps_to_100():
|
|
tb = TimeBox(
|
|
id=None,
|
|
todo_id=None,
|
|
duration_seconds=600,
|
|
state=BoxState.paused,
|
|
accumulated_seconds=900,
|
|
)
|
|
assert tb.progress_percent() == 100
|
|
|
|
|
|
def test_duration_must_be_positive():
|
|
with pytest.raises(ValueError):
|
|
TimeBox(id=None, todo_id=None, duration_seconds=0)
|
|
with pytest.raises(ValueError):
|
|
TimeBox(id=None, todo_id=None, duration_seconds=-5)
|
|
|
|
|
|
def test_has_active_nonterminal_state():
|
|
assert TimeBox(id=None, todo_id=None, duration_seconds=600, state=BoxState.open).has_active_state() is True
|
|
assert TimeBox(id=None, todo_id=None, duration_seconds=600, state=BoxState.running).has_active_state() is True
|
|
assert TimeBox(id=None, todo_id=None, duration_seconds=600, state=BoxState.paused).has_active_state() is True
|
|
assert TimeBox(id=None, todo_id=None, duration_seconds=600, state=BoxState.interrupted).has_active_state() is True
|
|
assert TimeBox(id=None, todo_id=None, duration_seconds=600, state=BoxState.completed).has_active_state() is False
|
|
assert TimeBox(id=None, todo_id=None, duration_seconds=600, state=BoxState.abandoned).has_active_state() is False
|
|
assert TimeBox(id=None, todo_id=None, duration_seconds=600, state=BoxState.expired).has_active_state() is False
|