# -*- coding: utf-8 -*- """Эскалация недоделанных задач по уровням (аудит 2026-10-17). Hermetic. Запуск: ./venv/bin/python tests/test_task_escalation.py Кейс: «бюджет исчерпан» возвращал S-задачу как S — мин-уровень брал её по кругу с нуля (33 захвата одной задачи за 2 ч). Проверяет: 1-я неудача на min — остаётся S; 2-я — S→M, мин её больше не берёт, час берёт; guard «бюджет исчерпан» идёт тем же путём; закрытую внутри тика задачу guard не трогает; на часу (берёт S/M/L) размер не растёт — расти некуда; порог настраивается env. """ import os import sys import tempfile sys.path.insert(0, "/root/simbiot-harness") os.environ["SIMBIOT_DB"] = os.path.join(tempfile.mkdtemp(prefix="simbiot_esc_"), "harness.db") os.environ.pop("SIMBIOT_TASK_ATTEMPTS_PER_LEVEL", None) from harness import es, scheduler from harness.levels import LEVELS es.init_db() fails = [] def check(name, cond, extra=""): print(("PASS" if cond else "FAIL"), name, ("| " + str(extra)) if not cond else "") if not cond: fails.append(name) def submit(tid, size="S"): es.append_event("task", tid, "TaskSubmitted", {"id": tid, "title": f"задача {tid}", "size": size, "created": "2026-10-17T08:00:00"}) def claim(tid, lvl="min"): assert es.claim_task_if_ready(tid, {"level": lvl, "session_id": f"s-{lvl}-1"}) def task(tid): return es.run_projections()["tasks"][tid] MIN, HOUR = LEVELS["min"], LEVELS["hour"] submit("t-1") claim("t-1") scheduler.requeue_or_escalate("t-1", MIN, "S", "бюджет исчерпан") check("1-я неудача на min — остаётся S, ready", task("t-1")["size"] == "S" and task("t-1")["status"] == "ready") claim("t-1") scheduler.requeue_or_escalate("t-1", MIN, "S", "бюджет исчерпан") t = task("t-1") check("2-я неудача на min — S→M", t["size"] == "M" and t["status"] == "ready", t) check("мин больше не берёт", "t-1" not in scheduler.select_task_batch(MIN, es.run_projections())) check("час берёт", "t-1" in scheduler.select_task_batch(HOUR, es.run_projections())) # guard «бюджет исчерпан» с уровнем — тот же путь submit("t-2") for _ in range(2): claim("t-2") scheduler._guard_budget_stub_requeue(["t-2"], "бюджет исчерпан", MIN, [{"id": "t-2", "size": task("t-2")["size"]}]) check("guard: после 2 попыток S→M", task("t-2")["size"] == "M", task("t-2")) # закрытую внутри тика guard не воскрешает submit("t-3") claim("t-3") es.append_event("task", "t-3", "TaskCompleted", {"result": "сделано"}) scheduler._guard_budget_stub_requeue(["t-3"], "бюджет исчерпан", MIN, [{"id": "t-3", "size": "S"}]) check("guard: закрытая задача не возвращается", task("t-3")["status"] == "done", task("t-3")["status"]) # на часу размер не растёт: час берёт S/M/L submit("t-4", "M") for _ in range(3): claim("t-4", "hour") scheduler.requeue_or_escalate("t-4", HOUR, "M", "бюджет исчерпан") check("час: размер не растёт (выше брать некому)", task("t-4")["size"] == "M") os.environ["SIMBIOT_TASK_ATTEMPTS_PER_LEVEL"] = "1" submit("t-5") claim("t-5") scheduler.requeue_or_escalate("t-5", MIN, "S", "x") check("порог из env (1 попытка)", task("t-5")["size"] == "M") print(f"\n{'OK' if not fails else 'FAILED'}: {len(fails)} fail(s)") sys.exit(1 if fails else 0)