- 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
169 lines
5.8 KiB
Python
169 lines
5.8 KiB
Python
"""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
|