test: служебный текст inbox не порождает задачу-клон (t-261004160532-0fb500) — регресс обоих фиксов (e-261003125707 'list'=доска, e-261004134613 task<ID>=чтение): парсер, CLI-гварды (счёт задач не растёт, несуществующий ID → exit 2), инбокс-уровень ('list'/ID в тексте → команда/речь, без клонов); 4 теста pytest+plain
This commit is contained in:
parent
5ca2bbe96c
commit
20e655e4ec
1 changed files with 159 additions and 0 deletions
159
tests/test_no_clone_inbox.py
Normal file
159
tests/test_no_clone_inbox.py
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""t-261004160532: служебный текст inbox не порождает задачу-клон.
|
||||
|
||||
Паттерн само-клонирования чинился дважды:
|
||||
e-261003125707 — 'list' из чата → мин-агент звал `cli.py task list`,
|
||||
создавался клон «list»; фикс: 'list'/'tasks'/'task list' = доска;
|
||||
e-261004134613 — агент звал `cli.py task t-...` для ЧТЕНИЯ задачи,
|
||||
создавался клон с title=ID; фикс: title=ID → показать / exit 2.
|
||||
|
||||
Регресс проверяет весь путь: парсер команд ('list' — команда, а не речь),
|
||||
CLI-гварды (list/ID не создают задач), и инбокс-уровень (служебные тексты
|
||||
распознаются как команды → answered, LLM их не плодит).
|
||||
|
||||
Запуск: ./venv/bin/python tests/test_no_clone_inbox.py | pytest tests/test_no_clone_inbox.py
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# ── изоляция: свежая БД в /tmp, прод не нужен ──
|
||||
_DST = "/tmp/simbiot-test-no-clone.db"
|
||||
for p in (_DST, _DST + "-wal", _DST + "-shm"):
|
||||
if os.path.exists(p):
|
||||
os.unlink(p)
|
||||
sys.path.insert(0, "/root/simbiot-harness")
|
||||
os.environ["SIMBIOT_DB"] = _DST
|
||||
|
||||
from harness import es, tg_commands, tg_inbox
|
||||
|
||||
fails = []
|
||||
_REPO = "/root/simbiot-harness"
|
||||
_PY = os.path.join(_REPO, "venv", "bin", "python")
|
||||
|
||||
|
||||
def check(name, cond, extra=""):
|
||||
print(("PASS" if cond else "FAIL"), name, ("| " + extra) if not cond else "", flush=True)
|
||||
if not cond:
|
||||
fails.append(name)
|
||||
raise AssertionError(f"{name} {extra}")
|
||||
|
||||
|
||||
es.init_db()
|
||||
|
||||
|
||||
def _cli(*args):
|
||||
"""cli.py в изолированной БД (env наследуется)."""
|
||||
r = subprocess.run([_PY, os.path.join(_REPO, "cli.py")] + list(args),
|
||||
capture_output=True, text=True, timeout=60,
|
||||
cwd=_REPO)
|
||||
return r.returncode, (r.stdout or "") + (r.stderr or "")
|
||||
|
||||
|
||||
def _task_count():
|
||||
return len((es.run_projections().get("tasks") or {}))
|
||||
|
||||
|
||||
# ── 1. парсер: служебные слова — команды, а не речь для LLM ──────────────────
|
||||
|
||||
def test_parse_service_words():
|
||||
P = tg_commands.parse
|
||||
check("parse 'list' → команда задачи (e-261001124837)",
|
||||
P("list") == "задачи", f"{P('list')!r}")
|
||||
check("parse 'tasks' → команда", P("tasks") == "задачи")
|
||||
check("parse 'LIST' регистронезависимо", P("LIST") == "задачи")
|
||||
check("parse '/задачи' → команда", P("/задачи") == "задачи")
|
||||
check("parse 'list please' — НЕ команда (речь, но не служебный текст)",
|
||||
P("list please") is None)
|
||||
# ID задачи в тексте — тоже служебный шум, не команда и не «задача»
|
||||
check("parse 't-261004160531-ed25ed' — не команда", P("t-261004160531-ed25ed") is None)
|
||||
|
||||
|
||||
# ── 2. CLI-гварды: list / ID не создают задач ────────────────────────────────
|
||||
|
||||
def test_cli_list_is_board_not_clone():
|
||||
"""`cli.py task list` → доска; число задач не растёт (e-261003125707)."""
|
||||
from harness import commands
|
||||
commands.submit_task("реальная работа", "", minutes=5)
|
||||
before = _task_count()
|
||||
check("задача-незабудка создана", before >= 1)
|
||||
for variant in ("list", "tasks", "task list"):
|
||||
rc, out = _cli("task", variant)
|
||||
check(f"cli task '{variant}': rc=0", rc == 0, f"rc={rc} {out[:120]}")
|
||||
check(f"cli task '{variant}': показывает доску",
|
||||
"реальная работа" in out, out[:150])
|
||||
after = _task_count()
|
||||
check("cli task list/tasks/task list: клонов НЕТ (счёт задач тот же)",
|
||||
after == before, f"было {before}, стало {after}")
|
||||
|
||||
|
||||
def test_cli_task_id_reads_not_clones():
|
||||
"""`cli.py task <id>` → чтение; несуществующий → exit 2 без клона."""
|
||||
from harness import commands
|
||||
real_id, _ = commands.submit_task("целевая задача для чтения", "", minutes=5)
|
||||
before = _task_count()
|
||||
rc, out = _cli("task", real_id)
|
||||
check("cli task <существующий id>: rc=0", rc == 0, f"rc={rc}")
|
||||
check("cli task <id>: показывает задачу", "целевая задача для чтения" in out, out[:200])
|
||||
fake = "t-269999999999-deadbe"
|
||||
rc2, out2 = _cli("task", fake)
|
||||
check("cli task <несуществующий id>: exit 2", rc2 == 2, f"rc={rc2}")
|
||||
check("cli task <несуществующий id>: отказ в создании клона",
|
||||
"не найдена" in out2 and "запрещено" in out2, out2[:200])
|
||||
after = _task_count()
|
||||
check("cli task <id>: клонов НЕТ (e-261004134613)",
|
||||
after == before, f"было {before}, стало {after}")
|
||||
|
||||
|
||||
# ── 3. инбокс-уровень: служебный текст распознан и отвечен без клона ────────
|
||||
|
||||
def test_inbox_service_text_no_task():
|
||||
"""'list' и ID задачи в inbox → ответ командой, задач не прибавилось."""
|
||||
before = _task_count()
|
||||
# в реальном стриме их кладёт поллер; кладём так же, как он
|
||||
tg_inbox.receive_message({"update_id": 991001, "message": {
|
||||
"message_id": 991001 % 100000, "date": 1780000100,
|
||||
"chat": {"id": 42}, "from": {"id": 1, "username": "alex", "first_name": "Alex"},
|
||||
"text": "list"}})
|
||||
tg_inbox.receive_message({"update_id": 991002, "message": {
|
||||
"message_id": 991002 % 100000, "date": 1780000101,
|
||||
"chat": {"id": 42}, "from": {"id": 1, "username": "alex", "first_name": "Alex"},
|
||||
"text": "t-261004160531-ed25ed что там по срокам?"}})
|
||||
model = es.run_projections()
|
||||
inbox = model.get("inbox") or {}
|
||||
check("инбокс: оба служебных текста приняты",
|
||||
"m-991001" in inbox and "m-991002" in inbox)
|
||||
check("инбокс: 'list' распознан как команда ('задачи')",
|
||||
tg_commands.parse(inbox["m-991001"]["text"]) == "задачи")
|
||||
check("инбокс: ID в фразе — не команда (речь, уходит LLM)",
|
||||
tg_commands.parse(inbox["m-991002"]["text"]) is None)
|
||||
# minute-level фильтр: команды не показываются мин-LLM как «сырые» входящие
|
||||
from harness.scheduler import build_context, LEVELS
|
||||
fake = {"inbox": dict(inbox), "tasks": dict(model.get("tasks") or {}),
|
||||
"escalations": {}, "policy": {}}
|
||||
ctx = build_context(LEVELS["min"], fake)
|
||||
check("build_context(min): 'list' не показан как открытое входящее",
|
||||
"m-991001" not in ctx or "list" not in _line(ctx, "m-991001"))
|
||||
check("после обработки служебных текстов задач-клонов НЕТ",
|
||||
_task_count() == before, f"было {before}, стало {_task_count()}")
|
||||
|
||||
|
||||
def _line(ctx, marker):
|
||||
for ln in ctx.splitlines():
|
||||
if marker in ln:
|
||||
return ln
|
||||
return ""
|
||||
|
||||
|
||||
# ────────────────────────────── plain-run режим ──────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
|
||||
for t in tests:
|
||||
try:
|
||||
t()
|
||||
except AssertionError:
|
||||
pass
|
||||
print(f"\n{'='*60}")
|
||||
print(f"{'ALL OK' if not fails else 'FAILED: ' + ', '.join(fails)} ({len(tests)} тестов)")
|
||||
sys.exit(1 if fails else 0)
|
||||
Loading…
Reference in a new issue