from datetime import datetime, timedelta, timezone import json from sqlalchemy import select from services.routing_service import engine as routing_engine from services.shared.core import new_id, utc_now_iso from services.shared.db import get_session from services.shared.sql_init import init_sql_schema from services.shared.sql_models import AgentRow def _make_agent(session, *, level="L2", tenant_ids=None, skills=None, status="AVAILABLE", enabled=True): now = utc_now_iso() row = AgentRow( agent_id=new_id("agt"), tenant_ids_json=json.dumps(tenant_ids or []), extension="2001", endpoint=None, display_name="Test Agent", level=level, skills_json=json.dumps(skills or []), status=status, max_concurrent_calls=1, enabled=enabled, created_at=now, updated_at=now, ) session.add(row) session.commit() session.refresh(row) return row def test_reserve_agent_picks_available_agent_of_requested_level(): init_sql_schema() session = get_session() try: agent_l2 = _make_agent(session, level="L2") _make_agent(session, level="L3") reserved = routing_engine.reserve_agent( session, call_id="call-1", level="L2", tenant_id=None, required_skills=[], ) assert reserved is not None assert reserved.agent_id == agent_l2.agent_id assert reserved.status == "RESERVED" assert reserved.current_call_id == "call-1" finally: session.close() def test_reserve_agent_does_not_double_reserve(): init_sql_schema() session = get_session() try: _make_agent(session, level="L2") first = routing_engine.reserve_agent(session, call_id="call-a", level="L2", tenant_id=None, required_skills=[]) second = routing_engine.reserve_agent(session, call_id="call-b", level="L2", tenant_id=None, required_skills=[]) assert first is not None assert second is None finally: session.close() def test_reserve_agent_filters_by_tenant_and_skills(): init_sql_schema() session = get_session() try: wrong_tenant = _make_agent(session, level="L2", tenant_ids=["other"]) no_skill = _make_agent(session, level="L2", tenant_ids=["konturai"], skills=[]) matching = _make_agent(session, level="L2", tenant_ids=["konturai"], skills=["billing"]) reserved = routing_engine.reserve_agent( session, call_id="call-skill", level="L2", tenant_id="konturai", required_skills=["billing"], ) assert reserved is not None assert reserved.agent_id == matching.agent_id assert reserved.agent_id != wrong_tenant.agent_id assert reserved.agent_id != no_skill.agent_id finally: session.close() def test_release_agent_by_call_id_makes_agent_available_again(): init_sql_schema() session = get_session() try: agent = _make_agent(session, level="L2") reserved = routing_engine.reserve_agent(session, call_id="call-z", level="L2", tenant_id=None, required_skills=[]) assert reserved is not None released = routing_engine.release_agent_by_call_id(session, call_id="call-z") assert released is not None assert released.agent_id == agent.agent_id assert released.status == "AVAILABLE" assert released.current_call_id is None assert released.calls_handled_count == 1 again = routing_engine.reserve_agent(session, call_id="call-again", level="L2", tenant_id=None, required_skills=[]) assert again is not None assert again.agent_id == agent.agent_id finally: session.close() def test_reserve_agent_excludes_disabled_and_excluded_ids(): init_sql_schema() session = get_session() try: disabled = _make_agent(session, level="L2", enabled=False) excluded = _make_agent(session, level="L2") available = _make_agent(session, level="L2") reserved = routing_engine.reserve_agent( session, call_id="call-exc", level="L2", tenant_id=None, required_skills=[], exclude_agent_ids=[excluded.agent_id], ) assert reserved is not None assert reserved.agent_id == available.agent_id assert reserved.agent_id != disabled.agent_id assert reserved.agent_id != excluded.agent_id finally: session.close() def test_set_agent_status_transitions_ringing_to_talking(): init_sql_schema() session = get_session() try: agent = _make_agent(session, level="L2") routing_engine.reserve_agent(session, call_id="call-status", level="L2", tenant_id=None, required_skills=[]) ringing = routing_engine.set_agent_status(session, agent_id=agent.agent_id, status="RINGING") assert ringing is not None assert ringing.status == "RINGING" talking = routing_engine.set_agent_status(session, agent_id=agent.agent_id, status="TALKING") assert talking.status == "TALKING" missing = routing_engine.set_agent_status(session, agent_id="unknown-agent", status="AVAILABLE") assert missing is None finally: session.close() def test_sweep_after_call_work_releases_only_expired_agents(): init_sql_schema() session = get_session() try: stale = _make_agent(session, level="L2", status="AFTER_CALL_WORK") fresh = _make_agent(session, level="L2", status="AFTER_CALL_WORK") stale_ts = (datetime.now(timezone.utc) - timedelta(seconds=120)).replace(microsecond=0).isoformat() stale.updated_at = stale_ts stale.current_call_id = "call-stale" session.commit() swept = routing_engine.sweep_after_call_work(session, older_than_seconds=30) swept_ids = {a.agent_id for a in swept} assert stale.agent_id in swept_ids assert fresh.agent_id not in swept_ids session.refresh(stale) session.refresh(fresh) assert stale.status == "AVAILABLE" assert stale.current_call_id is None assert fresh.status == "AFTER_CALL_WORK" finally: session.close()