fix(mcp): status active + search_products guard + item.product + cleanup auto-name

This commit is contained in:
2026-05-28 06:41:04 +02:00
parent 87efbcb03d
commit 5d7dbec67c
2 changed files with 27 additions and 18 deletions
+19 -18
View File
@@ -361,19 +361,19 @@ async def get_shopping_lists() -> str:
@mcp.tool()
async def get_active_shopping_list() -> str:
"""Retourne la première liste en statut 'draft' avec tous ses articles triés."""
"""Retourne la première liste en statut 'draft' ou 'active' avec tous ses articles triés."""
async with AsyncSessionLocal() as session:
stmt = (
select(ShoppingList)
.where(ShoppingList.status == "draft")
.where(ShoppingList.status.in_(["draft", "active"]))
.options(selectinload(ShoppingList.items).selectinload(ListItem.product))
.order_by(ShoppingList.created_at.desc())
.order_by(ShoppingList.status.asc(), ShoppingList.created_at.desc())
.limit(1)
)
result = await session.execute(stmt)
lst = result.scalar_one_or_none()
if not lst:
return _dumps({"error": "Aucune liste active (statut draft)"})
return _dumps({"error": "Aucune liste active (statut draft ou active)"})
sorted_items = sorted(lst.items, key=lambda i: (i.sort_order or 999, str(i.id)))
return _dumps({
"id": lst.id,
@@ -394,6 +394,8 @@ async def get_active_shopping_list() -> str:
@mcp.tool()
async def search_products(q: str) -> str:
"""Recherche dans le catalogue produits par nom, description ou catégorie."""
if not q or not q.strip():
return _dumps({"error": "Paramètre q requis"})
async with AsyncSessionLocal() as session:
stmt = (
select(Product)
@@ -490,19 +492,18 @@ async def check_shopping_item(list_id: str, item_id: str) -> str:
return _dumps({"error": f"Article introuvable : {item_id} dans liste {list_id}"})
was_checked = item.is_checked
item.is_checked = True
if not was_checked and item.product_id:
product = await session.get(Product, item.product_id)
if product:
today = date_type.today()
if product.last_purchased_at and product.last_purchased_at < today:
days = (today - product.last_purchased_at).days
if product.avg_interval_days is None:
product.avg_interval_days = Decimal(str(days))
else:
product.avg_interval_days = Decimal(str(
round(float(product.avg_interval_days) * 0.7 + days * 0.3, 1)
))
product.last_purchased_at = today
product.frequency_score += 1
if not was_checked and item.product:
product = item.product
today = date_type.today()
if product.last_purchased_at and product.last_purchased_at < today:
days = (today - product.last_purchased_at).days
if product.avg_interval_days is None:
product.avg_interval_days = Decimal(str(days))
else:
product.avg_interval_days = Decimal(str(
round(float(product.avg_interval_days) * 0.7 + days * 0.3, 1)
))
product.last_purchased_at = today
product.frequency_score += 1
await session.commit()
return _dumps({"id": item.id, "is_checked": item.is_checked})
+8
View File
@@ -1,4 +1,5 @@
import json
import uuid
import pytest
from sqlalchemy import delete
import app.api.mcp_server as mcp_server_module
@@ -155,7 +156,14 @@ async def test_create_shopping_list_outil():
async def test_create_shopping_list_nom_auto():
result = await create_shopping_list()
data = json.loads(result)
# Le nom auto est au format "S{semaine} {année}" (ex: "S22 2026")
assert data["name"].startswith("S")
# Cleanup manuel — la liste auto n'a pas le préfixe TEST_MCP_
async with mcp_server_module.AsyncSessionLocal() as session:
await session.execute(
delete(ShoppingList).where(ShoppingList.id == uuid.UUID(data["id"]))
)
await session.commit()
async def test_add_shopping_item_outil():