"""Metrics, achievements and the rendered report.""" from __future__ import annotations from datetime import timedelta from app.models import utcnow from app.services import pomodoro, report, settings_service, stats, tasks_service from app.timeutil import local_date def _close(session, settings, task) -> None: """Simulate what a refresh does when a task has been closed in its own tool.""" task.is_done = True task.completed_at = utcnow() task.completed_date = local_date(settings.timezone) session.commit() def _run_pomodoros(session, settings, count: int, *, minutes: int = 25) -> None: """Record `count` finished work intervals, backdated so they do not overlap.""" for index in range(count): row = pomodoro.start(session, settings, kind="work", minutes=minutes) start = utcnow() - timedelta(minutes=(count - index) * (minutes + 5)) row.started_at = start row.ended_at = start + timedelta(minutes=minutes) row.actual_seconds = minutes * 60 row.status = "completed" session.commit() def test_day_metrics_counts_what_happened(session, settings): _run_pomodoros(session, settings, 3) metrics = stats.day_metrics(session, settings, local_date(settings.timezone)) assert metrics["pomodoros"] == 3 assert metrics["focus_minutes"] == 75 assert metrics["goal"] == settings.pomodoro.daily_goal_pomodoros assert metrics["first_start"] and metrics["last_end"] def test_longest_stretch_joins_adjacent_pomodoros(session, settings): _run_pomodoros(session, settings, 3, minutes=25) metrics = stats.day_metrics(session, settings, local_date(settings.timezone)) # 25-minute intervals five minutes apart are one unbroken stretch. assert metrics["longest_stretch_minutes"] >= 75 def test_streak_survives_an_empty_today(session, settings): today = local_date(settings.timezone) _run_pomodoros(session, settings, 1) # Move the work to yesterday and leave today empty. from app.models import PomodoroSession for row in session.query(PomodoroSession).all(): row.local_date = today - timedelta(days=1) session.commit() streak = stats.streak(session, settings, until=today) assert streak["current"] == 1 def test_report_payload_has_the_pieces_the_ui_draws(session, settings): settings.pomodoro.daily_goal_pomodoros = 2 settings_service.save_settings(session, settings) _run_pomodoros(session, settings, 2) task = tasks_service.create_local_task(session, title="Ship the thing") _close(session, settings, task) payload = report.build_daily(session, settings, local_date(settings.timezone)) assert payload["metrics"]["pomodoros"] == 2 assert payload["metrics"]["tasks_completed_count"] == 1 assert any(item["code"] == "goal_met" for item in payload["achievements"]) assert payload["fun_facts"] assert 0 <= payload["score"] <= 100 def test_rendered_report_escapes_borrowed_text(session, settings): task = tasks_service.create_local_task(session, title="Fix & ship") _close(session, settings, task) payload = report.build_daily(session, settings, local_date(settings.timezone)) text = report.render_daily_telegram(payload, settings) assert "Fix <b>&</b> ship" in text def test_russian_report_renders(session, settings): settings.notifications.report.language = "ru" settings.notifications.report.tone = "drill_sergeant" settings_service.save_settings(session, settings) _run_pomodoros(session, settings, 1) payload = report.build_daily(session, settings, local_date(settings.timezone)) text = report.render_daily_telegram(payload, settings) assert "Отчёт за день" in text def test_weekly_review_covers_seven_days(session, settings): _run_pomodoros(session, settings, 2) payload = report.build_weekly(session, settings, local_date(settings.timezone)) assert len(payload["days"]) == 7 assert payload["pomodoros"] == 2 assert "Weekly review" in report.render_weekly_telegram(payload, settings) def test_progress_bar_is_proportional(): assert report.progress_bar(0, 8) == "░" * 10 assert report.progress_bar(8, 8) == "█" * 10 assert report.progress_bar(4, 8).count("█") == 5