Files

177 lines
6.8 KiB
Python

"""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