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