# -*- coding: utf-8 -*- """Guard requeue при ready-статусе — регресс e-261001083543-0eab6d. Сценарий падения min-тика 09:00: claim → воркер умер → exception-путь написал TaskPromoted (задача уже ready) → guard-фаза «бюджет исчерпан» вызвала requeue_task → ValueError «requeue не нужен» → тик упал, дайджест/спенд/доставed остальных задач пачки потеряны. Проверяет: _guard_budget_stub_requeue со смешанной пачкой (failed / ready / done) НЕ бросает исключений, failed уходит в ready, TaskCompleted не пишется, тик продолжает финализацию. """ import os import sys _DST = "/tmp/simbiot-test-guard-ready.db" for p in (_DST, _DST + "-wal", _DST + "-shm"): if os.path.exists(p): os.unlink(p) os.environ["SIMBIOT_DB"] = _DST sys.path.insert(0, "/root/simbiot-harness") from harness import es from harness import commands from harness.scheduler import _guard_budget_stub_requeue 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 task_state(tid): model = es.run_projections() t = model["tasks"].get(tid) return t["status"] if t else None def task_events(tid): db = es._conn() try: rows = db.execute("SELECT event_type FROM events WHERE aggregate_id=?", (tid,)).fetchall() return [r["event_type"] for r in rows] finally: db.close() stub = "тик завершён: бюджет шагов исчерпан (30 tool-вызовов)." # Пачка: три задачи в РАЗНЫХ статусах — как в реальном сбойном тике: # failed — orphan-обработчик успел TaskFailed; # ready — exception-путь уже вернул claim в очередь (TaskPromoted); # running — обычный случай, guard должен вернуть в ready. tid_failed, _ = commands.submit_task(title="пачка: failed", minutes=5, details="") es.append_event("task", tid_failed, "TaskClaimed", {"level": "min", "session_id": "s-a"}) es.append_event("task", tid_failed, "TaskFailed", {"error": "симуляция: воркер умер после claim"}) es.run_projections() check("0.1: подготовка failed", task_state(tid_failed) == "failed", task_state(tid_failed)) tid_ready, _ = commands.submit_task(title="пачка: ready", minutes=5, details="") es.append_event("task", tid_ready, "TaskClaimed", {"level": "min", "session_id": "s-a"}) es.run_projections() es.append_event("task", tid_ready, "TaskPromoted", {"to": "S", "from": "S", "reason": "симуляция: exception-путь вернул claim в очередь"}) es.run_projections() check("0.2: подготовка ready", task_state(tid_ready) == "ready", task_state(tid_ready)) tid_running, _ = commands.submit_task(title="пачка: running", minutes=5, details="") es.append_event("task", tid_running, "TaskClaimed", {"level": "min", "session_id": "s-a"}) es.run_projections() check("0.3: подготовка running", task_state(tid_running) == "running", task_state(tid_running)) batch = [tid_failed, tid_ready, tid_running] # ── 1. Главный регресс: guard на смешанной пачке НЕ бросает ── out_text, out_ids = _guard_budget_stub_requeue(batch, stub) check("1.0: нет исключения на ready-члене пачки", out_text == stub and out_ids == []) check("1.1: failed → ready", task_state(tid_failed) == "ready", task_state(tid_failed)) check("1.2: running → ready", task_state(tid_running) == "ready", task_state(tid_running)) check("1.3: ready остаётся ready (без второго TaskPromoted от guard)", task_events(tid_ready).count("TaskPromoted") == 1, task_events(tid_ready)) check("1.4: TaskCompleted не пишется guard-ом", all("TaskCompleted" not in task_events(t) for t in batch)) # ── 2. Идемпотентность: повторный guard по той же пачке тоже без исключений ── _guard_budget_stub_requeue(batch, stub) check("2.0: повторный guard не валился", task_state(tid_running) == "ready") # ── 3. Пустой claim → no-op ── t2, i2 = _guard_budget_stub_requeue([], stub) check("3.0: пустая пачка — no-op", t2 == stub and i2 == []) # ── 4. Сценарий end-to-end уровня: run_level_tick не падает на guard-фазе ── # Готовим пачку с ready-членом и напрямую вызываем guard-путь run_level_tick # через monkeypatch: executor подменяем на стаб-воркера со стаб-стоп-фразой. tid_e2e, _ = commands.submit_task(title="e2e: guard не валил тик", minutes=5, details="") es.append_event("task", tid_e2e, "TaskClaimed", {"level": "min", "session_id": "s-e2e"}) es.append_event("task", tid_e2e, "TaskPromoted", {"to": "S", "from": "S", "reason": "симуляция: task_promoted в том же тике"}) es.run_projections() check("4.0: e2e задача в ready", task_state(tid_e2e) == "ready") # run_level_tick вызывает executor внутри — guard-путь тестируем напрямую (§1), # здесь проверяем лишь тестируемость точки вызова import harness.scheduler as sch import inspect src = inspect.getsource(sch.run_level_tick) check("4.1: guard в run_level_tick идёт через функцию (тестируемо)", "_guard_budget_stub_requeue" in src) print() print("FAILS:", len(fails)) sys.exit(1 if fails else 0)