from __future__ import annotations from sqlalchemy import select from app.models import ChatSession, Event, Tenant from app.tenancy import generate_dashboard_token from app.tools import knowledge_tool from app.tools.registry import ToolContext async def _ctx(db, tenant_id): s = ChatSession(shop="x", tenant_id=tenant_id) db.add(s) await db.flush() return ToolContext(db=db, session=s, tenant_id=tenant_id) async def test_unanswered_question_is_logged(db_session): t = Tenant(slug="kx") db_session.add(t) await db_session.flush() ctx = await _ctx(db_session, t.id) out = await knowledge_tool.run({"query": "¿cuánto pesa el modelo XZ?"}, ctx) assert out["note"] == "no relevant information found" assert "NO la inventes" in out["hint"] # tells the model not to make it up rows = (await db_session.execute( select(Event).where(Event.type == "unresolved") )).scalars().all() assert len(rows) == 1 assert rows[0].tenant_id == t.id assert "XZ" in rows[0].meta["q"] async def test_portal_lists_unresolved_scoped(app_client, db_session): _app, client = app_client a = Tenant(slug="ua") b = Tenant(slug="ub") db_session.add_all([a, b]) await db_session.flush() a_tok = await generate_dashboard_token(db_session, a) # log one unresolved for each tenant await knowledge_tool.run({"query": "pregunta de A"}, await _ctx(db_session, a.id)) await knowledge_tool.run({"query": "pregunta de B"}, await _ctx(db_session, b.id)) await db_session.commit() rows = (await client.get( "/portal/api/unresolved", headers={"Authorization": "Bearer " + a_tok} )).json() assert len(rows) == 1 assert "A" in rows[0]["q"] # only tenant A's gap, never B's def test_portal_ui_has_unresolved_section(): from pathlib import Path html = ( Path(__file__).resolve().parent.parent / "app" / "portal_ui" / "index.html" ).read_text() assert "loadUnresolved" in html assert "/portal/api/unresolved" in html