guard(tasks): «бюджет исчерпан» — не результат (e-261001074527): complete со стабом → ValueError + авто-requeue в ready; батч-финал тика со стабом → requeue всего клея; tests/test_budget_stub_guard.py 14 checks (FAILS 0)
This commit is contained in:
parent
ed7ffd4461
commit
4e7f62d35a
3 changed files with 150 additions and 0 deletions
|
|
@ -138,6 +138,27 @@ def mark_awaiting_delivery(task_id: str, reason: str = "", reply_to: str = None)
|
|||
return task_id
|
||||
|
||||
|
||||
import re as _re
|
||||
|
||||
# Guard (e-261001074527-f0c1b4): ночной тик 20727 пачкой закрыл 5 L-задач
|
||||
# шаблонной отпиской воркера «бюджет исчерпан» — это стоп-фраза тика, а не
|
||||
# результат. Такие «результаты» закрывать задачу не могут.
|
||||
_BUDGET_STUB_RE = _re.compile(r"бюджет\s+(?:шагов\s+)?исчерпан", _re.IGNORECASE)
|
||||
_BUDGET_STUB_MARKERS_RE = _re.compile(r"тик\s+завершён|tool-вызов|шагов", _re.IGNORECASE)
|
||||
|
||||
|
||||
def looks_like_budget_stub(answer: str) -> bool:
|
||||
"""True, если answer — шаблонная отписка воркера о бюджете шагов.
|
||||
|
||||
Требование маркера («тик завершён», «tool-вызов», «шагов») отсекает
|
||||
ложные срабатывания: честный результат работы, где по делу упомянут
|
||||
исчерпанный бюджет («проверил, бюджет месяца исчерпан»), под гвард
|
||||
не попадает — а в стоп-фразе воркера маркер всегда есть.
|
||||
"""
|
||||
s = answer or ""
|
||||
return bool(_BUDGET_STUB_RE.search(s) and _BUDGET_STUB_MARKERS_RE.search(s))
|
||||
|
||||
|
||||
def complete_task_with_delivery(task_id: str, answer: str, level: str,
|
||||
result_ref: str = None):
|
||||
"""TaskCompleted + немедленная доставка результата в TG-чат.
|
||||
|
|
@ -157,6 +178,12 @@ def complete_task_with_delivery(task_id: str, answer: str, level: str,
|
|||
raise ValueError(f"задача {task_id} в статусе {t['status']} — завершать можно только running")
|
||||
if (answer or "").strip() == "":
|
||||
raise ValueError("пустой результат — доставлять нечего, заверши как failed/requeue")
|
||||
if looks_like_budget_stub(answer):
|
||||
requeue_task(task_id, reason="guard: 'бюджет исчерпан' — не результат, задача в ready (инцидент e-261001074527)")
|
||||
raise ValueError(
|
||||
f"задача {task_id} ПЕРЕВОДЖЕНА в ready: отпиской «бюджет исчерпан» "
|
||||
f"задачу закрывать нельзя. Либо доделай и завершай с реальным "
|
||||
f"результатом, либо явно requeue/failed.")
|
||||
# TaskCompleted — независимо от доставки (событие-истина о завершении)
|
||||
es.append_event("task", task_id, "TaskCompleted", {
|
||||
"result_ref": result_ref, "answer": answer[:2000],
|
||||
|
|
|
|||
|
|
@ -468,6 +468,14 @@ def run_level_tick(level_cfg, seq: int, model: dict):
|
|||
# исполнитель пишет в дайджесте). Доставка — той же командой
|
||||
# commands.deliver_result (идемпотентна: без флага/уже sent — Skip).
|
||||
# Ответ урезаем на задачу, чтобы 6 задач не раздули 6×2000.
|
||||
# GUARD (e-261001074527-f0c1b4): финальный текст тика «бюджет исчерпан» —
|
||||
# стоп-сообщение воркера, а не результат; ночь 20727 им фейково закрылись
|
||||
# 5 L-задач. Такой тик работы не завершал — задачи возвращаются в ready.
|
||||
from .commands import looks_like_budget_stub, requeue_task as _requeue
|
||||
if (text or "") and looks_like_budget_stub(text):
|
||||
for tid in (claimed or []):
|
||||
_requeue(tid, reason="guard: финал тика «бюджет исчерпан» — работа не завершена, задача в ready (e-261001074527)")
|
||||
return text, []
|
||||
for tid in (claimed or []):
|
||||
es.append_event("task", tid, "TaskCompleted", {
|
||||
"result_ref": trace_id, "answer": (text or "")[:2000],
|
||||
|
|
|
|||
115
tests/test_budget_stub_guard.py
Normal file
115
tests/test_budget_stub_guard.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Guard бюджет-стабов (e-261001074527-f0c1b4, hermetic, без LLM/TG).
|
||||
|
||||
Запуск: ./venv/bin/python tests/test_budget_stub_guard.py
|
||||
|
||||
Ночной тик 20727 пачкой закрыл 5 L-задач отпиской воркера «бюджет исчерпан».
|
||||
Гвард: complete_task_with_delivery со стоп-фразой → ValueError + авто-requeue
|
||||
в ready (TaskPromoted), TaskCompleted НЕ пишется. Честные результаты со
|
||||
встречным упоминанием бюджета гвард не трогает.
|
||||
|
||||
Проверяет: looks_like_budget_stub на ночных текстах (стоп-фраза воркера,
|
||||
end_turn-сообщение tool-budget, шаблон ночной доставки) — true; честные
|
||||
результаты («уточнил бюджет», «бюджет рекламной кампании исчерпан») — false;
|
||||
complete со стабом → ValueError + задача вернулась в ready; повторный
|
||||
complete с реальным результатом → done. CLI requeue по guard-сообщению
|
||||
работает (задача не теряется).
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
_DST = "/tmp/simbiot-test-guard.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
|
||||
|
||||
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()
|
||||
|
||||
|
||||
# ── 1. Predicate: ночные шаблоны распознаются ──
|
||||
nightly = [
|
||||
"✅ Готово, тик завершён: бюджет исчерпан",
|
||||
"тик завершён: бюджет шагов исчерпан (30 tool-вызовов). Выше — результат того, что успело выполниться.",
|
||||
"тиковый бюджет шагов исчерпан (30 tool-вызовов): вызов отменён; новые вызовы инструментов отключены до конца тика",
|
||||
]
|
||||
for i, txt in enumerate(nightly):
|
||||
check(f"1.{i}: stub распознан", commands.looks_like_budget_stub(txt), txt[:50])
|
||||
|
||||
# ── 2. Predicate: честные результаты не трогаем ──
|
||||
honest = [
|
||||
"Утверждён оффер: 10 мест × $99.9/мес. Задача закрыта, счётчик на лендинг — следующая задача.",
|
||||
"Проверил рекламный кабинет: бюджет рекламной кампании исчерпан, переключил на резервный.",
|
||||
"Проанализировал траты за месяц; бюджет месяца исчерпан частично — отчёт готов.",
|
||||
"Рефакторинг завершён из 3 шагов. Всё влезло, бюджет не трогали.",
|
||||
]
|
||||
for i, txt in enumerate(honest):
|
||||
check(f"2.{i}: честный не стаб", not commands.looks_like_budget_stub(txt), txt[:50])
|
||||
|
||||
# ── 3. Живая задача: complete со стабом → ValueError + requeue в ready ──
|
||||
tid, _sz = commands.submit_task(title="Тест guard", minutes=30,
|
||||
details="тестовая задача гвирда бюджет-стабов")
|
||||
es.append_event("task", tid, "TaskClaimed", {"level": "hour", "session_id": "t-guard"})
|
||||
es.run_projections()
|
||||
check("3.0: задача running", task_state(tid) == "running")
|
||||
|
||||
stub = "тик завершён: бюджет исчерпан"
|
||||
try:
|
||||
commands.complete_task_with_delivery(tid, stub, level="hour")
|
||||
check("3.1: complete со стабом отклонён", False, "нет ValueError")
|
||||
except ValueError as e:
|
||||
check("3.1: complete со стабом отклонён", "ПЕРЕВЕД" in str(e).upper() or "ready" in str(e), str(e)[:80])
|
||||
|
||||
check("3.2: после guard — задача в ready", task_state(tid) == "ready", task_state(tid))
|
||||
check("3.3: TaskCompleted НЕ записан", "TaskCompleted" not in task_events(tid), task_events(tid))
|
||||
check("3.4: requeue-событие есть", "TaskPromoted" in task_events(tid))
|
||||
|
||||
# ── 4. Тот же тик: повторный complete с реальным результатом → done ──
|
||||
es.append_event("task", tid, "TaskClaimed", {"level": "hour", "session_id": "t-guard2"})
|
||||
tid2, delivered = commands.complete_task_with_delivery(
|
||||
tid, "Реальный результат: расчёт суммы бюджета проекта завершён", level="hour")
|
||||
check("4.0: честный complete после requeue", task_state(tid) == "done", task_state(tid))
|
||||
check("4.1: TaskCompleted записан", task_events(tid).count("TaskCompleted") == 1)
|
||||
|
||||
# ── 5. Честный «бюджет … исчерпан» в реальном результате → TaskCompleted ок ──
|
||||
tid3, _sz3 = commands.submit_task(title="Тест честного", minutes=30,
|
||||
details="тест: результат содержит бюджет-фразу по делу")
|
||||
es.append_event("task", tid3, "TaskClaimed", {"level": "hour", "session_id": "t-guard3"})
|
||||
try:
|
||||
commands.complete_task_with_delivery(
|
||||
tid3, "Проверил кабинет: бюджет рекламной кампании исчерпан, отчёт готов",
|
||||
level="hour")
|
||||
check("5.0: честный результат проходит", task_state(tid3) == "done", task_state(tid3))
|
||||
except ValueError as e:
|
||||
check("5.0: честный результат проходит", False, str(e)[:80])
|
||||
|
||||
print()
|
||||
print("FAILS:", len(fails))
|
||||
sys.exit(1 if fails else 0)
|
||||
Loading…
Reference in a new issue