import pytest from agent_todos.models import Priority, Status, Todo from agent_todos.store import Store from agent_todos.timebox import BoxState, TimeBox def _store(tmp_path, name="tb.db"): return Store(db_path=str(tmp_path / name)) # --- schema / round-trip ------------------------------------------------- def test_schema_created_on_init(tmp_path): store = _store(tmp_path) box = store.create_timebox( TimeBox(id=None, todo_id=None, duration_seconds=1500, state=BoxState.open) ) assert box.id is not None def test_create_with_todo_link(tmp_path): store = _store(tmp_path) todo = store.create(Todo(id=None, title="focus task", priority=Priority.high)) box = store.create_timebox( TimeBox(id=None, todo_id=todo.id, duration_seconds=900) ) fetched = store.get_timebox(box.id) assert fetched.todo_id == todo.id assert fetched.duration_seconds == 900 assert fetched.state == BoxState.open def test_persists_interruption_and_reason(tmp_path): store = _store(tmp_path) box = store.create_timebox( TimeBox( id=None, todo_id=None, duration_seconds=600, state=BoxState.interrupted, reason="phone rang", interruptions=[{"at": "2026-01-01T00:00:00Z", "reason": "phone rang"}], ) ) fetched = store.get_timebox(box.id) assert fetched.reason == "phone rang" assert fetched.interruptions == [{"at": "2026-01-01T00:00:00Z", "reason": "phone rang"}] def test_list_active_excludes_terminal(tmp_path): store = _store(tmp_path) store.create_timebox(TimeBox(id=None, todo_id=None, duration_seconds=600, state=BoxState.open)) store.create_timebox(TimeBox(id=None, todo_id=None, duration_seconds=600, state=BoxState.running)) store.create_timebox(TimeBox(id=None, todo_id=None, duration_seconds=600, state=BoxState.completed)) store.create_timebox(TimeBox(id=None, todo_id=None, duration_seconds=600, state=BoxState.abandoned)) active = store.list_timeboxes(active_only=True) states = {b.state for b in active} assert BoxState.completed not in states assert BoxState.abandoned not in states assert BoxState.expired not in states assert states <= {BoxState.open, BoxState.running, BoxState.paused, BoxState.interrupted} # --- recovery ------------------------------------------------------------ def test_recover_active_box_returns_most_recent(tmp_path): store = _store(tmp_path) older = store.create_timebox( TimeBox(id=None, todo_id=None, duration_seconds=600, state=BoxState.paused) ) newer = store.create_timebox( TimeBox(id=None, todo_id=None, duration_seconds=600, state=BoxState.interrupted, reason="got pulled into a call") ) recovered = store.recover_timebox() assert recovered is not None # most-recent by updated_at assert recovered.id in {older.id, newer.id} assert recovered.has_active_state() def test_recover_returns_none_when_all_terminal(tmp_path): store = _store(tmp_path) store.create_timebox(TimeBox(id=None, todo_id=None, duration_seconds=600, state=BoxState.completed)) store.create_timebox(TimeBox(id=None, todo_id=None, duration_seconds=600, state=BoxState.abandoned)) assert store.recover_timebox() is None def test_recover_returns_none_on_empty_store(tmp_path): store = _store(tmp_path) assert store.recover_timebox() is None def test_get_nonexistent_returns_none(tmp_path): store = _store(tmp_path) assert store.get_timebox(9999) is None # --- update -------------------------------------------------------------- def test_update_state_sets_timestamp(tmp_path): store = _store(tmp_path) box = store.create_timebox(TimeBox(id=None, todo_id=None, duration_seconds=600)) updated = store.update_timebox(box.id, state=BoxState.running) assert updated.state == BoxState.running assert updated.updated_at >= box.updated_at def test_update_unknown_returns_none(tmp_path): store = _store(tmp_path) assert store.update_timebox(9999, state=BoxState.completed) is None # --- relationship with todos -------------------------------------------- def test_create_timebox_without_todo_is_allowed(tmp_path): store = _store(tmp_path) box = store.create_timebox(TimeBox(id=None, todo_id=None, duration_seconds=300)) assert box.id is not None fetched = store.get_timebox(box.id) assert fetched.todo_id is None