"""Tests for the ADHD-friendly live focus REPL (issue #16).""" import pytest from agent_todos import cli_repl from agent_todos.box import BoxController from agent_todos.store import Store from agent_todos.timebox import BoxState, TimeBox def _store(tmp_path, name="r.db"): return Store(db_path=str(tmp_path / name)) def _box(duration=1500): return TimeBox(id=None, todo_id=None, duration_seconds=duration) def _seq(inputs): """Return a callable that yields items from `inputs`, then forever returns the last one.""" it = iter(inputs) state = {"val": None, "done": False} def _next(timeout): if state["done"]: return state["val"] try: state["val"] = next(it) return state["val"] except StopIteration: state["done"] = True return state["val"] return _next def test_repl_shows_help_and_headline_on_enter(tmp_path): store = _store(tmp_path) box = store.create_timebox(_box()) outputs = [] final = cli_repl.run_repl( store, box, wait_input=_seq(["c"]), display=outputs.append, tick_interval=0, ) assert final.state == BoxState.completed assert any("25:00" in o for o in outputs) assert any("p" in o and "pause" in o for o in outputs) # help lists commands def test_repl_auto_starts_fresh_box(tmp_path): store = _store(tmp_path) box = store.create_timebox(_box(duration=1500)) assert box.state == BoxState.open outputs = [] final = cli_repl.run_repl( store, box, wait_input=_seq(["c"]), display=outputs.append, tick_interval=0, ) assert final.state == BoxState.completed assert any("focusing" in o for o in outputs) def test_repl_auto_resumes_paused_box(tmp_path): store = _store(tmp_path) box = store.create_timebox(_box()) ctrl = BoxController(store) box = ctrl.start(box) box = ctrl.pause(box) assert box.state == BoxState.paused outputs = [] final = cli_repl.run_repl( store, box, wait_input=_seq(["c"]), display=outputs.append, tick_interval=0, ) # should have resumed, then completed assert final.state == BoxState.completed assert any("focusing" in o for o in outputs) def test_repl_pause_and_resume(tmp_path): store = _store(tmp_path) box = store.create_timebox(_box()) outputs = [] final = cli_repl.run_repl( store, box, wait_input=_seq(["p", "r", "c"]), display=outputs.append, tick_interval=0, ) assert final.state == BoxState.completed assert any("paused" in o.lower() for o in outputs) assert any("resumed" in o.lower() or "focusing" in o for o in outputs) def test_repl_interrupt_records_reason(tmp_path): store = _store(tmp_path) box = store.create_timebox(_box()) outputs = [] final = cli_repl.run_repl( store, box, wait_input=_seq(["i phone call", "c"]), display=outputs.append, tick_interval=0, ) assert final.state == BoxState.completed assert final.interruptions, "interruptions should be recorded" assert final.interruptions[-1]["reason"] == "phone call" def test_repl_interrupt_without_reason_prompts(tmp_path): store = _store(tmp_path) box = store.create_timebox(_box()) outputs = [] final = cli_repl.run_repl( store, box, wait_input=_seq(["i", "c"]), display=outputs.append, tick_interval=0, ) assert final.state == BoxState.completed assert any("needs a reason" in o for o in outputs) def test_repl_quit_abandons_with_no_judgment(tmp_path): store = _store(tmp_path) box = store.create_timebox(_box()) outputs = [] final = cli_repl.run_repl( store, box, wait_input=_seq(["q"]), display=outputs.append, tick_interval=0, ) assert final.state == BoxState.abandoned ok_phrase = "it's okay" in " ".join(outputs).lower() or "no judgment" in " ".join(outputs).lower() or "come back" in " ".join(outputs).lower() assert ok_phrase def test_repl_auto_expires_when_timer_runs_out(tmp_path): store = _store(tmp_path) box = store.create_timebox(_box(duration=3)) ctrl = BoxController(store) box = ctrl.start(box) # Force elapsed time far in the past so remaining == 0. store.update_timebox(box.id, last_started_at="2000-01-01T00:00:00Z") outputs = [] final = cli_repl.run_repl( store, box, wait_input=_seq([None, None]), # never type anything; should auto-expire display=outputs.append, tick_interval=0, ) assert final.state == BoxState.expired assert any("timer ran out" in o.lower() or "expired" in o.lower() for o in outputs) def test_repl_unknown_command_shows_error_and_help_hint(tmp_path): store = _store(tmp_path) box = store.create_timebox(_box()) outputs = [] final = cli_repl.run_repl( store, box, wait_input=_seq(["xyz", "c"]), display=outputs.append, tick_interval=0, ) assert final.state == BoxState.completed assert any("unknown command" in o for o in outputs) assert any("h" in o and "help" in o for o in outputs[-5:]) # hint to press h def test_repl_help_command(tmp_path): store = _store(tmp_path) box = store.create_timebox(_box()) outputs = [] final = cli_repl.run_repl( store, box, wait_input=_seq(["h", "c"]), display=outputs.append, tick_interval=0, ) assert final.state == BoxState.completed # help text appears at least twice (on enter + on "h") help_lines = [o for o in outputs if "commands" in o.lower() and "pause" in o.lower()] assert len(help_lines) >= 2 def test_repl_refuses_pause_when_already_paused(tmp_path): store = _store(tmp_path) box = store.create_timebox(_box()) outputs = [] final = cli_repl.run_repl( store, box, wait_input=_seq(["p", "p", "c"]), # pause, then pause again (should error), then complete display=outputs.append, tick_interval=0, ) assert final.state == BoxState.completed assert any("can't do that" in o for o in outputs) from agent_todos.models import Priority, Todo def test_repl_preserves_data_integrity(tmp_path): """Existing TODO data must be untouched by the focus session.""" store = _store(tmp_path) store.create(Todo(id=None, title="my task", priority=Priority.high)) todo_count_before = len(store.list()) box = store.create_timebox(_box()) cli_repl.run_repl( store, box, wait_input=_seq(["c"]), display=lambda _: None, tick_interval=0, ) assert len(store.list()) == todo_count_before