Кейс 2026-10-18 (t-261018103940): Коди закрыл задачу итогом «x» до конца
правки tg_inbox, без теста и без коммита. Проверка — в complete (не в промпте):
- незакоммиченные правки harness/ cli.py tests/ scripts/, сделанные тиком
задачи после её клейма → отказ со списком файлов. «Сделанные тиком» — по
трассе: edit/write по пути, shell — путь + признак записи (sed -i, >, tee,
cp/mv/rm, patch, git apply, open(…,'w')); чтение (grep, sed -n) и запуск
(python cli.py …) — не правка; нет трассы — по времени изменения файла.
Иначе чужая незакоммиченная правка того же времени блокировала бы задачу;
- коммиты после клейма (точно по %ct, не по секунде --since) меняют код без
tests/test_*.py → отказ; исключение — complete --no-test «причина»
(причина уходит Alex в итоге);
- изменённые тесты запускаются при закрытии, FAIL/падение → отказ;
- в итог Alex автоматически: «Код: <хеши> · тесты PASS: …»;
- правило — в постановке батча задач; SIMBIOT_CODE_GATE=off выключает.
Сухой прогон на задаче Коди: сейчас — «Код: 70a2015 · тесты PASS:
tests/test_tg_group_ux.py»; в 10:42 был бы отказ (tg_inbox.py не закоммичен).
Тест: test_code_gate 14 PASS; полный набор — без регрессий.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
155 lines
6.8 KiB
Python
155 lines
6.8 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Гейт кода в закрытии задачи (commands.code_change_check) — hermetic, временный git-репо.
|
||
|
||
Запуск: ./venv/bin/python tests/test_code_gate.py
|
||
|
||
Кейс 2026-10-18: Коди закрыл задачу итогом «x» до конца правки tg_inbox, без
|
||
теста и коммита. Проверяет: задача без правок кода — закрывается как раньше;
|
||
незакоммиченная правка после клейма — отказ; старая чужая грязь (до клейма) —
|
||
не мешает; код без теста — отказ, с --no-test «причина» — можно, причина в
|
||
итоге; код + зелёный тест — закрыта, хеш и тест в итоге; красный тест — отказ;
|
||
SIMBIOT_CODE_GATE=off — гейт выключен.
|
||
"""
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
import time
|
||
|
||
sys.path.insert(0, "/root/simbiot-harness")
|
||
_tmp = tempfile.mkdtemp(prefix="simbiot_codegate_")
|
||
os.environ["SIMBIOT_DB"] = os.path.join(_tmp, "harness.db")
|
||
REPO = os.path.join(_tmp, "repo")
|
||
os.environ["SIMBIOT_CODE_REPO"] = REPO
|
||
os.environ.pop("SIMBIOT_CODE_GATE", None)
|
||
|
||
from harness import commands, es
|
||
|
||
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 git(*a):
|
||
subprocess.run(["git", "-C", REPO, "-c", "user.name=t", "-c", "user.email=t@t", *a],
|
||
check=True, capture_output=True)
|
||
|
||
|
||
def write(rel, text, mtime=None):
|
||
p = os.path.join(REPO, rel)
|
||
os.makedirs(os.path.dirname(p), exist_ok=True)
|
||
open(p, "w").write(text)
|
||
if mtime:
|
||
os.utime(p, (mtime, mtime))
|
||
|
||
|
||
os.makedirs(REPO)
|
||
git("init", "-q")
|
||
write("harness/a.py", "x = 1\n")
|
||
write("scripts/old.sh", "echo old\n")
|
||
git("add", ".")
|
||
git("commit", "-q", "-m", "init")
|
||
write("scripts/old.sh", "echo old dirty\n", mtime=time.time() - 3600) # чужая старая грязь
|
||
N = [0]
|
||
|
||
|
||
def task():
|
||
N[0] += 1
|
||
tid = f"t-{N[0]}"
|
||
es.append_event("task", tid, "TaskSubmitted", {"id": tid, "title": "правка", "size": "S"})
|
||
es.append_event("task", tid, "TaskClaimed", {"level": "min"})
|
||
time.sleep(1.1) # git --since с точностью до секунды
|
||
return tid
|
||
|
||
|
||
def done(tid, answer="сделана правка харнеса", **kw):
|
||
try:
|
||
commands.complete_task_with_delivery(tid, answer, level="min", **kw)
|
||
return True, ""
|
||
except ValueError as e:
|
||
return False, str(e)
|
||
|
||
|
||
def answer_of(tid):
|
||
import json
|
||
return [json.loads(e["data"])["answer"] for e in es.get_events(0, 10**6)
|
||
if e["event_type"] == "TaskCompleted" and e["aggregate_id"] == tid][-1]
|
||
|
||
|
||
t = task()
|
||
ok, why = done(t, "ответил в чат, код не трогал")
|
||
check("без правок кода — закрывается как раньше, итог без приписки",
|
||
ok and "Код:" not in answer_of(t), why)
|
||
check("старая чужая грязь (до клейма) не мешает", ok)
|
||
|
||
t = task()
|
||
write("harness/a.py", "x = 2\n")
|
||
ok, why = done(t)
|
||
check("незакоммиченная правка — отказ с файлом", not ok and "harness/a.py" in why, why)
|
||
git("add", "harness/a.py")
|
||
git("commit", "-q", "-m", "a=2")
|
||
ok, why = done(t)
|
||
check("код без теста — отказ", not ok and "без теста" in why, why)
|
||
ok, why = done(t, no_test_reason="правка константы, тест невозможен")
|
||
check("--no-test с причиной — закрыта, причина в итоге",
|
||
ok and "без теста: правка константы" in answer_of(t), why)
|
||
|
||
t = task()
|
||
write("harness/a.py", "x = 3\n")
|
||
write("tests/test_a.py", "print('PASS a ok')\n")
|
||
git("add", ".")
|
||
git("commit", "-q", "-m", "a=3 + test")
|
||
ok, why = done(t)
|
||
ans = answer_of(t) if ok else ""
|
||
check("код + зелёный тест — закрыта", ok, why)
|
||
check("в итоге хеш коммита и тест", "Код: " in ans and "тесты PASS: tests/test_a.py" in ans, ans)
|
||
|
||
t = task()
|
||
write("harness/a.py", "x = 4\n")
|
||
write("tests/test_a.py", "print('FAIL a broken')\nraise SystemExit(1)\n")
|
||
git("add", ".")
|
||
git("commit", "-q", "-m", "a=4 broken test")
|
||
ok, why = done(t)
|
||
check("красный тест — отказ", not ok and "не проходит" in why, why)
|
||
|
||
# чужая незакоммиченная правка того же времени: тик её не трогал — не блокирует
|
||
N[0] += 1
|
||
tid = f"t-{N[0]}"
|
||
es.append_event("task", tid, "TaskSubmitted", {"id": tid, "title": "правка", "size": "S"})
|
||
es.append_event("task", tid, "TaskClaimed", {"level": "min", "session_id": "s-min-777"})
|
||
time.sleep(1.1)
|
||
es.append_event("trace", "min-777", "ToolCalled", {"trace_id": "min-777", "tool": "edit",
|
||
"input": '{"path": "/x/repo/harness/b.py"}'})
|
||
es.append_event("trace", "min-777", "ToolCalled", {"trace_id": "min-777", "tool": "shell",
|
||
"input": '{"command": "./venv/bin/python cli.py tg-reply m-1 \\"ok\\""}'})
|
||
es.append_event("trace", "min-777", "ToolCalled", {"trace_id": "min-777", "tool": "shell",
|
||
"input": '{"command": "grep -n tg_reply cli.py 2>/dev/null | head"}'})
|
||
es.append_event("trace", "min-777", "ToolCalled", {"trace_id": "min-777", "tool": "shell",
|
||
"input": '{"command": "sed -i s/a/b/ scripts/mine.sh"}'})
|
||
write("cli.py", "# чужая правка cli\n") # запускали и читали, но не правили — не своё
|
||
write("scripts/mine.sh", "echo b\n") # правил sed -i — своё
|
||
write("harness/b.py", "y = 1\n") # своя правка (в трассе)
|
||
write("scripts/other.sh", "echo чужое\n") # чужая правка (не в трассе)
|
||
ok, why = done(tid)
|
||
check("своя незакоммиченная правка (в трассе) — отказ", not ok and "harness/b.py" in why, why)
|
||
check("чужая правка того же времени не в списке", "scripts/other.sh" not in why, why)
|
||
check("запуск и grep cli.py — не правка cli.py", "cli.py" not in why, why)
|
||
check("sed -i в shell — своя правка", "scripts/mine.sh" in why, why)
|
||
git("add", "harness/b.py", "scripts/mine.sh")
|
||
write("tests/test_a.py", "print('PASS b ok')\n")
|
||
git("add", "tests/test_a.py")
|
||
git("commit", "-q", "-m", "b + test")
|
||
ok, why = done(tid)
|
||
check("свою закоммитил — закрыта, чужая грязь не мешает", ok, why)
|
||
|
||
os.environ["SIMBIOT_CODE_GATE"] = "off"
|
||
ok, why = done(t)
|
||
check("SIMBIOT_CODE_GATE=off — гейт выключен", ok, why)
|
||
|
||
print(f"\n{'OK' if not fails else 'FAILED'}: {len(fails)} fail(s)")
|
||
sys.exit(1 if fails else 0)
|