Files
Spark f1dd921e63 test(palier-1): 16 tests — journal, métriques, détail, âge, WIP (52 verts)
FakeGitea étendu (get_issue, commentaires); le test de réconciliation
suit le nouveau retour de reconcile_board_state; l'assertion de
version suit APP_VERSION.
2026-10-11 10:05:51 -04:00

184 lines
6.5 KiB
Python

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