From d1799ca7386f0a94c2e1cc4fdbef2eacc2c64b0d Mon Sep 17 00:00:00 2001 From: Hermes Date: Mon, 28 Sep 2026 22:34:44 +0200 Subject: [PATCH] =?UTF-8?q?test(autocall):=20=D0=B4=D0=B5=D1=82=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B8=D0=BD=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D0=B0=D1=8F=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BE=D0=BA=D0=BD=D0=B0=20=D0=BE=D0=B1=D0=B7=D0=B2?= =?UTF-8?q?=D0=BE=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Харнесс прогоняет планировщик с фейковым временем и подменённым push_call_file: 19:58 и 21:03 МСК (инцидент) — звонков нет, 09:00/10:00/18:59 — звонок есть, воскресенье — нет, суббота — есть. Работает и для файла-кандидата до установки. --- scripts/check_autocall_window.py | 100 +++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 scripts/check_autocall_window.py diff --git a/scripts/check_autocall_window.py b/scripts/check_autocall_window.py new file mode 100644 index 0000000..0357385 --- /dev/null +++ b/scripts/check_autocall_window.py @@ -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)