chore: versioning initial du systeme Context Continuity
Code du systeme de memoire multi-LLM sur Trilium : - trilium_api.py : wrapper trilium-py (notes, labels, relations) - mcp_server.py : serveur MCP Starlette (19 tools, OAuth + Bearer) - api_context.py : API REST FastAPI - trilium_context.py : workflow CLI - watchdog.sh, start_*.sh : supervision et demarrage - skills, docs et ontologie associes Secrets (.env, oauth_state.json) exclus via .gitignore.
This commit is contained in:
+310
@@ -0,0 +1,310 @@
|
||||
"""
|
||||
test_api.py — Tests unitaires FastAPI Context Continuity
|
||||
Usage : pytest test_api.py -v
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from fastapi.testclient import TestClient
|
||||
from api_context import app
|
||||
|
||||
CLIENT = TestClient(app)
|
||||
HEADERS_CLAUDE = {"Authorization": "VyFeOxT1nfyPULlx4z5uts7p5R_RrJIOZ3y_f1aoMk8"}
|
||||
HEADERS_LECHAT = {"Authorization": "WE2gw12Eerz0H-kV6SHNDAq-NN9Z6I81KAq3T2giNME"}
|
||||
HEADERS_INVALID = {"Authorization": "mauvaise_cle"}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
NOTE_MOCK = {
|
||||
"noteId": "test123",
|
||||
"title": "Note Test",
|
||||
"utcDateCreated": "2026-05-30T10:00:00Z",
|
||||
"utcDateModified": "2026-05-30T10:00:00Z",
|
||||
"attributes": [
|
||||
{"type": "label", "name": "type", "value": "backlogItem"},
|
||||
{"type": "label", "name": "projet", "value": "TestProjet"},
|
||||
{"type": "label", "name": "statut", "value": "a faire"},
|
||||
{"type": "label", "name": "priorite","value": "haute"},
|
||||
],
|
||||
"childNoteIds": [],
|
||||
"parentNoteIds": ["root"],
|
||||
}
|
||||
|
||||
CREATE_RESULT = {
|
||||
"note": {"noteId": "new456", "title": "Nouvelle Note",
|
||||
"utcDateCreated": "2026-05-30T10:00:00Z",
|
||||
"utcDateModified": "2026-05-30T10:00:00Z",
|
||||
"attributes": [], "childNoteIds": [], "parentNoteIds": []},
|
||||
"branch": {}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests Auth
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestAuth:
|
||||
def test_health_no_auth(self):
|
||||
"""Health ne nécessite pas d'auth."""
|
||||
r = CLIENT.get("/api/health")
|
||||
assert r.status_code == 200
|
||||
assert r.json()["status"] == "ok"
|
||||
|
||||
def test_protected_no_auth(self):
|
||||
r = CLIENT.get("/api/backlog/TestProjet")
|
||||
assert r.status_code == 422 # Header manquant
|
||||
|
||||
def test_protected_invalid_key(self):
|
||||
r = CLIENT.get("/api/backlog/TestProjet", headers=HEADERS_INVALID)
|
||||
assert r.status_code == 401
|
||||
|
||||
def test_auth_claude(self):
|
||||
with patch("trilium_api.search_by_label", return_value=[]), \
|
||||
patch("trilium_api.get_label_value", return_value="a faire"):
|
||||
r = CLIENT.get("/api/backlog/TestProjet", headers=HEADERS_CLAUDE)
|
||||
assert r.status_code == 200
|
||||
|
||||
def test_auth_lechat(self):
|
||||
with patch("trilium_api.search_by_label", return_value=[]), \
|
||||
patch("trilium_api.get_label_value", return_value="a faire"):
|
||||
r = CLIENT.get("/api/backlog/TestProjet", headers=HEADERS_LECHAT)
|
||||
assert r.status_code == 200
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests Health
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestHealth:
|
||||
def test_health(self):
|
||||
r = CLIENT.get("/api/health")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["status"] == "ok"
|
||||
assert "version" in data
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests Backlog
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBacklog:
|
||||
def test_list_backlog_vide(self):
|
||||
with patch("trilium_api.search_by_label", return_value=[]), \
|
||||
patch("trilium_api.get_label_value", return_value="a faire"):
|
||||
r = CLIENT.get("/api/backlog/ProjetInexistant", headers=HEADERS_CLAUDE)
|
||||
assert r.status_code == 200
|
||||
assert r.json() == []
|
||||
|
||||
def test_list_backlog(self):
|
||||
with patch("trilium_api.search_by_label", return_value=[NOTE_MOCK]), \
|
||||
patch("trilium_api.get_label_value", side_effect=lambda nid, key: {
|
||||
"projet": "TestProjet", "statut": "a faire", "priorite": "haute"
|
||||
}.get(key, "")), \
|
||||
patch("trilium_api.get_note", return_value=NOTE_MOCK):
|
||||
r = CLIENT.get("/api/backlog/TestProjet", headers=HEADERS_CLAUDE)
|
||||
assert r.status_code == 200
|
||||
|
||||
def test_create_backlog(self):
|
||||
with patch("builtins.open", MagicMock(
|
||||
return_value=MagicMock(
|
||||
__enter__=MagicMock(return_value=MagicMock(
|
||||
read=MagicMock(return_value='{"Backlog":"root123"}'))),
|
||||
__exit__=MagicMock(return_value=False)))), \
|
||||
patch("json.load", return_value={"Backlog": "root123"}), \
|
||||
patch("trilium_api.create_note", return_value=CREATE_RESULT), \
|
||||
patch("trilium_api.get_note_id", return_value="new456"), \
|
||||
patch("trilium_api.set_label", return_value=None):
|
||||
r = CLIENT.post("/api/backlog", headers=HEADERS_CLAUDE,
|
||||
json={"projet": "TestProjet", "titre": "Ma tâche",
|
||||
"priorite": "haute"})
|
||||
assert r.status_code == 201
|
||||
assert r.json()["titre"] == "Ma tâche"
|
||||
assert r.json()["created_by"] == "Claude"
|
||||
|
||||
def test_create_backlog_priorite_invalide(self):
|
||||
r = CLIENT.post("/api/backlog", headers=HEADERS_CLAUDE,
|
||||
json={"projet": "TestProjet", "titre": "Tâche",
|
||||
"priorite": "ULTRA"})
|
||||
assert r.status_code == 422
|
||||
|
||||
def test_patch_backlog(self):
|
||||
with patch("trilium_api.get_note", return_value=NOTE_MOCK), \
|
||||
patch("trilium_api.set_label", return_value=None):
|
||||
r = CLIENT.patch("/api/backlog/test123", headers=HEADERS_CLAUDE,
|
||||
json={"statut": "en cours"})
|
||||
assert r.status_code == 200
|
||||
|
||||
def test_patch_backlog_inexistant(self):
|
||||
with patch("trilium_api.get_note", side_effect=Exception("404")):
|
||||
r = CLIENT.patch("/api/backlog/inexistant", headers=HEADERS_CLAUDE,
|
||||
json={"statut": "fait"})
|
||||
assert r.status_code == 404
|
||||
|
||||
def test_patch_backlog_statut_invalide(self):
|
||||
r = CLIENT.patch("/api/backlog/test123", headers=HEADERS_CLAUDE,
|
||||
json={"statut": "statut_invalide"})
|
||||
assert r.status_code == 422
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests Décisions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestDecisions:
|
||||
def test_list_decisions_vide(self):
|
||||
with patch("trilium_api.search_by_label", return_value=[]), \
|
||||
patch("trilium_api.get_label_value", return_value="active"):
|
||||
r = CLIENT.get("/api/decisions/TestProjet", headers=HEADERS_CLAUDE)
|
||||
assert r.status_code == 200
|
||||
assert r.json() == []
|
||||
|
||||
def test_create_decision(self):
|
||||
with patch("json.load", return_value={"Decisions": "dec123"}), \
|
||||
patch("builtins.open", MagicMock(
|
||||
return_value=MagicMock(
|
||||
__enter__=MagicMock(return_value=MagicMock()),
|
||||
__exit__=MagicMock(return_value=False)))), \
|
||||
patch("trilium_api.create_note", return_value=CREATE_RESULT), \
|
||||
patch("trilium_api.get_note_id", return_value="new456"), \
|
||||
patch("trilium_api.set_label", return_value=None):
|
||||
r = CLIENT.post("/api/decisions", headers=HEADERS_LECHAT,
|
||||
json={"projet": "TestProjet",
|
||||
"enonce": "On utilise FastAPI",
|
||||
"justification": "Validation auto Pydantic"})
|
||||
assert r.status_code == 201
|
||||
assert r.json()["created_by"] == "LeChat"
|
||||
|
||||
def test_patch_decision_annuler(self):
|
||||
with patch("trilium_api.get_note", return_value=NOTE_MOCK), \
|
||||
patch("trilium_api.set_label", return_value=None), \
|
||||
patch("trilium_api.get_note_content", return_value="<p>contenu</p>"), \
|
||||
patch("trilium_api.update_note_content", return_value=None):
|
||||
r = CLIENT.patch("/api/decisions/test123", headers=HEADERS_CLAUDE,
|
||||
json={"statut": "annulee"})
|
||||
assert r.status_code == 200
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests Historique
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestHistorique:
|
||||
def test_create_historique_valide(self):
|
||||
with patch("json.load", return_value={"Historique": "hist123"}), \
|
||||
patch("builtins.open", MagicMock(
|
||||
return_value=MagicMock(
|
||||
__enter__=MagicMock(return_value=MagicMock()),
|
||||
__exit__=MagicMock(return_value=False)))), \
|
||||
patch("trilium_api.create_note", return_value=CREATE_RESULT), \
|
||||
patch("trilium_api.get_note_id", return_value="new456"), \
|
||||
patch("trilium_api.set_label", return_value=None):
|
||||
r = CLIENT.post("/api/historique", headers=HEADERS_CLAUDE,
|
||||
json={"projet": "TestProjet",
|
||||
"enonce": "Test X effectue",
|
||||
"type": "Test effectue",
|
||||
"detail": "Resultat OK"})
|
||||
assert r.status_code == 201
|
||||
|
||||
def test_create_historique_type_invalide(self):
|
||||
r = CLIENT.post("/api/historique", headers=HEADERS_CLAUDE,
|
||||
json={"projet": "TestProjet",
|
||||
"enonce": "Test",
|
||||
"type": "Type invalide"})
|
||||
assert r.status_code == 422
|
||||
|
||||
def test_patch_invalider(self):
|
||||
with patch("trilium_api.get_note", return_value=NOTE_MOCK), \
|
||||
patch("trilium_api.set_label", return_value=None), \
|
||||
patch("trilium_api.get_note_content", return_value="<p>ok</p>"), \
|
||||
patch("trilium_api.update_note_content", return_value=None):
|
||||
r = CLIENT.patch("/api/historique/test123", headers=HEADERS_CLAUDE,
|
||||
json={"encore_valide": False})
|
||||
assert r.status_code == 200
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests Conversations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestConversations:
|
||||
def test_create_conversation(self):
|
||||
with patch("json.load", return_value={"Conversations": "conv123"}), \
|
||||
patch("builtins.open", MagicMock(
|
||||
return_value=MagicMock(
|
||||
__enter__=MagicMock(return_value=MagicMock()),
|
||||
__exit__=MagicMock(return_value=False)))), \
|
||||
patch("trilium_api.create_note", return_value=CREATE_RESULT), \
|
||||
patch("trilium_api.get_note_id", return_value="new456"), \
|
||||
patch("trilium_api.set_label", return_value=None):
|
||||
r = CLIENT.post("/api/conversations", headers=HEADERS_CLAUDE,
|
||||
json={"projet": "TestProjet",
|
||||
"titre": "Session debug",
|
||||
"llm": "Claude Sonnet"})
|
||||
assert r.status_code == 201
|
||||
|
||||
def test_create_conversation_llm_invalide(self):
|
||||
r = CLIENT.post("/api/conversations", headers=HEADERS_CLAUDE,
|
||||
json={"projet": "TestProjet",
|
||||
"titre": "Session",
|
||||
"llm": "GPT-4"})
|
||||
assert r.status_code == 422
|
||||
|
||||
def test_patch_cloture(self):
|
||||
with patch("trilium_api.get_note", return_value=NOTE_MOCK), \
|
||||
patch("trilium_api.set_label", return_value=None), \
|
||||
patch("trilium_api.get_note_content",
|
||||
return_value="<i>À remplir en fin de session</i>"), \
|
||||
patch("trilium_api.update_note_content", return_value=None):
|
||||
r = CLIENT.patch("/api/conversations/test123", headers=HEADERS_CLAUDE,
|
||||
json={"synthese_cloture": "Session terminee. Reste X."})
|
||||
assert r.status_code == 200
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests Glossaire
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestGlossaire:
|
||||
def test_create_terme(self):
|
||||
with patch("json.load", return_value={"Glossaire": "glos123"}), \
|
||||
patch("builtins.open", MagicMock(
|
||||
return_value=MagicMock(
|
||||
__enter__=MagicMock(return_value=MagicMock()),
|
||||
__exit__=MagicMock(return_value=False)))), \
|
||||
patch("trilium_api.create_note", return_value=CREATE_RESULT), \
|
||||
patch("trilium_api.get_note_id", return_value="new456"), \
|
||||
patch("trilium_api.set_label", return_value=None):
|
||||
r = CLIENT.post("/api/glossaire", headers=HEADERS_CLAUDE,
|
||||
json={"projet": "TestProjet",
|
||||
"terme": "Pipeline",
|
||||
"definition": "Chaine de traitement complète"})
|
||||
assert r.status_code == 201
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests Contexte Reprise
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestContexteReprise:
|
||||
def test_get_contexte_vide(self):
|
||||
with patch("trilium_api.search_by_label", return_value=[]), \
|
||||
patch("trilium_api.get_label_value", return_value="active"):
|
||||
r = CLIENT.get("/api/contexte/ProjetVide",
|
||||
headers=HEADERS_CLAUDE)
|
||||
assert r.status_code == 200
|
||||
assert "briefing" in r.json()
|
||||
assert "tokens" in r.json()
|
||||
|
||||
def test_get_last_contexte_inexistant(self):
|
||||
with patch("trilium_api.search_by_label", return_value=[]), \
|
||||
patch("trilium_api.get_label_value", return_value=None):
|
||||
r = CLIENT.get("/api/contexte/ProjetVide/last",
|
||||
headers=HEADERS_CLAUDE)
|
||||
assert r.status_code == 404
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests Docs
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestDocs:
|
||||
def test_swagger_accessible(self):
|
||||
r = CLIENT.get("/api/docs")
|
||||
assert r.status_code == 200
|
||||
|
||||
def test_openapi_accessible(self):
|
||||
r = CLIENT.get("/api/openapi.json")
|
||||
assert r.status_code == 200
|
||||
Reference in New Issue
Block a user