mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-17 23:09:35 +00:00
186 lines
6.8 KiB
Python
186 lines
6.8 KiB
Python
"""Smoke tests for the Full OS 12-stage orchestrator + WhatsApp multi-provider."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
_FULL_OS_ROUTER = _REPO_ROOT / "api" / "routers" / "full_os.py"
|
|
|
|
from core.config.settings import get_settings
|
|
|
|
from auto_client_acquisition.email.whatsapp_multi_provider import (
|
|
PROVIDER_CHAIN, _normalize_phone, configured_providers, send_whatsapp_smart,
|
|
)
|
|
|
|
|
|
# ── Phone normalization ───────────────────────────────────────────
|
|
def test_normalize_phone_966_prefix_kept():
|
|
assert _normalize_phone("+966500000001") == "966500000001"
|
|
|
|
|
|
def test_normalize_phone_local_05():
|
|
assert _normalize_phone("0500000001") == "966500000001"
|
|
|
|
|
|
def test_normalize_phone_short_local():
|
|
assert _normalize_phone("500000001") == "966500000001"
|
|
|
|
|
|
def test_normalize_phone_strips_punctuation():
|
|
assert _normalize_phone("+966 (50) 000-0001") == "966500000001"
|
|
|
|
|
|
def test_normalize_phone_double_zero():
|
|
assert _normalize_phone("00966500000001") == "966500000001"
|
|
|
|
|
|
# ── Provider chain order ─────────────────────────────────────────
|
|
def test_provider_chain_has_4_providers():
|
|
names = [name for name, _ in PROVIDER_CHAIN]
|
|
assert names == ["green_api", "ultramsg", "fonnte", "meta_cloud"]
|
|
|
|
|
|
def test_configured_providers_empty_without_env():
|
|
"""Without any provider env, no providers configured."""
|
|
keys = [
|
|
"GREEN_API_INSTANCE_ID", "GREEN_API_TOKEN",
|
|
"ULTRAMSG_INSTANCE_ID", "ULTRAMSG_TOKEN",
|
|
"FONNTE_TOKEN",
|
|
"META_WHATSAPP_PHONE_NUMBER_ID", "META_WHATSAPP_ACCESS_TOKEN",
|
|
]
|
|
saved = {k: os.environ.pop(k, None) for k in keys}
|
|
try:
|
|
assert configured_providers() == []
|
|
finally:
|
|
for k, v in saved.items():
|
|
if v is not None:
|
|
os.environ[k] = v
|
|
|
|
|
|
def test_configured_providers_detects_green_api():
|
|
saved_id = os.environ.pop("GREEN_API_INSTANCE_ID", None)
|
|
saved_tok = os.environ.pop("GREEN_API_TOKEN", None)
|
|
os.environ["GREEN_API_INSTANCE_ID"] = "test-instance"
|
|
os.environ["GREEN_API_TOKEN"] = "test-token"
|
|
try:
|
|
assert "green_api" in configured_providers()
|
|
finally:
|
|
os.environ.pop("GREEN_API_INSTANCE_ID", None)
|
|
os.environ.pop("GREEN_API_TOKEN", None)
|
|
if saved_id is not None: os.environ["GREEN_API_INSTANCE_ID"] = saved_id
|
|
if saved_tok is not None: os.environ["GREEN_API_TOKEN"] = saved_tok
|
|
|
|
|
|
# ── Smart send with no keys → no_keys status ─────────────────────
|
|
@pytest.mark.asyncio
|
|
async def test_send_whatsapp_smart_no_keys():
|
|
keys = [
|
|
"GREEN_API_INSTANCE_ID", "GREEN_API_TOKEN",
|
|
"ULTRAMSG_INSTANCE_ID", "ULTRAMSG_TOKEN",
|
|
"FONNTE_TOKEN",
|
|
"META_WHATSAPP_PHONE_NUMBER_ID", "META_WHATSAPP_ACCESS_TOKEN",
|
|
"WHATSAPP_MOCK_MODE",
|
|
]
|
|
saved = {k: os.environ.pop(k, None) for k in keys}
|
|
saved_flag = os.environ.pop("WHATSAPP_ALLOW_LIVE_SEND", None)
|
|
os.environ["WHATSAPP_ALLOW_LIVE_SEND"] = "true"
|
|
get_settings.cache_clear()
|
|
try:
|
|
result = await send_whatsapp_smart("+966500000001", "test")
|
|
assert result.status == "no_keys"
|
|
assert result.fallback_chain_tried == []
|
|
finally:
|
|
for k, v in saved.items():
|
|
if v is not None:
|
|
os.environ[k] = v
|
|
os.environ.pop("WHATSAPP_ALLOW_LIVE_SEND", None)
|
|
if saved_flag is not None:
|
|
os.environ["WHATSAPP_ALLOW_LIVE_SEND"] = saved_flag
|
|
get_settings.cache_clear()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_whatsapp_smart_blocked_when_flag_off():
|
|
keys = [
|
|
"GREEN_API_INSTANCE_ID", "GREEN_API_TOKEN",
|
|
"ULTRAMSG_INSTANCE_ID", "ULTRAMSG_TOKEN",
|
|
"FONNTE_TOKEN",
|
|
"META_WHATSAPP_PHONE_NUMBER_ID", "META_WHATSAPP_ACCESS_TOKEN",
|
|
"WHATSAPP_MOCK_MODE",
|
|
]
|
|
saved = {k: os.environ.pop(k, None) for k in keys}
|
|
saved_flag = os.environ.pop("WHATSAPP_ALLOW_LIVE_SEND", None)
|
|
os.environ["WHATSAPP_ALLOW_LIVE_SEND"] = "false"
|
|
get_settings.cache_clear()
|
|
try:
|
|
result = await send_whatsapp_smart("+966500000001", "test")
|
|
assert result.status == "blocked"
|
|
assert result.error == "whatsapp_allow_live_send_false"
|
|
finally:
|
|
for k, v in saved.items():
|
|
if v is not None:
|
|
os.environ[k] = v
|
|
os.environ.pop("WHATSAPP_ALLOW_LIVE_SEND", None)
|
|
if saved_flag is not None:
|
|
os.environ["WHATSAPP_ALLOW_LIVE_SEND"] = saved_flag
|
|
get_settings.cache_clear()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_whatsapp_smart_mock_mode():
|
|
saved = os.environ.pop("WHATSAPP_MOCK_MODE", None)
|
|
os.environ["WHATSAPP_MOCK_MODE"] = "true"
|
|
try:
|
|
result = await send_whatsapp_smart("+966500000001", "test")
|
|
assert result.status == "mock"
|
|
assert result.provider == "mock"
|
|
finally:
|
|
os.environ.pop("WHATSAPP_MOCK_MODE", None)
|
|
if saved is not None:
|
|
os.environ["WHATSAPP_MOCK_MODE"] = saved
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_whatsapp_smart_invalid_phone():
|
|
saved = os.environ.pop("WHATSAPP_MOCK_MODE", None)
|
|
try:
|
|
result = await send_whatsapp_smart("", "test")
|
|
assert result.status == "http_error"
|
|
assert result.error == "invalid_phone"
|
|
finally:
|
|
if saved is not None:
|
|
os.environ["WHATSAPP_MOCK_MODE"] = saved
|
|
|
|
|
|
# ── 12-stage transition logic ─────────────────────────────────────
|
|
def test_transitions_from_full_os_router():
|
|
assert _FULL_OS_ROUTER.is_file(), f"missing {_FULL_OS_ROUTER}"
|
|
with _FULL_OS_ROUTER.open(encoding="utf-8") as f:
|
|
src = f.read()
|
|
|
|
# Sanity: 12 stages + 3 terminal = 13 keys in TRANSITIONS
|
|
import re
|
|
keys = re.findall(r'^\s*"(\w+)":\s*\[', src, re.MULTILINE)
|
|
# Filter to stage-like names (drop dict keys like "messaging_product")
|
|
stage_names = {
|
|
"new_lead", "qualifying", "qualified", "nurturing",
|
|
"meeting_booked", "meeting_done", "proposal_sent", "negotiating",
|
|
"payment_requested", "pilot_active",
|
|
"closed_won", "closed_lost", "opted_out",
|
|
}
|
|
found = stage_names & set(keys)
|
|
assert found == stage_names, f"missing stages: {stage_names - found}"
|
|
|
|
|
|
def test_category_to_stage_map_in_source():
|
|
"""Every reply category should map to a valid stage."""
|
|
with _FULL_OS_ROUTER.open(encoding="utf-8") as f:
|
|
src = f.read()
|
|
# Just check unsubscribe → opted_out and angry → closed_lost
|
|
assert '"unsubscribe":' in src and '"opted_out"' in src
|
|
assert '"angry":' in src and '"closed_lost"' in src
|