feat: Fusion ACC-2.1 — preset connecteur TikTak (MCP) et outils agent mcp_tiktak_* (#19)
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
"""Fusion ACC-2.1 — connecteur MCP Flowdeck → TikTak (première paire).
|
||||
|
||||
Le preset « tiktak » crée un connecteur MCP préconfiguré (URL du
|
||||
endpoint /mcp de TikTak, auth bearer = clé `tck_` chiffrée). Le probe
|
||||
parle le dialecte JSON-RPC réel de TikTak (initialize →
|
||||
protocolVersion 2025-03-26, tools/list avec ses trois outils en
|
||||
lecture, tools/call → content[0].text en JSON) ; les outils sont
|
||||
cachés puis exposés à l'agent comme `mcp_tiktak_*`.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from app.db import get_conn
|
||||
from app.services import connectors, mcp_client
|
||||
from app.services.importers import url_fetch
|
||||
from app.services.tool_registry import ToolRegistry
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _dns_monde_reel(monkeypatch):
|
||||
"""L'environnement de test local résout les hôtes publics vers
|
||||
198.18.0.0/15 (plage « reserved ») : la garde SSRF les refuse, alors
|
||||
qu'en production/CI tiktak.dracodev.net résout vers Cloudflare.
|
||||
On neutralise la résolution pour la création — le comportement du
|
||||
produit n'est pas modifié, seuls les tests s'affranchissent du DNS
|
||||
local (les tests v755/v757 échouent localement pour la même cause).
|
||||
"""
|
||||
monkeypatch.setattr(url_fetch, "_validate_url", lambda url: url.strip())
|
||||
|
||||
# Outils tels que TikTak les déclare (app/routers/sync.py, _OUTILS).
|
||||
OUTILS_TIKTAK = [
|
||||
{"name": "list_today_tasks",
|
||||
"description": "Liste les tâches actives de l'utilisateur échéant aujourd'hui.",
|
||||
"inputSchema": {"type": "object", "properties": {}}},
|
||||
{"name": "search_tasks",
|
||||
"description": "Recherche des tâches et des notes par mots.",
|
||||
"inputSchema": {"type": "object",
|
||||
"properties": {"query": {"type": "string"}},
|
||||
"required": ["query"]}},
|
||||
{"name": "get_task",
|
||||
"description": "Lit une tâche par son identifiant.",
|
||||
"inputSchema": {"type": "object",
|
||||
"properties": {"id": {"type": "integer"}},
|
||||
"required": ["id"]}},
|
||||
]
|
||||
|
||||
|
||||
def _creer_tiktak(client, **kw):
|
||||
body = {"name": "TikTak", "kind": "tiktak",
|
||||
"url": "https://tiktak.dracodev.net/mcp",
|
||||
"secret": "tck_cle_de_test", "auth": "bearer", **kw}
|
||||
r = client.post("/api/agent/connectors", json=body)
|
||||
assert r.status_code == 200, r.text
|
||||
return r.json()
|
||||
|
||||
|
||||
def _faux_rpc_tiktak(appels, entetes):
|
||||
"""Rejoue le serveur MCP de TikTak (POST /mcp, JSON-RPC)."""
|
||||
async def fake_rpc(url, payload, headers=None):
|
||||
assert url == "https://tiktak.dracodev.net/mcp"
|
||||
entetes.append(headers or {})
|
||||
appels.append(payload.get("method"))
|
||||
method = payload.get("method")
|
||||
if method == "initialize":
|
||||
return {"result": {"protocolVersion": "2025-03-26",
|
||||
"capabilities": {"tools": {}},
|
||||
"serverInfo": {"name": "tiktak", "version": "0.51.0"}}}
|
||||
if method == "tools/list":
|
||||
return {"result": {"tools": OUTILS_TIKTAK}}
|
||||
if method == "tools/call":
|
||||
params = payload.get("params") or {}
|
||||
if params.get("name") == "list_today_tasks":
|
||||
res = [{"id": 42, "title": "Appeler le dentiste", "list": "Perso"}]
|
||||
else:
|
||||
res = {"id": 42, "title": "Appeler le dentiste"}
|
||||
return {"result": {"content": [{"type": "text",
|
||||
"text": json.dumps(res, ensure_ascii=False)}]}}
|
||||
return {}
|
||||
return fake_rpc
|
||||
|
||||
|
||||
def test_preset_tiktak_et_secret_chiffre(client):
|
||||
row = _creer_tiktak(client)
|
||||
assert row["kind"] == "tiktak" and row["auth"] == "bearer"
|
||||
assert row["has_secret"] is True
|
||||
assert "tck_cle_de_test" not in json.dumps(row) # jamais renvoyée
|
||||
assert connectors.PRESETS["tiktak"]["url"] == "https://tiktak.dracodev.net/mcp"
|
||||
hdrs = connectors._headers(row["id"])
|
||||
assert hdrs["Authorization"] == "Bearer tck_cle_de_test" # ce qu'attend TikTak
|
||||
|
||||
|
||||
def test_probe_tiktak_handshake_et_cache(client, monkeypatch):
|
||||
row = _creer_tiktak(client)
|
||||
appels, entetes = [], []
|
||||
monkeypatch.setattr(mcp_client, "_rpc", _faux_rpc_tiktak(appels, entetes))
|
||||
|
||||
res = asyncio.run(connectors.probe(str(row["id"])))
|
||||
assert res["status"] == "ok", res
|
||||
assert "3 outil(s)" in res["detail"]
|
||||
assert appels == ["initialize", "notifications/initialized", "tools/list"]
|
||||
assert entetes[0]["Authorization"] == "Bearer tck_cle_de_test"
|
||||
|
||||
with get_conn() as conn:
|
||||
tools = conn.execute("SELECT tools_json FROM agent_connectors WHERE id=?",
|
||||
(row["id"],)).fetchone()["tools_json"]
|
||||
for nom in ("mcp_tiktak_list_today_tasks", "mcp_tiktak_search_tasks",
|
||||
"mcp_tiktak_get_task"):
|
||||
assert nom in tools
|
||||
|
||||
|
||||
def test_outils_tiktak_exposes_a_l_agent(client, monkeypatch):
|
||||
row = _creer_tiktak(client)
|
||||
monkeypatch.setattr(mcp_client, "_rpc", _faux_rpc_tiktak([], []))
|
||||
asyncio.run(connectors.probe(str(row["id"])))
|
||||
|
||||
reg = ToolRegistry()
|
||||
schema = {t["name"]: t for t in reg.schema()}
|
||||
assert "mcp_tiktak_list_today_tasks" in schema
|
||||
assert schema["mcp_tiktak_search_tasks"]["parameters"]["required"] == ["query"]
|
||||
assert schema["mcp_tiktak_get_task"]["parameters"]["required"] == ["id"]
|
||||
|
||||
res = asyncio.run(reg.execute("mcp_tiktak_list_today_tasks", {}))
|
||||
assert res.status == "success"
|
||||
assert "Appeler le dentiste" in res.data["text"]
|
||||
|
||||
|
||||
def test_tiktak_desactive_masque_ses_outils(client, monkeypatch):
|
||||
row = _creer_tiktak(client)
|
||||
monkeypatch.setattr(mcp_client, "_rpc", _faux_rpc_tiktak([], []))
|
||||
asyncio.run(connectors.probe(str(row["id"])))
|
||||
client.patch(f"/api/agent/connectors/{row['id']}", json={"enabled": False})
|
||||
noms = {t["name"] for t in ToolRegistry().schema()}
|
||||
assert "mcp_tiktak_list_today_tasks" not in noms
|
||||
Reference in New Issue
Block a user