From 8e0c7b8f09ae852541a1e2c6ddfd1eb0ea59e73c Mon Sep 17 00:00:00 2001 From: Spark Date: Sun, 11 Oct 2026 13:55:03 -0400 Subject: [PATCH] =?UTF-8?q?test(0.12.0):=20pr=C3=A9visions=20(fonctions=20?= =?UTF-8?q?pures=20+=20pages),=20focus=20du=20jour,=20r=C3=A9cit=20?= =?UTF-8?q?=E2=80=94=2011=20tests,=20147=20au=20total?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_palier8.py | 2 +- tests/test_palier9.py | 176 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 tests/test_palier9.py diff --git a/tests/test_palier8.py b/tests/test_palier8.py index 5cd1f0d..ec26373 100644 --- a/tests/test_palier8.py +++ b/tests/test_palier8.py @@ -1,6 +1,6 @@ """Palier 8 (0.11.0): navigation manuelle du Wallboard.""" -from datetime import datetime, timedelta, timezone +from datetime import datetime, timezone def _make_two_active_projects(fake_gitea): diff --git a/tests/test_palier9.py b/tests/test_palier9.py new file mode 100644 index 0000000..ef3d19c --- /dev/null +++ b/tests/test_palier9.py @@ -0,0 +1,176 @@ +"""Palier 9 (0.12.0): Focus du jour (#5), Prévisions (#7), Récit (#4).""" + +from datetime import date, timedelta + +from app import forecast as fc +from app import models +from tests.conftest import csrf_from + + +def _ev(day: date, kind: str = "closed") -> dict: + return {"event_type": kind, "created_at": day.strftime("%Y-%m-%d 10:00:00")} + + +# ── Prévisions : fonctions pures ────────────────────────────────── + + +def test_weekly_closures_buckets(): + today = date(2026, 10, 14) # a Wednesday + monday = today - timedelta(days=2) + events = [ + _ev(monday), _ev(monday + timedelta(days=1)), # this week: 2 + _ev(monday - timedelta(days=7)), # last week: 1 + _ev(monday - timedelta(days=30), "moved"), # not a closure + _ev(monday - timedelta(days=90)), # too old + ] + weekly = fc.weekly_closures(events, weeks=8, today=today) + assert len(weekly) == 8 + assert weekly[-1]["count"] == 2 + assert weekly[-2]["count"] == 1 + assert sum(b["count"] for b in weekly) == 3 + + +def test_throughput_excludes_current_week_and_needs_history(): + today = date(2026, 10, 14) + monday = today - timedelta(days=2) + events = [] + # 4 complete weeks: 2, 4, 0, 6 closures; current week: 99 (ignored) + for weeks_ago, n in ((1, 2), (2, 4), (3, 0), (4, 6)): + for k in range(n): + events.append(_ev(monday - timedelta(weeks=weeks_ago) + timedelta(days=k % 5))) + for k in range(9): + events.append(_ev(monday + timedelta(days=k % 2))) + weekly = fc.weekly_closures(events, weeks=8, today=today) + stats = fc.throughput_stats(weekly) + assert stats == {"avg": 3.0, "min": 0, "max": 6, "weeks": 4} + assert fc.throughput_stats(fc.weekly_closures([], today=today)) is None + + +def test_estimate_dates_and_labels(): + today = date(2026, 10, 14) + monday = today - timedelta(days=2) + events = [_ev(monday - timedelta(weeks=w)) for w in (1, 2, 3, 4) for _ in range(2)] + weekly = fc.weekly_closures(events, weeks=8, today=today) + est = fc.estimate(6, weekly, today=today) + assert est["done"] is False + assert est["avg_per_week"] == 2.0 + assert est["central"] == today + timedelta(days=21) + # min == max == 2/week -> no spread: latest==earliest==central + assert est["earliest"] == est["central"] == est["latest"] + label = fc.eta_label(est) + assert "à ce rythme ≈" in label + assert fc.estimate(0, weekly, today=today) == {"done": True} + assert fc.estimate(5, [], today=today) is None + assert fc.french_date(date(2026, 11, 3)) == "3 novembre" + + +def test_metrics_include_weekly_closures(client, fake_gitea): + models.log_event("Projets/demo", 3, "closed", to_column="done") + r = client.get("/api/metrics", params={"repo": "Projets/demo"}) + assert r.status_code == 200 + weekly = r.json()["metrics"]["weekly_closures"] + assert len(weekly) == 8 + assert weekly[-1] == 1 # the closure we just logged is this week + + +def test_forecast_page_and_milestone_banner(client, fake_gitea): + r = client.get("/forecast") + assert r.status_code == 200 + assert "Prévisions" in r.text + assert "Projets/demo" in r.text + assert "Sprint 1" in r.text + board = client.get("/board/Projets/demo", params={"milestone": "Sprint 1"}) + assert board.status_code == 200 + assert "🔮" in board.text + assert "historique pour prévoir" in board.text # empty journal + assert "/sprint/Projets/demo" in board.text + + +# ── Focus du jour ───────────────────────────────────────────────── + + +def _pin(client, token, number, repo="Projets/demo"): + return client.post("/api/today/pin", data={ + "repo_full_name": repo, "issue_number": number, "csrf_token": token, + }) + + +def test_today_pin_unpin_flow(client, fake_gitea): + token = csrf_from(client) + r = _pin(client, token, 1) + assert r.status_code == 200 and r.json()["count"] == 1 + r = _pin(client, token, 1) # idempotent + assert r.json().get("already") is True + page = client.get("/today") + assert page.status_code == 200 + assert "Tâche un" in page.text + r = client.post("/api/today/unpin", data={ + "repo_full_name": "Projets/demo", "issue_number": 1, "csrf_token": token, + }) + assert r.status_code == 200 + assert models.get_picks(models_now()) == [] + + +def models_now() -> str: + from datetime import datetime + return datetime.now().strftime("%Y-%m-%d") + + +def test_today_pin_cap_three(client, fake_gitea): + token = csrf_from(client) + today = models_now() + for n in (11, 12, 13): + models.add_pick(today, "Projets/demo", n) + r = _pin(client, token, 1) + assert r.status_code == 400 + assert "3 cartes max" in r.json()["error"] + + +def test_briefing_includes_today_picks(client, fake_gitea): + models.add_pick(models_now(), "Projets/demo", 5) + r = client.get("/api/briefing") + assert r.status_code == 200 + picks = r.json()["briefing"]["today_picks"] + assert [p["number"] for p in picks] == [5] + assert picks[0]["title"] == "Tâche focus" + + +def test_today_page_shows_candidates(client, fake_gitea): + token = csrf_from(client) + r = client.post("/api/move", data={ + "repo_full_name": "Projets/demo", "issue_number": 1, + "from_column": "backlog", "to_column": "in_progress", + "to_order": "1", "csrf_token": token, + }) + assert r.status_code == 200 + page = client.get("/today") + assert page.status_code == 200 + assert "Candidates" in page.text + assert "Tâche un" in page.text + + +# ── Récit de sprint ─────────────────────────────────────────────── + + +def test_sprint_markdown_from_fake_milestone(client, fake_gitea): + models.log_event("Projets/demo", 3, "moved", to_column="in_progress") + models.log_event("Projets/demo", 3, "closed", to_column="done") + r = client.get("/api/sprint/Projets/demo", params={"milestone": "Sprint 1"}) + assert r.status_code == 200 + md = r.json()["markdown"] + assert "# 📖 Récit de sprint — Projets/demo · Sprint 1" in md + assert "## ✅ Livré (1)" in md + assert "Ancienne tâche" in md + assert "## ↩️ Reporté (1)" in md + assert "Tâche un" in md + page = client.get("/sprint/Projets/demo", params={"milestone": "Sprint 1"}) + assert page.status_code == 200 + assert "Copier le markdown" in page.text + + +def test_sprint_unknown_milestone_404(client, fake_gitea): + r = client.get("/api/sprint/Projets/demo", params={"milestone": "Nope"}) + assert r.status_code == 404 + page = client.get("/sprint/Projets/demo") + assert page.status_code == 200 + assert "Sprint 1" in page.text # picker lists the milestones