165 lines
6.1 KiB
Python
165 lines
6.1 KiB
Python
"""Palier 5 (0.8.0): palette de commandes (#10) + digest hebdo (#13)."""
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from app import digest as digest_service
|
|
from app import models
|
|
from tests.conftest import csrf_from
|
|
|
|
|
|
# ── Palette (#10) ─────────────────────────────────────────────────
|
|
|
|
|
|
def test_palette_endpoint_projects_and_issues(client, fake_gitea):
|
|
r = client.get("/api/palette", params={"q": "tâche"})
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
assert any(p["full_name"] == "Projets/demo" for p in data["projects"])
|
|
titles = [i["title"] for i in data["issues"]]
|
|
assert "Tâche un" in titles
|
|
found = next(i for i in data["issues"] if i["title"] == "Tâche un")
|
|
assert found["repo"] == "Projets/demo"
|
|
assert found["column"] == "backlog"
|
|
|
|
|
|
def test_palette_short_query_skips_issue_search(client):
|
|
r = client.get("/api/palette", params={"q": "x"})
|
|
assert r.status_code == 200
|
|
assert r.json()["issues"] == []
|
|
assert r.json()["projects"] # projects are always there
|
|
|
|
|
|
def test_palette_markup_on_pages(client):
|
|
for path in ("/", "/focus", "/capture"):
|
|
r = client.get(path)
|
|
assert r.status_code == 200
|
|
assert 'id="palette-overlay"' in r.text, path
|
|
assert "/static/js/palette.js" in r.text, path
|
|
assert "palette-trigger" in r.text, path
|
|
|
|
|
|
# ── Digest : composition (#13) ────────────────────────────────────
|
|
|
|
|
|
def test_digest_markdown_format():
|
|
md = digest_service.format_digest_markdown(
|
|
"2026-10-05 au 2026-10-11",
|
|
[{"repo": "Projets/demo", "number": 3, "title": "Ancienne tâche",
|
|
"html_url": "http://gitea.test/x"}],
|
|
created_count=2,
|
|
moved_count=5,
|
|
focus_groups=[{
|
|
"full_name": "Projets/demo",
|
|
"cards": [{"issue": {"number": 5, "title": "Tâche focus"},
|
|
"column": "🚧 En cours"}],
|
|
}],
|
|
)
|
|
assert "# 🧠 Kanbante — semaine du 2026-10-05" in md
|
|
assert "## ✅ Livré (1)" in md
|
|
assert "Projets/demo#3" in md
|
|
assert "Tâche focus" in md
|
|
assert "Déplacements de cartes : 5" in md
|
|
|
|
|
|
def test_render_mcp_args_escapes_content():
|
|
args = digest_service.render_mcp_args(
|
|
'{"title": "{title}", "content": "{content}", "folder": "Kanbante"}',
|
|
'Titre "cité"',
|
|
"Ligne 1\nLigne 2 avec \\ barre",
|
|
)
|
|
assert args["title"] == 'Titre "cité"'
|
|
assert args["content"] == "Ligne 1\nLigne 2 avec \\ barre"
|
|
assert args["folder"] == "Kanbante"
|
|
with pytest.raises(ValueError):
|
|
digest_service.render_mcp_args("{pas json", "t", "c")
|
|
|
|
|
|
def test_digest_page_renders(client):
|
|
r = client.get("/digest")
|
|
assert r.status_code == 200
|
|
assert "Digest hebdo" in r.text
|
|
assert "approbation" in r.text
|
|
|
|
|
|
# ── Digest : livraison ────────────────────────────────────────────
|
|
|
|
|
|
def _mcp_handler_factory(calls: list):
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
import json as _json
|
|
payload = _json.loads(request.content.decode() or "{}")
|
|
calls.append(payload.get("method"))
|
|
method = payload.get("method")
|
|
if method == "initialize":
|
|
return httpx.Response(200, json={"jsonrpc": "2.0", "id": 1,
|
|
"result": {"protocolVersion": "2024-11-05", "capabilities": {}}})
|
|
if method == "notifications/initialized":
|
|
return httpx.Response(202, json={})
|
|
if method == "tools/list":
|
|
return httpx.Response(200, json={"jsonrpc": "2.0", "id": 2,
|
|
"result": {"tools": [{"name": "create_note"}, {"name": "search"}]}})
|
|
if method == "tools/call":
|
|
calls.append(payload["params"])
|
|
return httpx.Response(200, json={"jsonrpc": "2.0", "id": 3,
|
|
"result": {"content": [{"type": "text", "text": "note créée"}]}})
|
|
return httpx.Response(200, json={})
|
|
return handler
|
|
|
|
|
|
@pytest.fixture
|
|
def mcp_transport(monkeypatch):
|
|
calls: list = []
|
|
transport = httpx.MockTransport(_mcp_handler_factory(calls))
|
|
real_async_client = httpx.AsyncClient
|
|
|
|
def factory(*args, **kwargs):
|
|
kwargs["transport"] = transport
|
|
return real_async_client(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(httpx, "AsyncClient", factory)
|
|
return calls
|
|
|
|
|
|
async def test_mcp_list_tools(mcp_transport):
|
|
ok, tools = await digest_service.mcp_list_tools("http://obsigate.test/mcp", "")
|
|
assert ok is True
|
|
assert tools == ["create_note", "search"]
|
|
|
|
|
|
async def test_mcp_call_tool(mcp_transport):
|
|
ok, message = await digest_service.mcp_call_tool(
|
|
"http://obsigate.test/mcp", "", "create_note",
|
|
{"title": "T", "content": "C"},
|
|
)
|
|
assert ok is True
|
|
assert "note créée" in message
|
|
call = next(c for c in mcp_transport if isinstance(c, dict))
|
|
assert call["name"] == "create_note"
|
|
assert call["arguments"] == {"title": "T", "content": "C"}
|
|
|
|
|
|
def test_digest_send_to_obsigate(client, fake_gitea, monkeypatch, mcp_transport):
|
|
monkeypatch.setenv("KANBANTE_OBSIGATE_MCP_URL", "http://obsigate.test/mcp")
|
|
monkeypatch.setenv("KANBANTE_OBSIGATE_MCP_TOOL", "create_note")
|
|
token = csrf_from(client)
|
|
r = client.post("/digest/send", data={"target": "obsigate", "csrf_token": token})
|
|
assert r.status_code == 200
|
|
assert "✅ Digest envoyé (obsigate)" in r.text
|
|
assert models.get_kv("digest_last_result")
|
|
|
|
|
|
def test_digest_send_requires_configuration(client, fake_gitea):
|
|
token = csrf_from(client)
|
|
r = client.post("/digest/send", data={"target": "obsigate", "csrf_token": token})
|
|
assert r.status_code == 200
|
|
assert "❌ ObsiGate incomplet" in r.text
|
|
|
|
|
|
def test_digest_probe_lists_tools(client, fake_gitea, monkeypatch, mcp_transport):
|
|
monkeypatch.setenv("KANBANTE_OBSIGATE_MCP_URL", "http://obsigate.test/mcp")
|
|
token = csrf_from(client)
|
|
r = client.post("/digest/probe", data={"csrf_token": token})
|
|
assert r.status_code == 200
|
|
assert "create_note" in r.text
|