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