flexigo-support-bot / tests /tools /test_tools.py
victor34593993's picture
wave3: agentic write-actions
61b5750 verified
Raw
History Blame
10.1 kB
from __future__ import annotations
from app.models import ChatSession, KnowledgeChunk, KnowledgeSource
from app.tools import registry
from app.tools.registry import ToolContext
class FakeShopify:
def __init__(self, orders_payload=None, products_payload=None):
self.orders_payload = orders_payload or {"orders": {"edges": []}}
self.products_payload = products_payload or {"products": {"edges": []}}
async def execute(self, query, variables=None):
return self.orders_payload if "orders(" in query.lower() else self.products_payload
async def _session(db):
s = ChatSession(shop="x")
db.add(s)
await db.flush()
return s
def test_specs_expose_tools():
names = {s.name for s in registry.specs()}
assert {
"search_knowledge",
"search_products",
"create_cart_link",
"watch_stock",
"lookup_order",
"reorder",
"cancel_order",
"change_shipping_address",
"start_return",
"escalate_to_human",
} <= names
async def test_cart_link_with_discount(db_session):
ctx = ToolContext(db=db_session, session=await _session(db_session), shop="s.myshopify.com")
out = await registry.dispatch(
"create_cart_link", {"items": [{"variant_id": "9"}], "discount": "VERANO10"}, ctx
)
assert out["checkout_url"] == "https://s.myshopify.com/cart/9:1?discount=VERANO10"
async def test_watch_stock_saves(db_session):
from sqlalchemy import select
from app.models import StockWatch
ctx = ToolContext(db=db_session, session=await _session(db_session), tenant_id=1)
miss = await registry.dispatch("watch_stock", {}, ctx)
assert miss["status"] == "need_info"
out = await registry.dispatch(
"watch_stock", {"email": "c@x.com", "product_title": "Cinta X100"}, ctx
)
assert out["status"] == "saved"
rows = (await db_session.execute(select(StockWatch))).scalars().all()
assert rows[0].email == "c@x.com" and rows[0].product_title == "Cinta X100"
async def test_dispatch_knowledge_returns_context(db_session, monkeypatch):
from app import embeddings as emb
async def fake_query(q):
return [1.0, 0.0]
async def fake_texts(texts, *, kind="passage"):
return [[1.0, 0.0] for _ in texts]
monkeypatch.setattr(emb, "embed_query", fake_query)
monkeypatch.setattr(emb, "embed_texts", fake_texts)
src = KnowledgeSource(kind="url", name="FAQ", location="https://e")
db_session.add(src)
await db_session.flush()
db_session.add(
KnowledgeChunk(
source_id=src.id, ordinal=0, text="Envíos gratis desde 199€", embedding=[1.0, 0.0],
meta={"source_name": "FAQ"},
)
)
await db_session.flush()
ctx = ToolContext(db=db_session, session=await _session(db_session))
out = await registry.dispatch("search_knowledge", {"query": "envíos"}, ctx)
assert "Envíos gratis" in out["context"]
assert out["sources"] == ["FAQ"]
async def test_dispatch_products_surfaces_cards_and_add_url(db_session):
payload = {
"products": {
"edges": [
{
"node": {
"title": "X100",
"onlineStoreUrl": "https://shop/x100",
"description": "d",
"featuredImage": {"url": "https://img/x.jpg"},
"variants": {"edges": [{"node": {"id": "gid://shopify/ProductVariant/555", "title": "Default", "price": "10.0", "availableForSale": True}}]},
}
}
]
}
}
ctx = ToolContext(
db=db_session, session=await _session(db_session),
shopify=FakeShopify(products_payload=payload), shop="shop.myshopify.com",
)
out = await registry.dispatch("search_products", {"query": "x"}, ctx)
assert out["status"] == "ok"
assert out["products"][0]["title"] == "X100"
# surfaced as a visual card with image + one-tap add-to-cart permalink
assert len(ctx.cards) == 1
assert ctx.cards[0]["image"] == "https://img/x.jpg"
assert ctx.cards[0]["add_url"] == "https://shop.myshopify.com/cart/555:1"
async def test_create_cart_link(db_session):
ctx = ToolContext(db=db_session, session=await _session(db_session), shop="shop.myshopify.com")
out = await registry.dispatch(
"create_cart_link",
{"items": [{"variant_id": "555", "quantity": 2}, {"variant_id": "gid://shopify/ProductVariant/777"}]},
ctx,
)
assert out["status"] == "ok"
assert out["checkout_url"] == "https://shop.myshopify.com/cart/555:2,777:1"
async def test_order_tool_needs_email_first(db_session):
ctx = ToolContext(db=db_session, session=await _session(db_session), shopify=FakeShopify())
out = await registry.dispatch("lookup_order", {}, ctx)
assert out["status"] == "need_info"
assert out["need"] == ["email"]
async def test_order_tool_email_only_asks_for_second_identifier(db_session):
ctx = ToolContext(db=db_session, session=await _session(db_session), shopify=FakeShopify())
out = await registry.dispatch("lookup_order", {"email": "a@b.c"}, ctx)
assert out["status"] == "need_info"
assert out["need"] == ["second_identifier"]
assert "postal_code" in out["accepts"] # flexible: not only the order number
async def test_order_tool_verified_with_postal_code(db_session):
payload = {
"orders": {
"edges": [
{
"node": {
"id": "gid://shopify/Order/1",
"name": "#1001",
"email": "a@b.c",
"customer": {"email": "a@b.c", "firstName": "Ana", "lastName": "G"},
"shippingAddress": {"zip": "08480", "city": "X"},
"displayFulfillmentStatus": "IN_PROGRESS",
"fulfillments": [
{"displayStatus": "IN_TRANSIT", "trackingInfo": [{"company": "GLS", "number": "T1", "url": "u"}]}
],
}
}
]
}
}
ctx = ToolContext(
db=db_session, session=await _session(db_session), shopify=FakeShopify(orders_payload=payload)
)
# customer gives email + postal code (NOT the order number) → still verifies
out = await registry.dispatch("lookup_order", {"email": "a@b.c", "postal_code": "08480"}, ctx)
assert out["status"] == "verified"
assert out["tracking"]["tracking"][0]["number"] == "T1"
assert "address" not in out["tracking"]
async def test_order_tool_verified_returns_tracking_no_pii(db_session):
payload = {
"orders": {
"edges": [
{
"node": {
"id": "gid://shopify/Order/1",
"name": "#1001",
"email": "a@b.c",
"displayFinancialStatus": "PAID",
"displayFulfillmentStatus": "IN_PROGRESS",
"customer": {"email": "a@b.c"},
"fulfillments": [
{
"displayStatus": "IN_TRANSIT",
"estimatedDeliveryAt": "2026-06-10",
"deliveredAt": None,
"inTransitAt": "2026-06-05",
"trackingInfo": [{"company": "GLS", "number": "T1", "url": "http://t/T1"}],
}
],
}
}
]
}
}
ctx = ToolContext(
db=db_session, session=await _session(db_session), shopify=FakeShopify(orders_payload=payload)
)
out = await registry.dispatch(
"lookup_order", {"email": "a@b.c", "order_number": "1001"}, ctx
)
assert out["status"] == "verified"
assert out["tracking"]["tracking"][0]["number"] == "T1"
assert "address" not in out["tracking"]
async def test_order_tool_not_found_is_generic(db_session):
ctx = ToolContext(db=db_session, session=await _session(db_session), shopify=FakeShopify())
out = await registry.dispatch(
"lookup_order", {"email": "a@b.c", "order_number": "9999"}, ctx
)
assert out["status"] == "not_found" # no field-specific leak
async def test_order_tool_invalid_email_is_generic(db_session):
ctx = ToolContext(db=db_session, session=await _session(db_session), shopify=FakeShopify())
out = await registry.dispatch(
"lookup_order", {"email": 'x" OR id:>0', "order_number": "1001"}, ctx
)
assert out["status"] == "not_found" # injection attempt → generic, no crash
async def test_order_tool_rate_limited_across_sessions(db_session, monkeypatch):
# Rotating the session must NOT reset the per-email throttle.
from app.ratelimit import get_order_limiter
limiter = get_order_limiter()
monkeypatch.setattr(limiter, "max_hits", 3)
last = None
for _ in range(4):
ctx = ToolContext(
db=db_session, session=await _session(db_session), shopify=FakeShopify()
)
last = await registry.dispatch(
"lookup_order", {"email": "spam@x.com", "order_number": "9999"}, ctx
)
assert last["status"] == "locked" # 4th attempt across fresh sessions is throttled
async def test_escalate_tool_sends(db_session):
captured = {}
async def fake_sender(to, subject, body):
captured.update(to=to, subject=subject, body=body)
return True
ctx = ToolContext(
db=db_session,
session=await _session(db_session),
support_email="support@store.com",
escalation_sender=fake_sender,
)
out = await registry.dispatch(
"escalate_to_human", {"email": "c@x.com", "question": "¿devoluciones?"}, ctx
)
assert out["status"] == "sent"
assert captured["to"] == "support@store.com"
assert "c@x.com" in captured["body"]
assert "¿devoluciones?" in captured["body"]