sync: 179 file da Baida98/AI@31efd48d (2026-08-25 10:47 UTC) [deploy-all]

#75
by Baida07 - opened
agents/file_conversion.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Conversioni tabellari deterministiche per allegati inclusi nei goal dell'agente.
2
+
3
+ Il modulo interpreta esclusivamente blocchi CSV esplicitamente allegati dal client. Non apre
4
+ path arbitrari, non esegue istruzioni contenute nel file e non invoca LLM.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import csv
9
+ import io
10
+ import json
11
+ import re
12
+ from dataclasses import dataclass
13
+ from typing import Any
14
+
15
+ _ATTACHMENT_RE = re.compile(
16
+ r"###\s*📎\s*(?P<name>[^\n`]+?\.csv)\s*\([^\n]*\)\s*```\s*(?P<body>[\s\S]*?)```",
17
+ re.IGNORECASE,
18
+ )
19
+ _TARGET_RE = re.compile(
20
+ r"\b(?:chiamat[oa]|nome|denominat[oa]|come)\s+['`\"]?(?P<name>[\w.-]+\.json)\b",
21
+ re.IGNORECASE,
22
+ )
23
+ _CONVERSION_RE = re.compile(
24
+ r"\b(?:converti|trasforma|conversione|convert|transform)\b[\s\S]{0,180}\b(?:csv|json)\b",
25
+ re.IGNORECASE,
26
+ )
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class CsvJsonConversion:
31
+ source_name: str
32
+ target_name: str
33
+ content: str
34
+ row_count: int
35
+
36
+
37
+ def _coerce_scalar(value: str) -> Any:
38
+ value = value.strip()
39
+ if re.fullmatch(r"-?(?:0|[1-9]\d*)", value):
40
+ return int(value)
41
+ if re.fullmatch(r"-?(?:0|[1-9]\d*)\.\d+", value):
42
+ return float(value)
43
+ return value
44
+
45
+
46
+ def _csv_body(raw_body: str) -> str:
47
+ lines = raw_body.replace("\r\n", "\n").replace("\r", "\n").split("\n")
48
+ while lines and (not lines[0].strip() or lines[0].lstrip().startswith("## Foglio:")):
49
+ lines.pop(0)
50
+ return "\n".join(lines).strip()
51
+
52
+
53
+ def convert_csv_attachment_to_json(goal: str) -> CsvJsonConversion | None:
54
+ """Converte il primo allegato CSV esplicitamente serializzato in JSON.
55
+
56
+ Il ritorno è ``None`` quando il goal non richiede una conversione CSV→JSON completa,
57
+ così il resto del loop conserva il comportamento esistente.
58
+ """
59
+ if not _CONVERSION_RE.search(goal):
60
+ return None
61
+ attachment = _ATTACHMENT_RE.search(goal)
62
+ if not attachment:
63
+ return None
64
+ target = _TARGET_RE.search(goal)
65
+ if not target:
66
+ return None
67
+
68
+ csv_body = _csv_body(attachment.group("body"))
69
+ if not csv_body:
70
+ return None
71
+ try:
72
+ reader = csv.DictReader(io.StringIO(csv_body))
73
+ if not reader.fieldnames or any(not header or not header.strip() for header in reader.fieldnames):
74
+ return None
75
+ rows = [
76
+ {str(key).strip(): _coerce_scalar(value or "") for key, value in row.items()}
77
+ for row in reader
78
+ ]
79
+ except (csv.Error, UnicodeError):
80
+ return None
81
+
82
+ return CsvJsonConversion(
83
+ source_name=attachment.group("name").strip(),
84
+ target_name=target.group("name").strip(),
85
+ content=json.dumps(rows, ensure_ascii=False, indent=2) + "\n",
86
+ row_count=len(rows),
87
+ )
agents/goal_verifier.py CHANGED
@@ -178,7 +178,18 @@ class GoalVerifier:
178
  r"flask|fastapi|django|express|nestjs|rails|laravel|"
179
  r"node|deno|bun|docker|dockerfile|nginx|github.*action|workflow\.yml|"
180
  r"database|schema|migration|model|table|index|query|"
181
- r"test|spec|fixture|mock|e2e|unit.*test|integration.*test)\b",
 
 
 
 
 
 
 
 
 
 
 
182
  re.IGNORECASE,
183
  )
184
 
@@ -193,7 +204,13 @@ class GoalVerifier:
193
 
194
  @classmethod
195
  def is_code_goal(cls, goal: str) -> bool:
196
- return bool(cls._CODE_RE.search(goal[:500]))
 
 
 
 
 
 
197
 
198
  @classmethod
199
  def adaptive_threshold(cls, goal: str) -> float:
 
178
  r"flask|fastapi|django|express|nestjs|rails|laravel|"
179
  r"node|deno|bun|docker|dockerfile|nginx|github.*action|workflow\.yml|"
180
  r"database|schema|migration|model|table|index|query|"
181
+ r"test|spec|fixture|mock|unit.*test|integration.*test)\b",
182
+ re.IGNORECASE,
183
+ )
184
+
185
+ _FILE_CONVERSION_RE = re.compile(
186
+ r"\b(?:converti|trasforma|conversione|convert|transform)\b[\s\S]{0,240}"
187
+ r"\b(?:csv|tsv|xlsx|xls|json|pdf|txt|markdown|md|docx)\b",
188
+ re.IGNORECASE,
189
+ )
190
+ _IMPLEMENTATION_CONTEXT_RE = re.compile(
191
+ r"\b(?:codice|script|funzione|function|class|componente|component|api|endpoint|"
192
+ r"typescript|javascript|python|react|backend|frontend|test\s+unit|test\s+e2e)\b",
193
  re.IGNORECASE,
194
  )
195
 
 
204
 
205
  @classmethod
206
  def is_code_goal(cls, goal: str) -> bool:
207
+ # Gli allegati sono serializzati dopo questo separatore: non devono trasformare
208
+ # una semplice lettura/conversione in un task di sviluppo da riparare.
209
+ user_goal = goal.split("--- **File allegati:**", 1)[0][:500]
210
+ if (cls._FILE_CONVERSION_RE.search(user_goal)
211
+ and not cls._IMPLEMENTATION_CONTEXT_RE.search(user_goal)):
212
+ return False
213
+ return bool(cls._CODE_RE.search(user_goal))
214
 
215
  @classmethod
216
  def adaptive_threshold(cls, goal: str) -> float:
agents/unified_loop.py CHANGED
@@ -4056,13 +4056,20 @@ class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin, LLMSelectionMixin,
4056
  "loop": 0, "action": "direct_tools", "status": "done",
4057
  "tools_fired": _tools_count,
4058
  }))
4059
- await self._transition_state(state, AgentState.THINKING, on_step)
4060
- _r = await _finish(await self._run_fallback(
4061
- state, on_step,
4062
- preloaded_tool_results=direct_results or None,
4063
- preloaded_tool_exec_successes=_exec_success,
4064
- preloaded_tool_exec_errors=_exec_errors,
4065
- ))
 
 
 
 
 
 
 
4066
  _r["timing_ms"] = int((_time.monotonic() - _t_run) * 1000)
4067
  try:
4068
  from api.state import record_timing as _rtc_ttr
@@ -4101,13 +4108,20 @@ class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin, LLMSelectionMixin,
4101
  "loop": 0, "action": "direct_tools", "status": "done",
4102
  "tools_fired": _tools_count,
4103
  }))
4104
- await self._transition_state(state, AgentState.THINKING, on_step)
4105
- _r = await _finish(await self._run_fallback(
4106
- state, on_step,
4107
- preloaded_tool_results=direct_results,
4108
- preloaded_tool_exec_successes=_exec_success,
4109
- preloaded_tool_exec_errors=_exec_errors,
4110
- ))
 
 
 
 
 
 
 
4111
  _r["timing_ms"] = int((_time.monotonic() - _t_run) * 1000)
4112
  try:
4113
  from api.state import record_timing as _rtc_ttr
 
4056
  "loop": 0, "action": "direct_tools", "status": "done",
4057
  "tools_fired": _tools_count,
4058
  }))
4059
+ if direct_results.startswith("[DIRECT_TERMINAL]\n"):
4060
+ _r = await _finish({
4061
+ "success": _exec_success > 0,
4062
+ "output": direct_results.removeprefix("[DIRECT_TERMINAL]\n"),
4063
+ "steps": state.steps,
4064
+ })
4065
+ else:
4066
+ await self._transition_state(state, AgentState.THINKING, on_step)
4067
+ _r = await _finish(await self._run_fallback(
4068
+ state, on_step,
4069
+ preloaded_tool_results=direct_results or None,
4070
+ preloaded_tool_exec_successes=_exec_success,
4071
+ preloaded_tool_exec_errors=_exec_errors,
4072
+ ))
4073
  _r["timing_ms"] = int((_time.monotonic() - _t_run) * 1000)
4074
  try:
4075
  from api.state import record_timing as _rtc_ttr
 
4108
  "loop": 0, "action": "direct_tools", "status": "done",
4109
  "tools_fired": _tools_count,
4110
  }))
4111
+ if direct_results.startswith("[DIRECT_TERMINAL]\n"):
4112
+ _r = await _finish({
4113
+ "success": _exec_success > 0,
4114
+ "output": direct_results.removeprefix("[DIRECT_TERMINAL]\n"),
4115
+ "steps": state.steps,
4116
+ })
4117
+ else:
4118
+ await self._transition_state(state, AgentState.THINKING, on_step)
4119
+ _r = await _finish(await self._run_fallback(
4120
+ state, on_step,
4121
+ preloaded_tool_results=direct_results,
4122
+ preloaded_tool_exec_successes=_exec_success,
4123
+ preloaded_tool_exec_errors=_exec_errors,
4124
+ ))
4125
  _r["timing_ms"] = int((_time.monotonic() - _t_run) * 1000)
4126
  try:
4127
  from api.state import record_timing as _rtc_ttr
agents/unified_loop_tools.py CHANGED
@@ -24,6 +24,7 @@ _logger = logging.getLogger("agents.unified_loop_tools")
24
  # StepCallback centralizzato in unified_loop_types.py (P20-TD1 Fase 1)
25
  # S-FIX-IMPORT: aggiunto _maybe_await mancante che causava crash nel tool layer
26
  from agents.unified_loop_types import StepCallback, _maybe_await
 
27
  class DirectToolsMixin:
28
  # ── Direct tool execution (S193) ─────────────────────────────────────────
29
  # Chiama TOOL_REGISTRY direttamente, senza smolagents, senza LLM per routing.
@@ -293,6 +294,52 @@ class DirectToolsMixin:
293
  return f"[web_search: timeout {TOOL_TIMEOUT}s]"
294
  except Exception as exc:
295
  return f"[web_search: errore — {str(exc)[:300]}]"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  async def _t_generate_image() -> str | None:
297
  if not self._IMAGE_INTENT_RE.search(goal):
298
  return None
@@ -317,8 +364,10 @@ class DirectToolsMixin:
317
  img_url = r.get("url", "")
318
  if img_url:
319
  return (
320
- f"[IMMAGINE AI GENERATA]\n"
321
- f"URL: {img_url}\n"
 
 
322
  f"Prompt usato: {r.get('prompt', _img_prompt)[:200]}\n"
323
  f"Dimensioni: {r.get('width')}x{r.get('height')} px"
324
  )
@@ -501,7 +550,20 @@ class DirectToolsMixin:
501
  return "[python_analyze: timeout]"
502
  except Exception as _exc:
503
  return f"[python_analyze: errore — {str(_exc)[:200]}]"
504
- # Esecuzione parallela
 
 
 
 
 
 
 
 
 
 
 
 
 
505
  _sem = asyncio.Semaphore(3)
506
  async def _sem_wrap(coro):
507
  if coro is None: return None
@@ -511,7 +573,6 @@ class DirectToolsMixin:
511
  _sem_wrap(_t_read_page()),
512
  _sem_wrap(_t_calculate()),
513
  _sem_wrap(_t_web_search()),
514
- _sem_wrap(_t_generate_image()),
515
  _sem_wrap(_t_run_python()),
516
  _sem_wrap(_t_web_research()),
517
  _sem_wrap(_t_directory_tree()),
@@ -527,7 +588,7 @@ class DirectToolsMixin:
527
  # S428 Sprint1-Fix1: Tool Success Contract
528
  _REAL_DATA_PREFIXES = (
529
  "[RICERCA WEB REALE", "[METEO REALE", "[PAGINA REALE", "[CALCOLO REALE",
530
- "[IMMAGINE AI GENERATA", "[CODICE PYTHON ESEGUITO", "[RICERCA APPROFONDITA REALE",
531
  "[STRUTTURA PROGETTO REALE", "[RICERCA FILE REALE", "[NOTIZIE REALI",
532
  "[STATO GIT REALE", "[ANALISI PYTHON REALE"
533
  )
 
24
  # StepCallback centralizzato in unified_loop_types.py (P20-TD1 Fase 1)
25
  # S-FIX-IMPORT: aggiunto _maybe_await mancante che causava crash nel tool layer
26
  from agents.unified_loop_types import StepCallback, _maybe_await
27
+ from agents.file_conversion import convert_csv_attachment_to_json
28
  class DirectToolsMixin:
29
  # ── Direct tool execution (S193) ─────────────────────────────────────────
30
  # Chiama TOOL_REGISTRY direttamente, senza smolagents, senza LLM per routing.
 
294
  return f"[web_search: timeout {TOOL_TIMEOUT}s]"
295
  except Exception as exc:
296
  return f"[web_search: errore — {str(exc)[:300]}]"
297
+ async def _t_convert_csv_attachment() -> str | None:
298
+ conversion = convert_csv_attachment_to_json(goal)
299
+ if conversion is None:
300
+ return None
301
+ if not _gov_check("convert_csv_to_json", conversion.target_name):
302
+ return None
303
+ try:
304
+ if on_step:
305
+ await _maybe_await(on_step({"action": "tool_start", "status": "running",
306
+ "title": "Conversione CSV in JSON",
307
+ "explanation": f"Converto {conversion.source_name} in {conversion.target_name}…"}))
308
+ _t0 = asyncio.get_event_loop().time()
309
+ written = await asyncio.wait_for(
310
+ TOOL_REGISTRY["write_file"]["_fn"](
311
+ path=conversion.target_name,
312
+ content=conversion.content,
313
+ ),
314
+ timeout=TOOL_TIMEOUT,
315
+ )
316
+ if not written.get("success"):
317
+ return f"[convert_csv_to_json: errore — {str(written.get('error', 'scrittura non riuscita'))[:300]}]"
318
+ if on_step:
319
+ await _maybe_await(on_step({
320
+ "action": "file_written",
321
+ "status": "done",
322
+ "path": conversion.target_name,
323
+ "content": conversion.content,
324
+ "title": "File JSON creato",
325
+ "explanation": f"Creato {conversion.target_name} con {conversion.row_count} record.",
326
+ }))
327
+ try:
328
+ from api.state import record_timing as _rtc
329
+ _rtc("tool_call", (asyncio.get_event_loop().time() - _t0) * 1000)
330
+ except Exception as _e:
331
+ _logger.debug('[timing/record_timing] %s', _e)
332
+ return (
333
+ "[DIRECT_TERMINAL]\n"
334
+ f"E2E_CONVERSION_OK: creato `{conversion.target_name}` da `{conversion.source_name}` "
335
+ f"con {conversion.row_count} record.\n\n"
336
+ f"Percorso workspace: `{conversion.target_name}`."
337
+ )
338
+ except asyncio.TimeoutError:
339
+ return "[convert_csv_to_json: timeout]"
340
+ except Exception as exc:
341
+ return f"[convert_csv_to_json: errore — {str(exc)[:300]}]"
342
+
343
  async def _t_generate_image() -> str | None:
344
  if not self._IMAGE_INTENT_RE.search(goal):
345
  return None
 
364
  img_url = r.get("url", "")
365
  if img_url:
366
  return (
367
+ "[DIRECT_TERMINAL]\n"
368
+ f"![Immagine generata]({img_url})\n\n"
369
+ "E2E_IMAGE_OK: immagine generata e visualizzata qui sopra. "
370
+ f"[Apri o scarica l’immagine]({img_url}).\n\n"
371
  f"Prompt usato: {r.get('prompt', _img_prompt)[:200]}\n"
372
  f"Dimensioni: {r.get('width')}x{r.get('height')} px"
373
  )
 
550
  return "[python_analyze: timeout]"
551
  except Exception as _exc:
552
  return f"[python_analyze: errore — {str(_exc)[:200]}]"
553
+ # Conversione e immagine sono artefatti terminali: eseguirli prima del
554
+ # fan-out evita risultati accessori e, soprattutto, una successiva chiamata LLM.
555
+ _terminal_conversion = await _t_convert_csv_attachment()
556
+ if _terminal_conversion is not None:
557
+ return (_terminal_conversion, 1,
558
+ int(_terminal_conversion.startswith("[DIRECT_TERMINAL]")),
559
+ int(": errore" in _terminal_conversion or ": timeout" in _terminal_conversion))
560
+ _terminal_image = await _t_generate_image()
561
+ if _terminal_image is not None:
562
+ return (_terminal_image, 1,
563
+ int(_terminal_image.startswith("[DIRECT_TERMINAL]")),
564
+ int(": errore" in _terminal_image or ": timeout" in _terminal_image))
565
+
566
+ # Esecuzione parallela per i tool non terminali.
567
  _sem = asyncio.Semaphore(3)
568
  async def _sem_wrap(coro):
569
  if coro is None: return None
 
573
  _sem_wrap(_t_read_page()),
574
  _sem_wrap(_t_calculate()),
575
  _sem_wrap(_t_web_search()),
 
576
  _sem_wrap(_t_run_python()),
577
  _sem_wrap(_t_web_research()),
578
  _sem_wrap(_t_directory_tree()),
 
588
  # S428 Sprint1-Fix1: Tool Success Contract
589
  _REAL_DATA_PREFIXES = (
590
  "[RICERCA WEB REALE", "[METEO REALE", "[PAGINA REALE", "[CALCOLO REALE",
591
+ "[IMMAGINE AI GENERATA", "[DIRECT_TERMINAL]", "[CODICE PYTHON ESEGUITO", "[RICERCA APPROFONDITA REALE",
592
  "[STRUTTURA PROGETTO REALE", "[RICERCA FILE REALE", "[NOTIZIE REALI",
593
  "[STATO GIT REALE", "[ANALISI PYTHON REALE"
594
  )
tests/test_direct_file_conversion.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import sys
3
+ import types
4
+
5
+
6
+ def _install_tool_stubs(writes, image_url="https://example.test/generated.png"):
7
+ tools_pkg = types.ModuleType("tools")
8
+ registry = types.ModuleType("tools.registry")
9
+
10
+ async def write_file(path, content):
11
+ writes[path] = content
12
+ return {"success": True, "path": path}
13
+
14
+ async def generate_image(prompt, width=512, height=512):
15
+ return {"url": image_url, "prompt": prompt, "width": width, "height": height}
16
+
17
+ registry.TOOL_REGISTRY = {
18
+ "write_file": {"_fn": write_file},
19
+ "generate_image": {"_fn": generate_image},
20
+ }
21
+ sys.modules["tools"] = tools_pkg
22
+ sys.modules["tools.registry"] = registry
23
+
24
+ api_pkg = types.ModuleType("api")
25
+ speculative = types.ModuleType("api.speculative")
26
+ speculative.get_speculative_result = lambda *_args, **_kwargs: None
27
+ sys.modules["api"] = api_pkg
28
+ sys.modules["api.speculative"] = speculative
29
+
30
+
31
+ def test_direct_csv_conversion_writes_vfs_content_and_returns_terminal_output():
32
+ writes = {}
33
+ _install_tool_stubs(writes)
34
+ from agents.unified_loop_tools import DirectToolsMixin
35
+
36
+ class Harness(DirectToolsMixin):
37
+ def _max_tokens_for_goal(self, _goal):
38
+ return 4096
39
+
40
+ events = []
41
+
42
+ async def on_step(event):
43
+ events.append(event)
44
+
45
+ goal = """Converti e2e_metrics.csv in un file chiamato e2e_metrics.json.
46
+ --- **File allegati:**
47
+ ### 📎 e2e_metrics.csv (excel, 66B)
48
+ ```
49
+ mese,richieste,successi
50
+ 2026-01,12,11
51
+ 2026-02,15,14
52
+ ```
53
+ """
54
+ output, called, succeeded, errors = asyncio.run(
55
+ Harness()._run_direct_tools(goal, on_step=on_step)
56
+ )
57
+
58
+ assert output.startswith("[DIRECT_TERMINAL]\nE2E_CONVERSION_OK")
59
+ assert called == 1
60
+ assert succeeded == 1
61
+ assert errors == 0
62
+ assert '"richieste": 15' in writes["e2e_metrics.json"]
63
+ assert any(event.get("action") == "file_written" for event in events)
64
+
65
+
66
+ def test_direct_image_returns_renderable_terminal_markdown_without_llm():
67
+ writes = {}
68
+ _install_tool_stubs(writes)
69
+ from agents.unified_loop_tools import DirectToolsMixin
70
+
71
+ class Harness(DirectToolsMixin):
72
+ def _max_tokens_for_goal(self, _goal):
73
+ return 4096
74
+
75
+ output, called, succeeded, errors = asyncio.run(
76
+ Harness()._run_direct_tools(
77
+ "Genera un’immagine quadrata di un aeroplanino arancione su fondo blu notte."
78
+ )
79
+ )
80
+
81
+ assert output.startswith("[DIRECT_TERMINAL]\n![Immagine generata](https://example.test/generated.png)")
82
+ assert "E2E_IMAGE_OK" in output
83
+ assert called == 1
84
+ assert succeeded == 1
85
+ assert errors == 0
tests/test_file_conversion.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from agents.file_conversion import convert_csv_attachment_to_json
2
+ from agents.goal_verifier import GoalVerifier
3
+
4
+
5
+ def _goal(target: str = "e2e_metrics.json") -> str:
6
+ return f"""Converti il file allegato e2e_metrics.csv in un nuovo file workspace chiamato {target}.
7
+ Il JSON deve preservare tutti i valori del CSV.
8
+ --- **File allegati:**
9
+ ### 📎 e2e_metrics.csv (excel, 66B)
10
+ ```
11
+ ## Foglio: Sheet1
12
+ mese,richieste,successi
13
+ 1/1/26,12,11
14
+ 2/1/26,15,14
15
+ 3/1/26,18,17
16
+ ```
17
+ """
18
+
19
+
20
+ def test_convert_csv_attachment_to_json_preserves_rows_and_scalars():
21
+ result = convert_csv_attachment_to_json(_goal())
22
+
23
+ assert result is not None
24
+ assert result.source_name == "e2e_metrics.csv"
25
+ assert result.target_name == "e2e_metrics.json"
26
+ assert result.row_count == 3
27
+ assert '"mese": "1/1/26"' in result.content
28
+ assert '"richieste": 18' in result.content
29
+ assert '"successi": 17' in result.content
30
+
31
+
32
+ def test_convert_csv_attachment_requires_explicit_json_target():
33
+ assert convert_csv_attachment_to_json(_goal(target="output.txt")) is None
34
+
35
+
36
+ def test_file_conversion_is_not_a_code_goal_even_with_e2e_marker():
37
+ assert GoalVerifier.is_code_goal(_goal()) is False
38
+
39
+
40
+ def test_conversion_with_explicit_code_context_remains_code_goal():
41
+ assert GoalVerifier.is_code_goal(
42
+ "Converti questo JSON in una funzione Python e scrivi il codice completo."
43
+ ) is True
44
+
45
+
46
+ def test_e2e_marker_alone_does_not_turn_file_reading_into_a_code_goal():
47
+ assert GoalVerifier.is_code_goal(
48
+ "Leggi il file allegato e restituisci il marker E2E_FILE_READ_OK."
49
+ ) is False
50
+ assert GoalVerifier.is_code_goal(
51
+ "Aggiungi un test e2e per l’endpoint API di conversione file."
52
+ ) is True