"""Palier 1 (0.3.0): event journal, card detail panel, aging + WIP limits.""" from datetime import datetime, timedelta, timezone from app import models from tests.conftest import csrf_from def _days_ago(days: int) -> str: return (datetime.now(timezone.utc) - timedelta(days=days)).strftime( "%Y-%m-%d %H:%M:%S" ) # ── Journal ──────────────────────────────────────────────── def test_log_event_and_get_events(): models.log_event("a/b", 1, "created", to_column="backlog") models.log_event("a/b", 1, "moved", from_column="backlog", to_column="todo") events = models.get_events("a/b") assert [e["event_type"] for e in events] == ["moved", "created"] assert events[0]["from_column"] == "backlog" def test_log_event_rejects_unknown_type(): import pytest with pytest.raises(ValueError): models.log_event("a/b", 1, "exploded") def test_move_via_api_writes_journal(client): token = csrf_from(client) client.post( "/api/move", data={ "repo_full_name": "Projets/demo", "issue_number": 1, "to_column": "done", "from_column": "backlog", "to_order": "1", "csrf_token": token, }, ) types = [e["event_type"] for e in models.get_events("Projets/demo")] assert types == ["closed", "moved"] # newest first def test_same_column_reorder_does_not_log_move(client): token = csrf_from(client) client.post( "/api/move", data={ "repo_full_name": "Projets/demo", "issue_number": 5, "to_column": "backlog", "from_column": "backlog", "to_order": "5,1", "csrf_token": token, }, ) assert models.get_events("Projets/demo") == [] def test_create_issue_logs_created_event(client): token = csrf_from(client) client.post( "/api/issues", data={ "repo_full_name": "Projets/demo", "title": "Nouvelle", "column": "todo", "csrf_token": token, }, ) events = models.get_events("Projets/demo") assert events and events[0]["event_type"] == "created" assert events[0]["to_column"] == "todo" def test_events_api_and_activity_page(client): models.log_event("Projets/demo", 1, "moved", from_column="backlog", to_column="review") r = client.get("/api/events?repo=Projets/demo") assert r.status_code == 200 assert r.json()["events"][0]["event_type"] == "moved" r = client.get("/activity") assert r.status_code == 200 assert "Projets/demo" in r.text def test_metrics_counts_and_cycle_time(): models.log_event("a/b", 1, "created", to_column="backlog", created_at=_days_ago(10)) models.log_event("a/b", 1, "moved", from_column="backlog", to_column="in_progress", created_at=_days_ago(9)) models.log_event("a/b", 1, "closed", to_column="done", created_at=_days_ago(4)) metrics = models.get_metrics("a/b", window_days=30) assert metrics["created"] == 1 assert metrics["closed"] == 1 assert metrics["avg_cycle_days"] == 5.0 # in_progress (J-9) -> closed (J-4) r = models.get_metrics("a/b", window_days=30) assert r["cycle_samples"] == 1 def test_column_entry_times_ignores_comments(): models.log_event("a/b", 1, "moved", to_column="todo", created_at=_days_ago(3)) models.log_event("a/b", 1, "commented", created_at=_days_ago(0)) times = models.column_entry_times("a/b") assert times[1] == _days_ago(3) # ── Panneau détail ───────────────────────────────────────── def test_issue_detail_returns_body_and_comments(client): r = client.get("/api/issues/Projets/demo/1") assert r.status_code == 200 data = r.json() assert data["issue"]["title"] == "Tâche un" assert "markdown" in data["issue"]["body"] assert data["comments"][0]["body"] == "Premier commentaire" assert data["comments"][0]["user"]["login"] == "bruno" def test_issue_detail_404(client): r = client.get("/api/issues/Projets/demo/999") assert r.status_code == 404 def test_post_comment_goes_to_gitea_and_journals(client, fake_gitea): token = csrf_from(client) r = client.post( "/api/issues/Projets/demo/1/comments", data={"body": "Bien vu !", "csrf_token": token}, ) assert r.status_code == 201 assert fake_gitea.posted_comments == [("Projets", "demo", 1, "Bien vu !")] assert models.get_events("Projets/demo")[0]["event_type"] == "commented" def test_post_comment_requires_csrf_and_body(client): r = client.post("/api/issues/Projets/demo/1/comments", data={"body": "x"}) assert r.status_code == 403 token = csrf_from(client) r = client.post( "/api/issues/Projets/demo/1/comments", data={"body": " ", "csrf_token": token}, ) assert r.status_code == 400 def test_board_contains_detail_drawer(client): r = client.get("/board/Projets/demo") assert 'id="card-detail"' in r.text assert "marked.min.js" in r.text and "purify.min.js" in r.text # ── Vieillissement + WIP ─────────────────────────────────── def test_aging_badge_after_threshold(client): models.move_issue("Projets/demo", 1, "todo") models.log_event("Projets/demo", 1, "moved", to_column="todo", created_at=_days_ago(12)) r = client.get("/board/Projets/demo") assert "age-badge" in r.text assert "12 j" in r.text def test_no_aging_badge_for_fresh_card(client): # Every open card entered its column recently: no badge anywhere. # (A card with no journal entry falls back to its Gitea creation # date — the fake issues are old, so both cards get fresh entries.) models.move_issue("Projets/demo", 1, "todo") models.log_event("Projets/demo", 1, "moved", to_column="todo", created_at=_days_ago(1)) models.log_event("Projets/demo", 5, "created", to_column="backlog", created_at=_days_ago(1)) r = client.get("/board/Projets/demo") assert "age-badge" not in r.text def test_wip_limit_marks_column_over(client, monkeypatch): monkeypatch.setenv("KANBANTE_WIP_LIMITS", "in_progress:1") models.move_issue("Projets/demo", 1, "in_progress") models.move_issue("Projets/demo", 5, "in_progress") r = client.get("/board/Projets/demo") assert 'data-limit="1"' in r.text assert "2/1" in r.text assert "over" in r.text