simbiot-harness/tests/test_task_escalation.py
Simbiot Harness ebcfe3b9be ядро Коди: эскалация недоделанных задач, голова task_fit, рефлексия, недельный отчёт
Разбор гейта (трассы с 15.10): hour — 0% пустых тиков, min — 6% (15%
токенов уровня) → модель-гейт не нужен. Пустые мин-тики — зацикливание:
«бюджет исчерпан» возвращал S как S, мин брал её снова с нуля (S-задача
33 захвата за 2 ч; 45 из 272 задач ≥ 2 мин-захватов).
- requeue_or_escalate: после 2 захватов на уровне (SIMBIOT_TASK_ATTEMPTS_
  PER_LEVEL) размер растёт выше классов уровня (S→M, к часу, 300 шагов) —
  и для guard «бюджет исчерпан», и для смерти воркера; счётчик попыток
  best effort (сбой → обычный requeue, не ломает путь падения воркера);
- kodi_core — несколько голов (общие: хранение/обучение/промоут/откат,
  nf в модели; SIMBIOT_CORE_<ГОЛОВА>). Голова task_fit: влезет ли S в
  мин-тик (метки — из истории задач); shadow — прогноз перед первым
  мин-захватом, on — уверенное «не влезет» сразу S→M. Честно: на 93
  задачах 5 фолдов 0.58–0.62 при базе 0.52, уверенных 0% — пока инертна;
- рефлексия (как reflexion.py): ≥ 3 расхождений вердикта адресации с
  меткой человека/чата или ≥ 3 S-задач, не влезших в мин → задача часу
  с автодоставкой: разобрать и предложить Alex правило (код не трогать);
- недельный отчёт ядра (понедельник, без LLM, идемпотентно по неделе);
  ночью: train_all → reflect → weekly_report; cli.py core reflect|report|--head.
Тесты: test_task_escalation 8, test_kodi_core_tasks 13, test_kodi_core_
reflect 10, test_kodi_core 28 PASS; полный набор — без регрессий.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 09:32:41 +00:00

89 lines
3.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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)