Terminal / agents /tdd_runner.py
Pulka
feat(telegram): bot UX 100% — WebApp button, /logs, /ping, inline query, _BACK_KB
4cbd6c4 verified
Raw
History Blame Contribute Delete
6.37 kB
"""
tdd_runner.py — S-GAP3: Auto-Testing post-code execution.
Dopo ogni run_python con codice complesso (>=8 righe, def/class presente),
l'agente genera un test minimale, lo esegue, e verifica PASS/FAIL.
Questo chiude il ciclo: codice scritto → codice verificato.
Integrazione: chiamato da unified_loop_tools.py dopo run_python.
Zero overhead su task semplici (direct_response, query, output non-code).
"""
from __future__ import annotations
import asyncio
import logging
import re
from typing import Any
_logger = logging.getLogger("agente_ai.tdd")
_MIN_LINES = 8
_CODE_RE = re.compile(r"\b(def |class |function |return |async def )", re.MULTILINE)
_SUPPORTED = {"python", "py", ""}
def _should_test(code: str, language: str = "python") -> bool:
if language.lower() not in _SUPPORTED:
return False
lines = [l for l in code.splitlines() if l.strip() and not l.strip().startswith("#")]
return len(lines) >= _MIN_LINES and bool(_CODE_RE.search(code))
async def _gen_test(code: str, llm: Any) -> str | None:
try:
from models.role_router import RoleRouter, Role
coder = RoleRouter.get_client(Role.CODER)
except Exception:
coder = llm
prompt = (
"Dato questo codice Python, scrivi un micro-test con assert statements "
"(NON pytest/unittest — solo assert + print). "
"Verifica il comportamento principale. Rispondi SOLO con il codice.\n\n"
f"```python\n{code[:1800]}\n```"
)
try:
raw = await asyncio.wait_for(
coder.chat(
[
{"role": "system", "content": "Sei un tester Python. Solo codice, niente testo."},
{"role": "user", "content": prompt},
],
temperature=0.1, max_tokens=400,
),
timeout=18.0,
)
m = re.search(r"```(?:python)?\n?([\s\S]+?)```", raw)
return m.group(1).strip() if m else raw.strip()
except Exception as e:
_logger.warning("TDD gen failed: %s", e)
return None
async def run_tdd_check(code: str, executor: Any, llm: Any, language: str = "python") -> dict:
"""
Ciclo TDD completo.
Returns: { ran, passed, output, test_code }
"""
out = {"ran": False, "passed": False, "output": "", "test_code": ""}
if not _should_test(code, language):
return out
test_code = await _gen_test(code, llm)
if not test_code:
return out
out["ran"] = True
out["test_code"] = test_code
combined = f"{code}\n\n# === AUTO-TEST S-GAP3 ===\n{test_code}"
try:
r = await asyncio.wait_for(
executor.run_tool("run_python", {"code": combined}),
timeout=28.0,
)
stdout = r.get("output", r.get("stdout", ""))
stderr = r.get("stderr", "")
ec = r.get("exit_code", -1)
passed = ec == 0 and not stderr.strip()
out["passed"] = passed
out["output"] = (stdout or "")[:400]
if stderr:
out["output"] += f"\n[stderr] {stderr[:150]}"
_logger.info("TDD: passed=%s exit=%s", passed, ec)
except asyncio.TimeoutError:
out["output"] = "⏱ timeout 28s"
except Exception as e:
out["output"] = f"⚠️ {e}"
return out
# ── COG-3: TypeScript TDD ─────────────────────────────────────────────────────
_TS_FILE_RE = re.compile(r'\.(ts|tsx)$', re.IGNORECASE)
_TS_CODE_RE = re.compile(
r'(interface |type |const |function |class |export |import |=>|async )',
re.MULTILINE,
)
def _should_test_ts(content: str, path: str = '') -> bool:
"""COG-3: decides whether to type-check a TypeScript file change."""
if path and not _TS_FILE_RE.search(path):
return False
lines = [l for l in content.splitlines() if l.strip() and not l.strip().startswith('//')]
return len(lines) >= 5 and bool(_TS_CODE_RE.search(content))
async def run_tdd_check_ts(
content: str,
path: str,
executor: Any,
on_warn: Any = None,
) -> dict:
"""
COG-3: TypeScript TDD — esegue type_check dopo apply_patch / write_file su .ts/.tsx.
Pipeline:
1. _should_test_ts() — guard rapido, zero overhead su file non-TS
2. executor.run_tool('type_check', {'path': path}) — tsc --noEmit
3. Se errori TypeScript rilevati: emette warning via on_warn callback
Returns: { ran, passed, output, path }
"""
out = {'ran': False, 'passed': False, 'output': '', 'path': path}
if not _should_test_ts(content, path):
return out
if not executor:
return out
out['ran'] = True
try:
r = await asyncio.wait_for(
executor.run_tool('type_check', {'path': path}),
timeout=20.0,
)
raw_out = r.get('output', {})
stdout = raw_out.get('stdout', '') if isinstance(raw_out, dict) else str(raw_out)
stderr = raw_out.get('stderr', '') if isinstance(raw_out, dict) else ''
ec = r.get('exit_code', r.get('output', {}).get('exit_code', -1) if isinstance(r.get('output'), dict) else -1)
has_errors = (
'error TS' in (stdout + stderr)
or (isinstance(ec, int) and ec != 0)
)
out['passed'] = not has_errors
out['output'] = (stdout + stderr)[:500]
if not out['passed'] and on_warn:
ts_errors = [l for l in (stdout + stderr).splitlines() if 'error TS' in l][:5]
warn_msg = f'TypeScript errors in {path}:\n' + '\n'.join(ts_errors)
try:
import asyncio as _asyncio
_v = on_warn({'action': 'tdd_ts', 'status': 'warn', 'output': warn_msg})
if _asyncio.iscoroutine(_v):
await _v
except Exception as _exc:
_logger.debug("[tdd_runner] silenced %s", type(_exc).__name__) # noqa: BLE001
_logger.info('COG-3 TS TDD: path=%s passed=%s ec=%s', path, out['passed'], ec)
except asyncio.TimeoutError:
out['output'] = 'TS type_check timeout 20s'
_logger.warning('COG-3 TS TDD timeout for %s', path)
except Exception as exc:
out['output'] = f'TS TDD error: {exc}'
_logger.warning('COG-3 TS TDD exception: %s', exc)
return out