test(autocall): детерминированная проверка окна обзвона
Харнесс прогоняет планировщик с фейковым временем и подменённым push_call_file: 19:58 и 21:03 МСК (инцидент) — звонков нет, 09:00/10:00/18:59 — звонок есть, воскресенье — нет, суббота — есть. Работает и для файла-кандидата до установки.
This commit is contained in:
parent
341ac14607
commit
d1799ca738
|
|
@ -0,0 +1,100 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Детерминированная проверка окна обзвона автодозвона.
|
||||||
|
|
||||||
|
Проверяет, что автоматическая кампания НЕ ставит call-файлы вне рабочего окна
|
||||||
|
(часы и дни из панели «Автодозвон»), не звонит в выходные и звонит внутри окна.
|
||||||
|
Ничего не звонит: push_call_file, process_queue, backfill и выборка кандидатов
|
||||||
|
подменены, время — фейковое (модульная datetime). Запускать на .8 через обёртку
|
||||||
|
(она подставит DB_*):
|
||||||
|
|
||||||
|
bash /opt/supersam/scripts/sms_run_with_db.sh \
|
||||||
|
/opt/supersam/scripts/check_autocall_window.py [путь_к_проверяемому_файлу]
|
||||||
|
|
||||||
|
Без аргумента проверяется установленный /opt/supersam/scripts/autocall_scheduler.py
|
||||||
|
(можно проверять и файл-кандидат до установки). Зачем: инцидент 28.09.2026 —
|
||||||
|
окно проверялось только для ручной очереди, и робот позвонил клиенту в 21:03.
|
||||||
|
"""
|
||||||
|
import importlib.util, datetime as _dt, sys
|
||||||
|
from importlib.machinery import SourceFileLoader
|
||||||
|
|
||||||
|
PATH = sys.argv[1] if len(sys.argv) > 1 else "/opt/supersam/scripts/autocall_scheduler.py"
|
||||||
|
_loader = SourceFileLoader("acs", PATH)
|
||||||
|
spec = importlib.util.spec_from_loader("acs", _loader)
|
||||||
|
m = importlib.util.module_from_spec(spec)
|
||||||
|
_loader.exec_module(m)
|
||||||
|
|
||||||
|
MSK = _dt.timezone(_dt.timedelta(hours=3))
|
||||||
|
|
||||||
|
|
||||||
|
class FakeDT(_dt.datetime):
|
||||||
|
fixed = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def now(cls, tz=None):
|
||||||
|
if tz is None:
|
||||||
|
return cls.fixed.astimezone(MSK).replace(tzinfo=None)
|
||||||
|
return cls.fixed.astimezone(tz)
|
||||||
|
|
||||||
|
|
||||||
|
m.datetime = FakeDT
|
||||||
|
|
||||||
|
FAKE_GROUP = {
|
||||||
|
"id": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"customer_phone": "79000000000",
|
||||||
|
"customer_phone_normalized": "79000000000",
|
||||||
|
"town": "Симферополь",
|
||||||
|
"created_at": _dt.datetime(2026, 9, 28, 9, 0, tzinfo=_dt.timezone.utc),
|
||||||
|
"delivery_type": "pickup",
|
||||||
|
"attempt_count": 0,
|
||||||
|
"no_input_count": 0,
|
||||||
|
"sms_sent_at": _dt.datetime(2026, 9, 28, 8, 0, tzinfo=_dt.timezone.utc),
|
||||||
|
"notification_status": "first_sms_sent",
|
||||||
|
"delivery_status": "pending_confirmation",
|
||||||
|
"autocall_opt_out": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
pushes = []
|
||||||
|
manual = []
|
||||||
|
|
||||||
|
|
||||||
|
def run_case(label, when_msk, expect_push, settings_patch=None):
|
||||||
|
FakeDT.fixed = when_msk.astimezone(MSK)
|
||||||
|
pushes.clear(); manual.clear()
|
||||||
|
def _fake_push(group, settings, attempt_no, is_test_call=False, **kw):
|
||||||
|
pushes.append(group["customer_phone"])
|
||||||
|
return True
|
||||||
|
|
||||||
|
m.push_call_file = _fake_push
|
||||||
|
m.process_queue = lambda conn, settings: (manual.append(1), 0)[1]
|
||||||
|
m.backfill_no_answer = lambda conn, settings: 0
|
||||||
|
m.get_groups_to_call = lambda conn, settings: [FAKE_GROUP]
|
||||||
|
m.trunk_free_slots = lambda settings: 1
|
||||||
|
m.trunk_capacity = lambda settings: 1
|
||||||
|
m.trunk_name = lambda settings: "goip5"
|
||||||
|
m.get_city_exclusion = lambda conn, town: None
|
||||||
|
m.get_exclusion_match = lambda conn, phone: None
|
||||||
|
if settings_patch:
|
||||||
|
orig = m.load_settings
|
||||||
|
|
||||||
|
def patched(conn):
|
||||||
|
s = orig(conn)
|
||||||
|
s.update(settings_patch)
|
||||||
|
return s
|
||||||
|
|
||||||
|
m.load_settings = patched
|
||||||
|
m.main()
|
||||||
|
ok = (len(pushes) > 0) == expect_push
|
||||||
|
print(f"{'PASS' if ok else 'FAIL'}: {label} → звонков {len(pushes)} (ожидали {'есть' if expect_push else 'нет'})")
|
||||||
|
return ok
|
||||||
|
|
||||||
|
|
||||||
|
results = []
|
||||||
|
results.append(run_case("понедельник 21:03 МСК (вне окна, инцидент)", _dt.datetime(2026, 9, 28, 21, 3, tzinfo=MSK), False))
|
||||||
|
results.append(run_case("понедельник 19:58 МСК (вне окна, инцидент)", _dt.datetime(2026, 9, 28, 19, 58, tzinfo=MSK), False))
|
||||||
|
results.append(run_case("понедельник 10:00 МСК (в окне)", _dt.datetime(2026, 9, 28, 10, 0, tzinfo=MSK), True))
|
||||||
|
results.append(run_case("понедельник 18:59 МСК (последняя минута окна)", _dt.datetime(2026, 9, 28, 18, 59, tzinfo=MSK), True))
|
||||||
|
results.append(run_case("воскресенье 12:00 МСК (нерабочий день)", _dt.datetime(2026, 9, 27, 12, 0, tzinfo=MSK), False))
|
||||||
|
results.append(run_case("суббота 12:00 МСК (рабочий день)", _dt.datetime(2026, 9, 26, 12, 0, tzinfo=MSK), True))
|
||||||
|
results.append(run_case("понедельник 09:00 МСК (граница окна)", _dt.datetime(2026, 9, 28, 9, 0, tzinfo=MSK), True))
|
||||||
|
print("\nИТОГ:", "ВСЕ PASS" if all(results) else "ЕСТЬ FAIL")
|
||||||
|
sys.exit(0 if all(results) else 1)
|
||||||
Loading…
Reference in New Issue