guard мусорных заголовков задач: 'list'/id без деталей → ValueError (e-261003094628, повтор e-261001082241); тест 14/14
This commit is contained in:
parent
d369351e10
commit
bc59609d27
3 changed files with 115 additions and 1 deletions
6
cli.py
6
cli.py
|
|
@ -156,7 +156,11 @@ def main():
|
|||
elif args.cmd == "wizard":
|
||||
_run_wizard(args)
|
||||
elif args.cmd == "task":
|
||||
tid, size = commands.submit_task(args.title, args.details, args.minutes)
|
||||
try:
|
||||
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:
|
||||
commands.mark_awaiting_delivery(tid, reason="подана из TG с автодоставкой")
|
||||
print(f"OK: задача {tid} [{size}] подана (результат уйдёт в TG при готовности)")
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
валидируется и только потом пишет событие.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
|
@ -67,8 +68,29 @@ def list_escalations(only_open: bool = False):
|
|||
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"):
|
||||
"""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)}"
|
||||
size = size_class(minutes)
|
||||
es.append_event("task", task_id, "TaskSubmitted", {
|
||||
|
|
|
|||
88
tests/test_submit_title_guard.py
Normal file
88
tests/test_submit_title_guard.py
Normal 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: все проверки прошли")
|
||||
Loading…
Reference in a new issue