"""CLI tests for the `box` command group (issue #16).""" from click.testing import CliRunner from agent_todos.cli import main def _runner(tmp_path, db="todos.db"): return (CliRunner(), str(tmp_path / db)) def test_box_help_lists_commands(tmp_path): runner, _ = _runner(tmp_path) result = runner.invoke(main, ["box", "--help"]) assert result.exit_code == 0 for cmd in ["start", "status", "pause", "resume", "interrupt", "complete", "abandon", "recover", "list"]: assert cmd in result.output, f"{cmd} missing from box --help:\n{result.output}" def test_start_default_25m(tmp_path): runner, db = _runner(tmp_path) result = runner.invoke(main, ["box", "start", "--path", db]) assert result.exit_code == 0 assert "25:00" in result.output assert "started box" in result.output def test_start_custom_minutes(tmp_path): runner, db = _runner(tmp_path) result = runner.invoke(main, ["box", "start", "10", "--path", db]) assert result.exit_code == 0 assert "10:00" in result.output def test_start_with_todo_link(tmp_path): runner, db = _runner(tmp_path) runner.invoke(main, ["create", "deep work", "--path", db]) result = runner.invoke(main, ["box", "start", "5", "--todo", "1", "--path", db]) assert result.exit_code == 0 assert "started" in result.output def test_start_rejects_out_of_range(tmp_path): runner, db = _runner(tmp_path) result = runner.invoke(main, ["box", "start", "500", "--path", db]) assert result.exit_code != 0 assert "0.5 and 240" in result.output def test_status_before_any_box_is_friendly(tmp_path): runner, db = _runner(tmp_path) result = runner.invoke(main, ["box", "status", "--path", db]) assert result.exit_code != 0 assert "no active time-box" in result.output def test_full_lifecycle_open_running_paused_completed(tmp_path): runner, db = _runner(tmp_path) r = runner.invoke(main, ["box", "start", "5", "--path", db]) assert r.exit_code == 0 box_id = r.output.split("#")[1].split(" ")[0] r = runner.invoke(main, ["box", "status", "--path", db]) assert r.exit_code == 0 assert "focusing" in r.output r = runner.invoke(main, ["box", "pause", "--path", db]) assert r.exit_code == 0 assert "paused" in r.output r = runner.invoke(main, ["box", "resume", "--path", db]) assert r.exit_code == 0 assert "resumed" in r.output r = runner.invoke(main, ["box", "complete", "--reason", "made progress", "--path", db]) assert r.exit_code == 0 assert "completed" in r.output assert "made progress" in r.output # after completion, status should be terminal-friendly r = runner.invoke(main, ["box", "status", "--path", db]) assert r.exit_code != 0 # terminal box => active_box helper raises assert "no active time-box" in r.output def test_interrupt_and_resume(tmp_path): runner, db = _runner(tmp_path) runner.invoke(main, ["box", "start", "5", "--path", db]) r = runner.invoke(main, ["box", "interrupt", "meeting popped up", "--path", db]) assert r.exit_code == 0 assert "meeting popped up" in r.output r = runner.invoke(main, ["box", "status", "--path", db]) assert r.exit_code == 0 assert "interrupted" in r.output r = runner.invoke(main, ["box", "resume", "--path", db]) assert r.exit_code == 0 assert "resumed" in r.output def test_abandon_is_nonjudgmental(tmp_path): runner, db = _runner(tmp_path) runner.invoke(main, ["box", "start", "5", "--path", db]) r = runner.invoke(main, ["box", "abandon", "--reason", "tired", "--path", db]) assert r.exit_code == 0 assert "it's okay" in r.output.lower() or "no judgment" in r.output.lower() def test_pause_without_active_box_fails(tmp_path): runner, db = _runner(tmp_path) r = runner.invoke(main, ["box", "pause", "--path", db]) assert r.exit_code != 0 assert "no active time-box" in r.output def test_recover_after_restart(tmp_path): runner, db = _runner(tmp_path) runner.invoke(main, ["box", "start", "5", "--path", db]) r = runner.invoke(main, ["box", "interrupt", "power blip", "--path", db]) assert r.exit_code == 0 # simulate a fresh CLI invocation by using a new CliRunner on same db r = runner.invoke(main, ["box", "recover", "--path", db]) assert r.exit_code == 0 assert "recovered box" in r.output assert "power blip" in r.output def test_recover_nothing_returns_helpful_message(tmp_path): runner, db = _runner(tmp_path) r = runner.invoke(main, ["box", "recover", "--path", db]) assert r.exit_code == 0 assert "nothing to recover" in r.output def test_list_empty_is_friendly(tmp_path): runner, db = _runner(tmp_path) r = runner.invoke(main, ["box", "list", "--path", db]) assert r.exit_code == 0 assert "no boxes yet" in r.output def test_list_shows_active_boxes(tmp_path): runner, db = _runner(tmp_path) runner.invoke(main, ["box", "start", "5", "--path", db]) r = runner.invoke(main, ["box", "list", "--path", db]) assert r.exit_code == 0 assert "[open]" not in r.output # box started, so it's running # should still show the running box assert "running" in r.output def test_pause_while_not_running_rejected(tmp_path): runner, db = _runner(tmp_path) runner.invoke(main, ["box", "start", "5", "--path", db]) runner.invoke(main, ["box", "pause", "--path", db]) r = runner.invoke(main, ["box", "pause", "--path", db]) assert r.exit_code != 0 assert "Cannot pause" in r.output def test_complete_twice_fails_cleanly(tmp_path): runner, db = _runner(tmp_path) runner.invoke(main, ["box", "start", "5", "--path", db]) runner.invoke(main, ["box", "complete", "--path", db]) r = runner.invoke(main, ["box", "complete", "--path", db]) assert r.exit_code != 0 # no active box