diff --git a/tests/test_palier11.py b/tests/test_palier11.py new file mode 100644 index 0000000..b1237c9 --- /dev/null +++ b/tests/test_palier11.py @@ -0,0 +1,159 @@ +"""Palier 11 (0.14.0): branche depuis la carte (#3), santé du flux (#9).""" + +from datetime import datetime, timedelta + +from app import code_links as cl +from app import flow +from app import models +from tests.conftest import csrf_from + + +# ── #3 Branche depuis la carte ──────────────────────────────────── + + +def test_slugify_and_branch_name(): + assert cl.slugify_title("Créer la branche depuis la carte") == \ + "creer-la-branche-depuis-la-carte" + assert cl.slugify_title("Fix: l'été indien (urgent!)") == \ + "fix-l-ete-indien-urgent" + assert cl.slugify_title("Un titre vraiment très long qui dépasse" * 3) + assert len(cl.slugify_title("x" * 100)) == 40 + assert cl.slugify_title("!!!") == "issue" + assert cl.branch_name_for(12, "Capture éclair") == "issue-12-capture-eclair" + # La convention est reconnue par l'appariement (#1/#2). + assert cl.branch_issue_number(cl.branch_name_for(12, "Capture")) == 12 + + +def test_create_branch_endpoint(client, fake_gitea): + token = csrf_from(client) + r = client.post("/api/issues/Projets/demo/5/branch", + data={"csrf_token": token}) + assert r.status_code == 200 + data = r.json() + assert data["branch"] == "issue-5-tache-focus" + assert data["from"] == "main" + assert data["moved"] is True + assert "issue-5-tache-focus" in fake_gitea.branches + board = models.get_board_state("Projets/demo") + assert 5 in [c["issue_number"] for c in board["in_progress"]] + events = models.get_events(repo_full_name="Projets/demo") + assert any( + e["event_type"] == "moved" and e["source"] == "branch" + for e in events + ) + # Deuxième appel : la branche existe déjà, pas de doublon. + r2 = client.post("/api/issues/Projets/demo/5/branch", + data={"csrf_token": token}) + assert r2.json()["exists"] is True + + +def test_create_branch_custom_name_and_validation(client, fake_gitea): + token = csrf_from(client) + r = client.post("/api/issues/Projets/demo/1/branch", + data={"csrf_token": token, "name": "feature/super-truc"}) + assert r.status_code == 200 + assert r.json()["branch"] == "feature/super-truc" + r = client.post("/api/issues/Projets/demo/1/branch", + data={"csrf_token": token, "name": "mauvais nom.."}) + assert r.status_code == 400 + + +def test_code_endpoint_suggests_branch(client, fake_gitea): + r = client.get("/api/issues/Projets/demo/3/code") + assert r.json()["suggested_branch"] == "issue-3-ancienne-tache" + + +# ── #9 Santé du flux : fonctions pures ──────────────────────────── + + +def _ev(day_offset: float, kind: str, frm=None, to=None, number=1): + at = (datetime(2026, 9, 1) + timedelta(days=day_offset)).strftime( + "%Y-%m-%d %H:%M:%S" + ) + return { + "repo_full_name": "Projets/demo", "issue_number": number, + "event_type": kind, "from_column": frm, "to_column": to, + "created_at": at, + } + + +def test_column_dwell_reconstruction(): + events = [ + _ev(0, "created", to="backlog"), + _ev(4, "moved", frm="backlog", to="in_progress"), + _ev(10, "closed", frm="in_progress", to="done"), + _ev(1, "created", to="backlog", number=2), + _ev(3, "moved", frm="backlog", to="review", number=2), + ] + dwell = flow.column_dwell(events) + assert dwell["backlog"] == [4.0, 2.0] + assert dwell["in_progress"] == [6.0] + summary = flow.summarize_dwell(dwell) + by_col = {row["column"]: row for row in summary} + assert by_col["backlog"]["median_days"] == 3.0 + assert by_col["backlog"]["count"] == 2 + # backlog a 2 échantillons (médiane 3 j) : c'est le seul goulot + # nommable à ce stade (in_progress n'a qu'un séjour mesuré). + slowest = flow.slowest_column(summary) + assert slowest["column"] == "backlog" + events.append(_ev(20, "moved", frm="review", to="done", number=2)) + events += [ + _ev(0, "created", to="backlog", number=3), + _ev(2, "moved", frm="backlog", to="review", number=3), + _ev(19, "moved", frm="review", to="done", number=3), + ] + dwell = flow.column_dwell(events) + summary = flow.summarize_dwell(dwell) + slowest = flow.slowest_column(summary) + assert slowest["column"] == "review" + assert slowest["median_days"] == 17.0 + + +def test_last_activity(): + events = [_ev(0, "created", to="backlog"), _ev(5, "moved", to="todo")] + latest = flow.last_activity(events) + assert latest[("Projets/demo", 1)] == datetime(2026, 9, 6) + + +# ── #9 Pages santé ──────────────────────────────────────────────── + + +def _seed_stay(number: int, days_ago_start: int, days_ago_end: int, + frm="backlog", to="review"): + models.log_event("Projets/demo", number, "created", to_column="backlog") + models.log_event("Projets/demo", number, "moved", + from_column=frm, to_column=to) + now = datetime.now() + with models.get_db() as conn: + conn.execute( + "UPDATE board_events SET created_at = ? " + "WHERE issue_number = ? AND event_type = 'created'", + ((now - timedelta(days=days_ago_start)).strftime("%Y-%m-%d %H:%M:%S"), number), + ) + conn.execute( + "UPDATE board_events SET created_at = ? " + "WHERE issue_number = ? AND event_type = 'moved'", + ((now - timedelta(days=days_ago_end)).strftime("%Y-%m-%d %H:%M:%S"), number), + ) + + +def test_health_pages_and_bottleneck(client, fake_gitea): + _seed_stay(1, 40, 10) # 30 j en backlog + _seed_stay(5, 35, 5) # 30 j en backlog aussi + page = client.get("/health/Projets/demo") + assert page.status_code == 200 + assert "backlog" in page.text.lower() + index = client.get("/health") + assert index.status_code == 200 + assert "Projets/demo" in index.text + + +def test_health_zombies_threshold(client, fake_gitea, monkeypatch): + # Les issues du faux datent du 2026-09-02 : ~39 j d'inactivité, + # donc zombies au seuil par défaut de 30 j. + page = client.get("/health/Projets/demo") + assert "Tâche un" in page.text + assert 'data-zombie="close"' in page.text + monkeypatch.setenv("KANBANTE_ZOMBIE_DAYS", "10000") + page = client.get("/health/Projets/demo") + assert "Aucune carte zombie" in page.text