guard мусорных заголовков задач: 'list'/id без деталей → ValueError (e-261003094628, повтор e-261001082241); тест 14/14

This commit is contained in:
root 2026-10-04 16:03:07 +03:00
parent d369351e10
commit bc59609d27
3 changed files with 115 additions and 1 deletions

4
cli.py
View file

@ -156,7 +156,11 @@ def main():
elif args.cmd == "wizard": elif args.cmd == "wizard":
_run_wizard(args) _run_wizard(args)
elif args.cmd == "task": elif args.cmd == "task":
try:
tid, size = commands.submit_task(args.title, args.details, args.minutes) tid, size = commands.submit_task(args.title, args.details, args.minutes)
except ValueError as e:
print(f"ERROR: {e}")
sys.exit(1)
if args.await_delivery: if args.await_delivery:
commands.mark_awaiting_delivery(tid, reason="подана из TG с автодоставкой") commands.mark_awaiting_delivery(tid, reason="подана из TG с автодоставкой")
print(f"OK: задача {tid} [{size}] подана (результат уйдёт в TG при готовности)") print(f"OK: задача {tid} [{size}] подана (результат уйдёт в TG при готовности)")

View file

@ -4,6 +4,7 @@
валидируется и только потом пишет событие. валидируется и только потом пишет событие.
""" """
import os import os
import re
import secrets import secrets
from datetime import datetime, timezone from datetime import datetime, timezone
@ -67,8 +68,29 @@ def list_escalations(only_open: bool = False):
return out return out
# мусорные заголовки: LLM-тик нижнего уровня звал `cli.py task list` / `cli.py task <id>`,
# а argparse принял субкомандо-подобное слово / id как title → мусорные задачи 'list'
# (e-261001082241, e-261003094628). Ловим на входе command side.
_SUSPECT_TITLES = {"list", "tasks", "status", "events", "help", "complete", "promote", "requeue"}
_TASK_ID_RE = re.compile(r"^t-\d{12}-[0-9a-f]{6}$")
def _validate_task_title(title: str, details: str):
"""Guard: заголовок-субкоманда CLI или чужой id задачи без деталей — это просмотр, не подача."""
if not title or not title.strip():
raise ValueError("пустой заголовок задачи")
t = title.strip()
if not details.strip() and (t.lower() in _SUSPECT_TITLES or _TASK_ID_RE.match(t)):
raise ValueError(
f"подозрительный заголовок {t!r} без деталей: похоже на субкоманду CLI или id задачи "
f"(cli.py task list / cli.py task <id> — это просмотр, не создание). "
"Подача: cli.py task \"заголовок\" -d \"детали\"; доска: cli.py tasks"
)
def submit_task(title: str, details: str = "", minutes: int = 5, source: str = "cli"): def submit_task(title: str, details: str = "", minutes: int = 5, source: str = "cli"):
"""SubmitTask: класс по оценке длительности. Идемпотентно по id.""" """SubmitTask: класс по оценке длительности. Идемпотентно по id."""
_validate_task_title(title, details)
task_id = f"t-{datetime.now(timezone.utc).strftime('%y%m%d%H%M%S')}-{secrets.token_hex(3)}" task_id = f"t-{datetime.now(timezone.utc).strftime('%y%m%d%H%M%S')}-{secrets.token_hex(3)}"
size = size_class(minutes) size = size_class(minutes)
es.append_event("task", task_id, "TaskSubmitted", { es.append_event("task", task_id, "TaskSubmitted", {

View file

@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
"""e-261003094628 (повтор e-261001082241): guard мусорных заголовков задач (hermetic).
Запуск: ./venv/bin/python tests/test_submit_title_guard.py
Первопричина серии 'list': LLM-тик нижнего уровня звал
`cli.py task list` / `cli.py task <id>` (хотел доску/детали задачи), а
`task` принимает позиционный title → argparse молча подсунул слово
'list' / чужой id как заголовок и подал мусорную задачу. Min-уровень
закрыл их как no-op и эскалировал — дважды.
Проверяет: submit_task режет субкомандо-подобные заголовки и чужие id
задач БЕЗ деталей (ValueError), пропускает нормальные подачи, одно слово
с деталями, обычные слова не из стоп-листа; CLI `task list` → exit 1 с
ERROR, а не тихая подача.
"""
import os
import subprocess
import sys
# ── изоляция: своя БД в /tmp (prod не трогаем) ──
_DST = "/tmp/simbiot-test-title-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 commands
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)
# ── 1. Субкомандо-подобные заголовки без деталей — режутся ──
for bad in ("list", "tasks", "status", "events", "help", "LIST", " Complete "):
try:
commands.submit_task(bad, "", minutes=5)
check(f"мусорный заголовок {bad!r} отвергнут", False, "подача прошла")
except ValueError:
check(f"мусорный заголовок {bad!r} отвергнут", True)
# ── 2. Чужой id задачи как заголовок без деталей — режется ──
try:
commands.submit_task("t-261003094008-ed226f", "", minutes=5)
check("id задачи как заголовок отвергнут", False, "подача прошла")
except ValueError:
check("id задачи как заголовок отвергнут", True)
# ── 3. Нормальные подачи — проходят ──
for title, details in (
("Разбор голосового m-973940607 + HTML-страница", "транскрипт в docs/"),
("list", "собрать список всех ключей в реестре"), # слово из стоп-листа, но с деталями
("фикс деплоя", ""),
):
try:
tid, size = commands.submit_task(title, details, minutes=5)
ok = bool(tid) and size == "S"
except ValueError as e:
ok, tid = False, str(e)
check(f"нормальная подача {title[:30]!r} прошла", ok, tid)
# ── 4. CLI: `cli.py task list` — ошибка, а не тихая мусорная задача ──
env = dict(os.environ, SIMBIOT_DB=_DST)
r = subprocess.run(
["./venv/bin/python", "cli.py", "task", "list"],
capture_output=True, text=True, cwd="/root/simbiot-harness", env=env,
)
out = (r.stdout + r.stderr).strip()
check("CLI task list → exit 1", r.returncode == 1, f"rc={r.returncode} out={out!r}")
check("CLI task list → ERROR с подсказкой", "ERROR" in out and "tasks" in out, out)
model = commands.es.run_projections()
check("мусорной задачи 'list' в сторе нет",
not any(t.get("title", "").strip().lower() == "list" and not t.get("details")
for t in model["tasks"].values()))
print()
if fails:
print(f"FAILED: {len(fails)}")
sys.exit(1)
print("OK: все проверки прошли")