Spaces:
Running
Running
File size: 6,366 Bytes
28a08e7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | """
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
|