"""Palier 4 (0.6.0): QA column, label reconciliation, wallboard.""" from fastapi.testclient import TestClient import app.main as main from app import models from tests.conftest import _issue, csrf_from def _column_block(html: str, column: str) -> str: blocks = html.split('
", 1)[0]: return block raise AssertionError(f"column {column} not found") def _labelled(number: int, title: str, label_names: list[str]) -> dict: issue = _issue(number, title) issue["labels"] = [ {"id": 900 + i, "name": name, "color": "8b5cf6"} for i, name in enumerate(label_names) ] return issue # ── QA column ───────────────────────────────────────────────────── def test_board_has_six_columns_including_qa(client): r = client.get("/board/Projets/demo") assert r.status_code == 200 assert "🧪 QA" in r.text positions = [ r.text.index(f'data-column="{col}"') for col in ("backlog", "todo", "in_progress", "qa", "review", "done") ] assert positions == sorted(positions) # QA between in_progress and review def test_move_to_qa_syncs_kanban_qa_label(client, fake_gitea): token = csrf_from(client) r = client.post( "/api/move", data={ "repo_full_name": "Projets/demo", "issue_number": 1, "to_column": "qa", "from_column": "backlog", "to_order": "1", "from_order": "5", "csrf_token": token, }, ) assert r.status_code == 200 assert r.json()["label_sync"] == "ok" assert ("add", 1, "kanban:qa") in fake_gitea.label_ops state = models.get_board_state("Projets/demo") assert 1 in [c["issue_number"] for c in state["qa"]] def test_qa_cards_appear_in_focus(client, fake_gitea): models.move_issue("Projets/demo", 5, "qa") r = client.get("/focus") assert r.status_code == 200 assert "Tâche focus" in r.text assert "🧪 QA" in r.text # ── Label reconciliation (adoption) ─────────────────────────────── def test_board_adopts_diverging_kanban_label(client, fake_gitea): fake_gitea.issues["Projets/demo"].append( _labelled(9, "Déplacée via Gitea", ["kanban:review"]) ) r = client.get("/board/Projets/demo") assert r.status_code == 200 assert "Déplacée via Gitea" in _column_block(r.text, "review") state = models.get_board_state("Projets/demo") assert 9 in [c["issue_number"] for c in state["review"]] events = [ e for e in models.get_events("Projets/demo") if e["issue_number"] == 9 ] assert any( e["event_type"] == "moved" and e["to_column"] == "review" and e["source"] == "gitea" for e in events ) def test_board_ignores_conflicting_kanban_labels(client, fake_gitea): fake_gitea.issues["Projets/demo"].append( _labelled(9, "Étiquettes en conflit", ["kanban:todo", "kanban:review"]) ) r = client.get("/board/Projets/demo") assert r.status_code == 200 assert "Étiquettes en conflit" in _column_block(r.text, "backlog") state = models.get_board_state("Projets/demo") elsewhere = [ c["issue_number"] for col, cards in state.items() if col != "backlog" for c in cards ] assert 9 not in elsewhere def test_adoption_is_idempotent(client, fake_gitea): fake_gitea.issues["Projets/demo"].append( _labelled(9, "Stable", ["kanban:qa"]) ) client.get("/board/Projets/demo") client.get("/board/Projets/demo") events = [ e for e in models.get_events("Projets/demo") if e["issue_number"] == 9 and e["event_type"] == "moved" ] assert len(events) == 1 # adopted once, then label and state agree # ── Wallboard ───────────────────────────────────────────────────── def test_wall_renders_active_projects(client): models.move_issue("Projets/demo", 5, "in_progress") r = client.get("/wall") assert r.status_code == 200 assert "Projets/demo" in r.text assert "Tâche focus" in r.text # the in-progress card is on the wall assert 'class="wall-slide active"' in r.text assert "🧪 QA" in r.text # the six column counters include QA def test_wall_single_repo_mode(client): r = client.get("/wall", params={"repo": "Projets/demo"}) assert r.status_code == 200 assert "Projets/demo" in r.text def test_wall_unknown_repo_shows_empty_state(client): r = client.get("/wall", params={"repo": "Inconnu/xyz"}) assert r.status_code == 200 assert "Aucun projet actif" in r.text def test_wall_is_behind_auth(monkeypatch, fake_gitea): monkeypatch.setenv("KANBANTE_AUTH_USER", "bruno") monkeypatch.setenv("KANBANTE_AUTH_PASSWORD", "motdepasse") with TestClient(main.app) as secured: assert secured.get("/wall").status_code == 401