Merge branch '111-mcp-tools' into dev
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import pytest
|
||||
|
||||
import src.utils.search as search_mod
|
||||
from src.mcp.queries import (
|
||||
build_where_args,
|
||||
compute_org_stats,
|
||||
search_marches,
|
||||
search_organisations,
|
||||
)
|
||||
from src.utils.data import DF_ACHETEURS
|
||||
from src.utils.search import search_org
|
||||
|
||||
|
||||
def test_search_org_track_false_skips_track_search(monkeypatch):
|
||||
calls = []
|
||||
monkeypatch.setattr(search_mod, "track_search", lambda q, c: calls.append((q, c)))
|
||||
|
||||
search_org(DF_ACHETEURS, "ACHETEUR", "acheteur", track=False)
|
||||
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_search_org_track_true_calls_track_search(monkeypatch):
|
||||
calls = []
|
||||
monkeypatch.setattr(search_mod, "track_search", lambda q, c: calls.append((q, c)))
|
||||
|
||||
search_org(DF_ACHETEURS, "ACHETEUR", "acheteur", track=True)
|
||||
|
||||
assert calls == [("ACHETEUR", "home_page_search")]
|
||||
|
||||
|
||||
def test_search_organisations_finds_known_acheteur():
|
||||
result = search_organisations("ACHETEUR", "acheteur")
|
||||
assert any(r["id"] == "123" for r in result)
|
||||
first = next(r for r in result if r["id"] == "123")
|
||||
assert set(first.keys()) == {"id", "nom", "departement"}
|
||||
# Vérifier que le nom a été extrait en texte plain (HTML strippé)
|
||||
assert first["nom"] == "ACHETEUR 1"
|
||||
assert "<" not in first["nom"] # Défense : aucun markup HTML ne s'échappe
|
||||
|
||||
|
||||
def test_search_organisations_finds_known_titulaire():
|
||||
result = search_organisations("TITULAIRE", "titulaire")
|
||||
assert any(r["id"] == "345" for r in result)
|
||||
first = next(r for r in result if r["id"] == "345")
|
||||
assert set(first.keys()) == {"id", "nom", "departement"}
|
||||
# Vérifier que le nom a été extrait en texte plain (HTML strippé),
|
||||
# même chemin SIRET/non-SIRET différent de celui du test acheteur.
|
||||
assert first["id"] == "345"
|
||||
assert first["nom"] == "TITULAIRE 1"
|
||||
assert "<" not in first["id"]
|
||||
assert "<" not in first["nom"]
|
||||
|
||||
|
||||
def test_search_organisations_invalid_type_raises():
|
||||
with pytest.raises(ValueError):
|
||||
search_organisations("x", "autre")
|
||||
|
||||
|
||||
def test_search_organisations_respects_limite():
|
||||
result = search_organisations("ACHETEUR", "acheteur", limite=1)
|
||||
assert len(result) <= 1
|
||||
|
||||
|
||||
def test_build_where_args_named_params():
|
||||
args = build_where_args(
|
||||
{"acheteur_id": "123", "montant_min": 5, "objet_contient": "test"}, None
|
||||
)
|
||||
assert ("acheteur_id__exact", "123") in args
|
||||
assert ("montant__greater", "5") in args
|
||||
assert ("objet__contains", "test") in args
|
||||
|
||||
|
||||
def test_build_where_args_merges_filtres_avances():
|
||||
args = build_where_args(
|
||||
{"acheteur_id": "123"}, {"titulaire_departement_code__exact": "35"}
|
||||
)
|
||||
assert ("acheteur_id__exact", "123") in args
|
||||
assert ("titulaire_departement_code__exact", "35") in args
|
||||
|
||||
|
||||
def test_search_marches_returns_meta_and_rows():
|
||||
result = search_marches(acheteur_id="123")
|
||||
assert result["meta"]["total"] >= 1
|
||||
assert result["meta"]["page"] == 1
|
||||
assert result["meta"]["page_size"] == 50
|
||||
assert any(m["acheteur_id"] == "123" for m in result["marches"])
|
||||
# dates sérialisées en ISO
|
||||
assert result["marches"][0]["dateNotification"] == "2025-01-01"
|
||||
|
||||
|
||||
def test_search_marches_no_match_is_empty():
|
||||
result = search_marches(acheteur_id="inconnu-xyz")
|
||||
assert result["meta"]["total"] == 0
|
||||
assert result["marches"] == []
|
||||
|
||||
|
||||
def test_search_marches_bad_filter_returns_error():
|
||||
result = search_marches(filtres_avances={"colonne_bidon__exact": "x"})
|
||||
assert "error" in result
|
||||
|
||||
|
||||
def test_search_marches_montant_min_filters_correctly():
|
||||
assert search_marches(montant_min=100)["meta"]["total"] == 0
|
||||
assert search_marches(montant_min=1)["meta"]["total"] >= 1
|
||||
|
||||
|
||||
def test_search_marches_date_min_max_filters_correctly():
|
||||
assert search_marches(date_min="2025-01-01")["meta"]["total"] >= 1
|
||||
assert search_marches(date_min="2025-01-02")["meta"]["total"] == 0
|
||||
assert search_marches(date_max="2025-01-01")["meta"]["total"] >= 1
|
||||
assert search_marches(date_max="2024-12-31")["meta"]["total"] == 0
|
||||
|
||||
|
||||
def test_search_marches_cpv_filters_correctly():
|
||||
assert search_marches(cpv="716")["meta"]["total"] >= 1
|
||||
assert search_marches(cpv="999")["meta"]["total"] == 0
|
||||
|
||||
|
||||
def test_search_marches_page_2_is_empty_with_correct_meta():
|
||||
result = search_marches(acheteur_id="123", page=2)
|
||||
assert result["meta"]["page"] == 2
|
||||
assert result["marches"] == []
|
||||
|
||||
|
||||
def test_search_marches_page_zero_is_clamped_to_one():
|
||||
result = search_marches(acheteur_id="123", page=0)
|
||||
assert result["meta"]["page"] == 1
|
||||
|
||||
|
||||
def test_compute_org_stats_acheteur_known():
|
||||
stats = compute_org_stats("acheteur", "123")
|
||||
assert stats["nb_marches"] >= 1
|
||||
assert stats["montant_total"] == 10
|
||||
assert stats["identite"]["id"] == "123"
|
||||
assert stats["identite"]["nom"] == "ACHETEUR 1"
|
||||
assert "top_titulaires" in stats
|
||||
assert "top_cpv" in stats
|
||||
# répartition annuelle dérivée de dateNotification (2025)
|
||||
annees = [row["annee"] for row in stats["repartition_annuelle"]]
|
||||
assert 2025 in annees
|
||||
|
||||
|
||||
def test_compute_org_stats_titulaire_known():
|
||||
stats = compute_org_stats("titulaire", "345")
|
||||
assert stats["nb_marches"] >= 1
|
||||
assert "top_acheteurs" in stats
|
||||
|
||||
|
||||
def test_compute_org_stats_unknown_is_empty():
|
||||
stats = compute_org_stats("acheteur", "inconnu-xyz")
|
||||
assert stats["nb_marches"] == 0
|
||||
assert stats["montant_total"] == 0
|
||||
assert stats["repartition_annuelle"] == []
|
||||
assert stats["top_titulaires"] == []
|
||||
assert stats["top_cpv"] == []
|
||||
@@ -0,0 +1,20 @@
|
||||
import datetime
|
||||
|
||||
import polars as pl
|
||||
|
||||
from src.mcp.serialization import to_json_records
|
||||
|
||||
|
||||
def test_dates_become_iso_strings():
|
||||
df = pl.DataFrame({"d": [datetime.date(2025, 1, 1)], "n": [10]})
|
||||
assert to_json_records(df) == [{"d": "2025-01-01", "n": 10}]
|
||||
|
||||
|
||||
def test_none_is_preserved():
|
||||
df = pl.DataFrame({"nom": [None], "x": [3]})
|
||||
assert to_json_records(df) == [{"nom": None, "x": 3}]
|
||||
|
||||
|
||||
def test_strings_and_numbers_untouched():
|
||||
df = pl.DataFrame({"s": ["abc"], "f": [1.5]})
|
||||
assert to_json_records(df) == [{"s": "abc", "f": 1.5}]
|
||||
@@ -0,0 +1,33 @@
|
||||
from src.mcp import tools
|
||||
|
||||
|
||||
def test_all_four_tools_are_callable():
|
||||
for name in (
|
||||
"rechercher_organisations",
|
||||
"stats_acheteur",
|
||||
"stats_titulaire",
|
||||
"rechercher_marches",
|
||||
):
|
||||
assert callable(getattr(tools, name))
|
||||
|
||||
|
||||
def test_rechercher_organisations_returns_list():
|
||||
result = tools.rechercher_organisations("ACHETEUR", "acheteur")
|
||||
assert isinstance(result, list)
|
||||
assert any(r["id"] == "123" for r in result)
|
||||
|
||||
|
||||
def test_rechercher_marches_returns_meta():
|
||||
result = tools.rechercher_marches(acheteur_id="123")
|
||||
assert result["meta"]["total"] >= 1
|
||||
|
||||
|
||||
def test_stats_acheteur_returns_stats():
|
||||
result = tools.stats_acheteur("123")
|
||||
assert result["nb_marches"] >= 1
|
||||
assert result["identite"]["id"] == "123"
|
||||
|
||||
|
||||
def test_stats_titulaire_returns_stats():
|
||||
result = tools.stats_titulaire("345")
|
||||
assert result["nb_marches"] >= 1
|
||||
@@ -0,0 +1,42 @@
|
||||
import src.utils.tracking as tracking
|
||||
|
||||
|
||||
def test_track_mcp_tool_sends_action_and_dimension(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_post(url, params):
|
||||
captured["url"] = url
|
||||
captured["params"] = params
|
||||
|
||||
class _R:
|
||||
def raise_for_status(self):
|
||||
pass
|
||||
|
||||
return _R()
|
||||
|
||||
monkeypatch.setattr(tracking, "DEVELOPMENT", False)
|
||||
monkeypatch.setattr(tracking, "post", fake_post)
|
||||
monkeypatch.setenv("MATOMO_DOMAIN", "matomo.example")
|
||||
monkeypatch.setenv("MATOMO_ID_SITE", "1")
|
||||
|
||||
tracking.track_mcp_tool("rechercher_marches", query="informatique")
|
||||
|
||||
assert captured["params"]["action_name"] == "MCP / rechercher_marches"
|
||||
assert captured["params"]["dimension1"] == "rechercher_marches"
|
||||
assert captured["params"]["search"] == "informatique"
|
||||
|
||||
|
||||
def test_track_mcp_tool_noop_in_development(monkeypatch):
|
||||
called = False
|
||||
|
||||
def fake_post(url, params):
|
||||
nonlocal called
|
||||
called = True
|
||||
|
||||
monkeypatch.setattr(tracking, "DEVELOPMENT", True)
|
||||
monkeypatch.setattr(tracking, "post", fake_post)
|
||||
monkeypatch.setenv("MATOMO_DOMAIN", "matomo.example")
|
||||
|
||||
tracking.track_mcp_tool("stats_acheteur")
|
||||
|
||||
assert called is False
|
||||
Reference in New Issue
Block a user