192 lines
7.6 KiB
Python
192 lines
7.6 KiB
Python
"""Fusion ACC-6.2 — pilote d'écriture approuvée : archiver un résumé
|
|
de réunion Flowdeck dans ObsiGate via la file d'approbations unifiée.
|
|
|
|
Chaîne testée : demande déposée dans `agent_approvals` (outil
|
|
`archiver_reunion_obsigate`) → décision dans la file (route
|
|
gouvernance) → si approuvée, `propose_write_file` part vers ObsiGate
|
|
par le pont MCP (ACC-2.2) et la distribution est journalisée (canal
|
|
`obsigate`). Rejet = rien n'est envoyé. Sans connecteur ObsiGate,
|
|
l'exécution échoue proprement avec un message explicite.
|
|
|
|
La preuve réelle reste conditionnelle (chantier Meetings A/B +
|
|
clé ObsiGate dédiée de Bruno) ; ici le serveur MCP est simulé.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from app.auth.session import SessionManager
|
|
from app.db import get_conn
|
|
from app.services import mcp_client, meeting_archive, meetings
|
|
from app.services.importers import url_fetch
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _dns_monde_reel(monkeypatch):
|
|
monkeypatch.setattr(url_fetch, "_validate_url", lambda url: url.strip())
|
|
monkeypatch.setattr(mcp_client, "_sessions", {})
|
|
|
|
|
|
@pytest.fixture
|
|
def admin_cookies():
|
|
with get_conn() as conn:
|
|
conn.execute(
|
|
"INSERT OR IGNORE INTO users (id, login, full_name, is_admin)"
|
|
" VALUES (1, 'tester', 'Tester', 1)")
|
|
conn.commit()
|
|
session = SessionManager.create_session(
|
|
{"id": 1, "login": "tester", "is_admin": 1})
|
|
return {"flowdeck_session": session}
|
|
|
|
|
|
def _transcript_avec_resume():
|
|
with get_conn() as conn:
|
|
pid = conn.execute(
|
|
"INSERT INTO pages (workspace, title, content, content_format)"
|
|
" VALUES ('test', 'Réunion hebdo', '', 'blocks')").lastrowid
|
|
tid = conn.execute(
|
|
"INSERT INTO meeting_transcripts (page_id, summary)"
|
|
" VALUES (?, ?)",
|
|
(pid, "## Décisions\n\n- Lancer le pilote d'archivage.\n"
|
|
"- Revoir le pont MCP la semaine prochaine.")).lastrowid
|
|
conn.commit()
|
|
meetings._update(tid, generated_title="Hebdo produit", status="done")
|
|
return tid
|
|
|
|
|
|
def _creer_connecteur_obsigate(client):
|
|
r = client.post("/api/agent/connectors", json={
|
|
"name": "ObsiGate", "kind": "obsigate",
|
|
"url": "https://og.dracodev.net/mcp",
|
|
"secret": "og_cle_pont_test", "auth": "bearer"})
|
|
assert r.status_code == 200, r.text
|
|
return r.json()["id"]
|
|
|
|
|
|
def _faux_rpc_obsigate(appels):
|
|
async def fake_rpc(url, payload, headers=None):
|
|
appels.append(payload.get("method"))
|
|
method = payload.get("method")
|
|
if method == "initialize":
|
|
return {"result": {"protocolVersion": "2025-03-26",
|
|
"serverInfo": {"name": "obsigate"}}}
|
|
if method == "tools/call":
|
|
params = payload.get("params") or {}
|
|
appels.append((params.get("name"), params.get("arguments")))
|
|
return {"result": {"content": [
|
|
{"type": "text", "text": json.dumps({"draft_id": "b-42"})}]}}
|
|
return {}
|
|
return fake_rpc
|
|
|
|
|
|
def test_demande_approbation_et_gardes(client, admin_cookies):
|
|
tid = _transcript_avec_resume()
|
|
r = client.post(f"/api/v2/meetings/transcripts/{tid}/archiver-obsigate",
|
|
cookies=admin_cookies)
|
|
assert r.status_code == 201, r.text
|
|
body = r.json()
|
|
assert body["titre"] == "Hebdo produit"
|
|
assert body["chemin"].startswith("Réunions/")
|
|
assert body["chemin"].endswith("Hebdo produit.md")
|
|
approval = body["approval"]
|
|
assert approval["tool"] == "archiver_reunion_obsigate"
|
|
assert approval["status"] == "pending"
|
|
assert json.loads(approval["args_json"])["transcript_id"] == tid
|
|
# Doublon refusé tant que la demande est en attente.
|
|
r2 = client.post(f"/api/v2/meetings/transcripts/{tid}/archiver-obsigate",
|
|
cookies=admin_cookies)
|
|
assert r2.status_code == 400
|
|
|
|
|
|
def test_resume_vide_refuse(client, admin_cookies):
|
|
with get_conn() as conn:
|
|
pid = conn.execute(
|
|
"INSERT INTO pages (workspace, title, content, content_format)"
|
|
" VALUES ('test', 'Vide', '', 'blocks')").lastrowid
|
|
tid = conn.execute(
|
|
"INSERT INTO meeting_transcripts (page_id) VALUES (?)",
|
|
(pid,)).lastrowid
|
|
conn.commit()
|
|
r = client.post(f"/api/v2/meetings/transcripts/{tid}/archiver-obsigate",
|
|
cookies=admin_cookies)
|
|
assert r.status_code == 400
|
|
assert "Résumé vide" in r.text
|
|
|
|
|
|
def test_approbation_execute_la_proposition(client, admin_cookies, monkeypatch):
|
|
_creer_connecteur_obsigate(client)
|
|
appels: list = []
|
|
monkeypatch.setattr(mcp_client, "_rpc", _faux_rpc_obsigate(appels))
|
|
tid = _transcript_avec_resume()
|
|
r = client.post(f"/api/v2/meetings/transcripts/{tid}/archiver-obsigate",
|
|
cookies=admin_cookies)
|
|
aid = r.json()["approval"]["id"]
|
|
|
|
d = client.post(f"/api/v2/agent-approvals/{aid}/decide",
|
|
cookies=admin_cookies, json={"approve": True})
|
|
assert d.status_code == 200, d.text
|
|
body = d.json()
|
|
assert body["status"] == "approved"
|
|
assert body["archive"]["status"] == "ok", body["archive"]
|
|
assert "Réunions/" in body["archive"]["detail"]
|
|
# propose_write_file a reçu le résumé, jamais un apply direct.
|
|
calls = [a for a in appels if isinstance(a, tuple)]
|
|
assert len(calls) == 1 and calls[0][0] == "propose_write_file"
|
|
assert "Lancer le pilote" in calls[0][1]["content"]
|
|
assert calls[0][1]["path"].endswith("Hebdo produit.md")
|
|
# Distribution journalisée, canal obsigate.
|
|
with get_conn() as conn:
|
|
rows = conn.execute(
|
|
"SELECT * FROM meeting_distributions WHERE transcript_id=?",
|
|
(tid,)).fetchall()
|
|
assert len(rows) == 1
|
|
assert rows[0]["channel"] == "obsigate" and rows[0]["status"] == "propose"
|
|
|
|
|
|
def test_rejet_ne_envoie_rien(client, admin_cookies, monkeypatch):
|
|
_creer_connecteur_obsigate(client)
|
|
appels: list = []
|
|
monkeypatch.setattr(mcp_client, "_rpc", _faux_rpc_obsigate(appels))
|
|
tid = _transcript_avec_resume()
|
|
aid = meeting_archive.demander_approbation(tid, 1)["id"]
|
|
d = client.post(f"/api/v2/agent-approvals/{aid}/decide",
|
|
cookies=admin_cookies, json={"approve": False})
|
|
assert d.json()["archive"]["status"] == "rejete"
|
|
assert not [a for a in appels if isinstance(a, tuple)]
|
|
with get_conn() as conn:
|
|
n = conn.execute(
|
|
"SELECT COUNT(*) AS n FROM meeting_distributions"
|
|
" WHERE transcript_id=?", (tid,)).fetchone()["n"]
|
|
assert n == 0
|
|
|
|
|
|
def test_sans_connecteur_erreur_explicite(client, admin_cookies):
|
|
tid = _transcript_avec_resume()
|
|
aid = meeting_archive.demander_approbation(tid, 1)["id"]
|
|
d = client.post(f"/api/v2/agent-approvals/{aid}/decide",
|
|
cookies=admin_cookies, json={"approve": True})
|
|
archive = d.json()["archive"]
|
|
assert archive["status"] == "erreur"
|
|
assert "Connecteur ObsiGate non configuré" in archive["detail"]
|
|
|
|
|
|
def test_construire_archive_contenu(client):
|
|
tr = meetings.get_transcript(_transcript_avec_resume())
|
|
archive = meeting_archive.construire_archive(tr)
|
|
assert archive["chemin"].startswith("Réunions/")
|
|
assert "# Hebdo produit" in archive["contenu"]
|
|
assert "Lancer le pilote" in archive["contenu"]
|
|
assert "/pages/" in archive["contenu"] # lien profond vers la note
|
|
|
|
|
|
def test_executer_async_direct(client, monkeypatch):
|
|
appels: list = []
|
|
monkeypatch.setattr(mcp_client, "_rpc", _faux_rpc_obsigate(appels))
|
|
tid = _transcript_avec_resume()
|
|
approval = meeting_archive.demander_approbation(tid, 1)
|
|
# Sans connecteur : erreur propre, pas d'exception.
|
|
out = asyncio.run(meeting_archive.executer(approval))
|
|
assert out["status"] == "erreur"
|