| """Ephemeral demo tenants self-destruct: the daily scheduler sweep purges |
| expired demos WITH all their child rows, never touches live demos or real |
| clients, and the weekly reindex spends zero crawl resources on demos. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from datetime import UTC, datetime, timedelta |
|
|
| from sqlalchemy import func, select |
|
|
| from app.models import ChatMessage, ChatSession, KnowledgeSource, Tenant |
| from app.scheduler import purge_expired_demos, reindex_all_urls |
|
|
|
|
| def _expired() -> datetime: |
| return datetime.now(UTC) - timedelta(hours=1) |
|
|
|
|
| def _alive() -> datetime: |
| return datetime.now(UTC) + timedelta(hours=47) |
|
|
|
|
| async def _demo_tenant(db, slug: str, expires_at: datetime) -> Tenant: |
| t = Tenant(slug=slug, name=slug, is_demo=True, demo_expires_at=expires_at) |
| db.add(t) |
| await db.commit() |
| return t |
|
|
|
|
| async def _count(db, model, *where) -> int: |
| return ( |
| await db.execute(select(func.count()).select_from(model).where(*where)) |
| ).scalar_one() |
|
|
|
|
| async def test_expired_demo_purged_with_all_children(db_session): |
| """An expired demo tenant disappears completely: tenant row, knowledge |
| source, chat session AND chat messages — verified present before, gone |
| after.""" |
| t = await _demo_tenant(db_session, "demo-caducada", _expired()) |
| session = ChatSession(tenant_id=t.id) |
| db_session.add_all([ |
| KnowledgeSource( |
| tenant_id=t.id, kind="url", name="web", location="https://demo.example" |
| ), |
| session, |
| ]) |
| await db_session.flush() |
| db_session.add(ChatMessage(session_id=session.id, role="user", content="hola")) |
| await db_session.commit() |
|
|
| |
| assert await _count(db_session, Tenant, Tenant.slug == "demo-caducada") == 1 |
| assert await _count(db_session, KnowledgeSource, KnowledgeSource.tenant_id == t.id) == 1 |
| assert await _count(db_session, ChatSession, ChatSession.tenant_id == t.id) == 1 |
| assert await _count(db_session, ChatMessage, ChatMessage.session_id == session.id) == 1 |
| tenant_id = t.id |
|
|
| purged = await purge_expired_demos(db_session) |
|
|
| assert purged == 1 |
| db_session.expire_all() |
| assert await _count(db_session, Tenant, Tenant.slug == "demo-caducada") == 0 |
| assert await _count(db_session, KnowledgeSource, KnowledgeSource.tenant_id == tenant_id) == 0 |
| assert await _count(db_session, ChatSession, ChatSession.tenant_id == tenant_id) == 0 |
| assert await _count(db_session, ChatMessage, ChatMessage.session_id == session.id) == 0 |
|
|
|
|
| async def test_unexpired_demo_and_real_tenant_survive_purge_run(db_session): |
| """One purge run with all three kinds of tenant: the expired demo is |
| removed while the still-running demo and the real client keep their rows |
| AND their children intact.""" |
| await _demo_tenant(db_session, "demo-vieja", _expired()) |
| fresh = await _demo_tenant(db_session, "demo-fresca", _alive()) |
| real = Tenant(slug="cliente-real", name="Cliente Real") |
| db_session.add(real) |
| await db_session.commit() |
| db_session.add_all([ |
| KnowledgeSource( |
| tenant_id=fresh.id, kind="url", name="f", location="https://fresca.example" |
| ), |
| KnowledgeSource( |
| tenant_id=real.id, kind="url", name="r", location="https://real.example" |
| ), |
| ChatSession(tenant_id=fresh.id), |
| ChatSession(tenant_id=real.id), |
| ]) |
| await db_session.commit() |
|
|
| purged = await purge_expired_demos(db_session) |
|
|
| assert purged == 1 |
| db_session.expire_all() |
| assert await _count(db_session, Tenant, Tenant.slug == "demo-vieja") == 0 |
| |
| survivor_demo = ( |
| await db_session.execute(select(Tenant).where(Tenant.slug == "demo-fresca")) |
| ).scalar_one() |
| assert survivor_demo.is_demo is True |
| assert survivor_demo.demo_expires_at is not None |
| assert await _count( |
| db_session, KnowledgeSource, KnowledgeSource.tenant_id == survivor_demo.id |
| ) == 1 |
| assert await _count( |
| db_session, ChatSession, ChatSession.tenant_id == survivor_demo.id |
| ) == 1 |
| |
| survivor_real = ( |
| await db_session.execute(select(Tenant).where(Tenant.slug == "cliente-real")) |
| ).scalar_one() |
| assert survivor_real.is_demo is False |
| assert await _count( |
| db_session, KnowledgeSource, KnowledgeSource.tenant_id == survivor_real.id |
| ) == 1 |
| assert await _count( |
| db_session, ChatSession, ChatSession.tenant_id == survivor_real.id |
| ) == 1 |
|
|
|
|
| async def test_reindex_skips_demo_tenant_sources(db_session, monkeypatch): |
| """The periodic recrawl refreshes the real tenant's URL source and never |
| touches the 48h demo's one — demos get indexed once at creation, full |
| stop.""" |
| from app.rag import index |
|
|
| real = Tenant(slug="tienda-real", name="Tienda Real") |
| demo = Tenant( |
| slug="demo-viva", name="Demo Viva", is_demo=True, demo_expires_at=_alive() |
| ) |
| db_session.add_all([real, demo]) |
| await db_session.commit() |
| db_session.add_all([ |
| KnowledgeSource( |
| tenant_id=real.id, kind="url", name="r", location="https://tienda.example" |
| ), |
| KnowledgeSource( |
| tenant_id=demo.id, kind="url", name="d", location="https://demo-viva.example" |
| ), |
| ]) |
| await db_session.commit() |
|
|
| indexed: list[str] = [] |
|
|
| async def fake_index_source(db, src): |
| indexed.append(src.location) |
| src.status = "indexed" |
| return src |
|
|
| monkeypatch.setattr(index, "index_source", fake_index_source) |
| done = await reindex_all_urls(db_session) |
|
|
| |
| assert done == 1 |
| assert indexed == ["https://tienda.example"] |
| |
| assert "https://demo-viva.example" not in indexed |
|
|