snapcontext 3.3.0__tar.gz → 3.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. {snapcontext-3.3.0/snapcontext.egg-info → snapcontext-3.4.0}/PKG-INFO +39 -2
  2. {snapcontext-3.3.0 → snapcontext-3.4.0}/README.md +38 -1
  3. {snapcontext-3.3.0 → snapcontext-3.4.0}/agentes.py +173 -44
  4. {snapcontext-3.3.0 → snapcontext-3.4.0}/orquestador.py +3 -0
  5. {snapcontext-3.3.0 → snapcontext-3.4.0}/pyproject.toml +1 -1
  6. {snapcontext-3.3.0 → snapcontext-3.4.0/snapcontext.egg-info}/PKG-INFO +39 -2
  7. {snapcontext-3.3.0 → snapcontext-3.4.0}/snapcontext.egg-info/SOURCES.txt +1 -0
  8. {snapcontext-3.3.0 → snapcontext-3.4.0}/snapcontext.py +149 -3
  9. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_auto_017.py +1 -1
  10. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_diagnostico_310.py +1 -1
  11. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_diff_editor_210.py +1 -1
  12. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_editor_330.py +1 -1
  13. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_editor_ast_220.py +1 -1
  14. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_editor_propio_200.py +1 -1
  15. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_exe_150.py +1 -1
  16. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_jetbrains_170.py +1 -1
  17. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_mcp_014.py +1 -1
  18. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_mcp_140.py +1 -1
  19. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_memoria_015.py +1 -1
  20. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_memoria_300.py +1 -1
  21. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_permisos_013.py +1 -1
  22. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_plan_012.py +1 -1
  23. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_plan_mcp_230.py +1 -1
  24. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_v010.py +1 -1
  25. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_validacion_130.py +1 -1
  26. snapcontext-3.4.0/tests/test_validacion_sintaxis_340.py +195 -0
  27. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_vscode_016.py +1 -1
  28. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_web_160.py +1 -1
  29. {snapcontext-3.3.0 → snapcontext-3.4.0}/LICENSE +0 -0
  30. {snapcontext-3.3.0 → snapcontext-3.4.0}/setup.cfg +0 -0
  31. {snapcontext-3.3.0 → snapcontext-3.4.0}/snapcontext.egg-info/__pycache__/top_level.cpython-313.pyc +0 -0
  32. {snapcontext-3.3.0 → snapcontext-3.4.0}/snapcontext.egg-info/dependency_links.txt +0 -0
  33. {snapcontext-3.3.0 → snapcontext-3.4.0}/snapcontext.egg-info/entry_points.txt +0 -0
  34. {snapcontext-3.3.0 → snapcontext-3.4.0}/snapcontext.egg-info/requires.txt +0 -0
  35. {snapcontext-3.3.0 → snapcontext-3.4.0}/snapcontext.egg-info/top_level.txt +0 -0
  36. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_agentes.py +0 -0
  37. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_bienvenida_311.py +0 -0
  38. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_deteccion.py +0 -0
  39. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_embeddings_110.py +0 -0
  40. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_orquestador.py +0 -0
  41. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_vscode_ts_320.py +0 -0
  42. {snapcontext-3.3.0 → snapcontext-3.4.0}/tests/test_web_120.py +0 -0
  43. {snapcontext-3.3.0 → snapcontext-3.4.0}/web/__init__.py +0 -0
  44. {snapcontext-3.3.0 → snapcontext-3.4.0}/web/app.py +0 -0
  45. {snapcontext-3.3.0 → snapcontext-3.4.0}/web/static/index.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snapcontext
3
- Version: 3.3.0
3
+ Version: 3.4.0
4
4
  Summary: SnapContext: Asistente de IA con contexto automático para desarrollo
5
5
  Author-email: SnapContext Contributors <tu@email.com>
6
6
  License-Expression: MIT
@@ -45,7 +45,7 @@ Dynamic: license-file
45
45
 
46
46
  # SnapContext
47
47
 
48
- ![v2.1.0](https://img.shields.io/badge/version-2.1.0-blue.svg)
48
+ ![v3.4.0](https://img.shields.io/badge/version-3.4.0-blue.svg)
49
49
  [![PyPI](https://badge.fury.io/py/snapcontext.svg)](https://pypi.org/project/snapcontext/)
50
50
  [![CI](https://img.shields.io/github/actions/workflow/status/NicolasBruna24/snapcontext/ci.yml?branch=main&label=tests)](https://github.com/NicolasBruña24/snapcontext/actions)
51
51
  ![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue.svg)
@@ -199,6 +199,37 @@ snapcontext "corregir tipado en login" --editor propio --modo-edicion parche
199
199
 
200
200
  # Refactorización estructural con AST
201
201
  snapcontext "renombrar la variable 'usuario' a 'cuenta'" --editor propio --modo-edicion ast
202
+
203
+ # Validación de sintaxis desactivada (comportamiento previo a v3.4.0)
204
+ snapcontext "corregir tipado" --editor propio --no-validar-sintaxis
205
+ ```
206
+
207
+ ### Validación de sintaxis antes de guardar (v3.4.0)
208
+
209
+ Cuando uses `--editor propio`, SnapContext **valida la sintaxis** del código que
210
+ ha generado la IA **antes de escribirlo en disco**. Si la validación falla, el
211
+ cambio se rechaza y se le envía el error al proveedor para que lo corrija,
212
+ repitiéndose hasta `--max-intentos-validacion` intentos (por defecto 3). Si tras
213
+ esos intentos el código sigue roto, se **cancela la edición** y el archivo queda
214
+ intacto.
215
+
216
+ - Se detecta el lenguaje del archivo y se usa el validador adecuado:
217
+ Python (`py_compile`), JavaScript/TypeScript (`node --check`), Dart
218
+ (`dart analyze` → `dart format`), Go (`go build -n` → `gofmt -e`), Rust
219
+ (`rustc --parse-only`), Java (`javac -Xlint:none`), C/C++ (`gcc`/`clang
220
+ -fsyntax-only`). Si no hay validador o comando disponible, se omite la
221
+ validación (solo aviso en logs con `--depurar`).
222
+ - La validación se hace sobre un **archivo temporal**, nunca toca el original.
223
+ - Flag `--validar` (por defecto activado) / `--no-validar-sintaxis` para
224
+ desactivarla. Nota: el nombre usa `--no-validar-sintaxis` porque
225
+ `--no-validar` ya es alias de `--iniciar-proyecto`.
226
+
227
+ ```bash
228
+ # Desactivar la validación por completo (comportamiento anterior)
229
+ snapcontext "..." --editor propio --no-validar-sintaxis
230
+
231
+ # Ajustar el número de reintentos
232
+ snapcontext "..." --editor propio --max-intentos-validacion 5
202
233
  ```
203
234
 
204
235
  ## 🧭 Modos y alias
@@ -1013,6 +1044,12 @@ locales y te deja elegir con las flechas:
1013
1044
  | `--url-defecto` | `http://localhost:5000` | URL para abrir el navegador si Flutter no reporta una |
1014
1045
  | `--comando-test` | `"flutter test"` | Comando del bucle de pruebas |
1015
1046
  | `--max-iteraciones` | `3` | Iteraciones máximas del bucle |
1047
+ | `--validar` | on | Valida la sintaxis del código antes de guardar en el editor propio |
1048
+ | `--no-validar-sintaxis` | off | Desactiva la validación de sintaxis del editor propio |
1049
+ | `--max-intentos-validacion` | `3` | Intentos de validación de sintaxis antes de cancelar la edición |
1050
+
1051
+ > **Nota:** el flag negativo de validación de sintaxis se llama `--no-validar-sintaxis`
1052
+ > porque `--no-validar` ya existe como alias de `--iniciar-proyecto`.
1016
1053
 
1017
1054
  ---
1018
1055
 
@@ -1,6 +1,6 @@
1
1
  # SnapContext
2
2
 
3
- ![v2.1.0](https://img.shields.io/badge/version-2.1.0-blue.svg)
3
+ ![v3.4.0](https://img.shields.io/badge/version-3.4.0-blue.svg)
4
4
  [![PyPI](https://badge.fury.io/py/snapcontext.svg)](https://pypi.org/project/snapcontext/)
5
5
  [![CI](https://img.shields.io/github/actions/workflow/status/NicolasBruna24/snapcontext/ci.yml?branch=main&label=tests)](https://github.com/NicolasBruña24/snapcontext/actions)
6
6
  ![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue.svg)
@@ -154,6 +154,37 @@ snapcontext "corregir tipado en login" --editor propio --modo-edicion parche
154
154
 
155
155
  # Refactorización estructural con AST
156
156
  snapcontext "renombrar la variable 'usuario' a 'cuenta'" --editor propio --modo-edicion ast
157
+
158
+ # Validación de sintaxis desactivada (comportamiento previo a v3.4.0)
159
+ snapcontext "corregir tipado" --editor propio --no-validar-sintaxis
160
+ ```
161
+
162
+ ### Validación de sintaxis antes de guardar (v3.4.0)
163
+
164
+ Cuando uses `--editor propio`, SnapContext **valida la sintaxis** del código que
165
+ ha generado la IA **antes de escribirlo en disco**. Si la validación falla, el
166
+ cambio se rechaza y se le envía el error al proveedor para que lo corrija,
167
+ repitiéndose hasta `--max-intentos-validacion` intentos (por defecto 3). Si tras
168
+ esos intentos el código sigue roto, se **cancela la edición** y el archivo queda
169
+ intacto.
170
+
171
+ - Se detecta el lenguaje del archivo y se usa el validador adecuado:
172
+ Python (`py_compile`), JavaScript/TypeScript (`node --check`), Dart
173
+ (`dart analyze` → `dart format`), Go (`go build -n` → `gofmt -e`), Rust
174
+ (`rustc --parse-only`), Java (`javac -Xlint:none`), C/C++ (`gcc`/`clang
175
+ -fsyntax-only`). Si no hay validador o comando disponible, se omite la
176
+ validación (solo aviso en logs con `--depurar`).
177
+ - La validación se hace sobre un **archivo temporal**, nunca toca el original.
178
+ - Flag `--validar` (por defecto activado) / `--no-validar-sintaxis` para
179
+ desactivarla. Nota: el nombre usa `--no-validar-sintaxis` porque
180
+ `--no-validar` ya es alias de `--iniciar-proyecto`.
181
+
182
+ ```bash
183
+ # Desactivar la validación por completo (comportamiento anterior)
184
+ snapcontext "..." --editor propio --no-validar-sintaxis
185
+
186
+ # Ajustar el número de reintentos
187
+ snapcontext "..." --editor propio --max-intentos-validacion 5
157
188
  ```
158
189
 
159
190
  ## 🧭 Modos y alias
@@ -968,6 +999,12 @@ locales y te deja elegir con las flechas:
968
999
  | `--url-defecto` | `http://localhost:5000` | URL para abrir el navegador si Flutter no reporta una |
969
1000
  | `--comando-test` | `"flutter test"` | Comando del bucle de pruebas |
970
1001
  | `--max-iteraciones` | `3` | Iteraciones máximas del bucle |
1002
+ | `--validar` | on | Valida la sintaxis del código antes de guardar en el editor propio |
1003
+ | `--no-validar-sintaxis` | off | Desactiva la validación de sintaxis del editor propio |
1004
+ | `--max-intentos-validacion` | `3` | Intentos de validación de sintaxis antes de cancelar la edición |
1005
+
1006
+ > **Nota:** el flag negativo de validación de sintaxis se llama `--no-validar-sintaxis`
1007
+ > porque `--no-validar` ya existe como alias de `--iniciar-proyecto`.
971
1008
 
972
1009
  ---
973
1010
 
@@ -232,9 +232,55 @@ class AgenteEditorPropio:
232
232
  return sc._editor_ast(archivo, tarea, directorio=directorio,
233
233
  proveedor=proveedor, modelo=modelo)
234
234
 
235
+ @staticmethod
236
+ def _aplicar_parche_preview(parche: str, contenido_actual: str):
237
+ """Aplica ``parche`` a ``contenido_actual`` **en memoria** para poder
238
+ validar la sintaxis del resultado sin tocar el archivo real.
239
+
240
+ Reutiliza la segmentación de hunks de ``_parsear_hunks`` y aplica cada
241
+ hunk con tolerancia a pequeños desfases. Devuelve ``(resultado, n)``
242
+ donde ``n`` es el número de hunks aplicados (0 ⇒ no se pudo reproducir).
243
+ """
244
+ import snapcontext as sc
245
+
246
+ resultado = (contenido_actual or "").splitlines()
247
+ desplazamiento = 0
248
+ aplicados = 0
249
+ for inicio_orig, cambios in sc._parsear_hunks(parche):
250
+ base = max(inicio_orig - 1 + desplazamiento, 0)
251
+ n_borrados = sum(1 for marca, _ in cambios if marca != "+")
252
+ nuevos = [texto for marca, texto in cambios if marca != "-"]
253
+
254
+ posicion = -1
255
+ for delta in (0, 1, -1, 2, -2, 3, -3, 5, -5, 10, -10):
256
+ candidato = base + delta
257
+ if candidato < 0 or candidato > len(resultado):
258
+ continue
259
+ idx = candidato
260
+ encaja = True
261
+ for marca, texto in cambios:
262
+ if marca == "+":
263
+ continue
264
+ if idx >= len(resultado) or resultado[idx] != texto:
265
+ encaja = False
266
+ break
267
+ idx += 1
268
+ if encaja:
269
+ posicion = candidato
270
+ break
271
+ if posicion < 0:
272
+ continue
273
+
274
+ resultado[posicion:posicion + n_borrados] = nuevos
275
+ desplazamiento += len(nuevos) - n_borrados
276
+ aplicados += 1
277
+
278
+ return "\n".join(resultado) + "\n", aplicados
279
+
235
280
  def _aplicar_modo_parche(self, archivo: str, mensaje: str,
236
281
  contenido_actual: str, modelo: Optional[str],
237
- directorio: str) -> bool:
282
+ directorio: str, validar: bool = True,
283
+ max_intentos_validacion: int = 3) -> bool:
238
284
  """Intenta editar `archivo` pidiendo un parche unificado al proveedor."""
239
285
  import snapcontext as sc
240
286
 
@@ -243,57 +289,134 @@ class AgenteEditorPropio:
243
289
  lenguaje = sc._lenguaje_archivo(archivo, contenido_actual) or "?"
244
290
  num_lineas = (contenido_actual.count("\n") + 1
245
291
  if contenido_actual else 0)
246
- prompt = (
247
- f"Genera un parche unificado (unified diff) que modifique el archivo "
248
- f"para cumplir con la tarea con precisión máxima.\n"
249
- f"El parche debe ser aplicable con 'patch -p1' o 'git apply' "
250
- f"(encabezados --- a/{archivo} y +++ b/{archivo}).\n\n"
251
- f"Tarea: {mensaje}\n"
252
- f"Archivo: {archivo} (lenguaje: {lenguaje}, {num_lineas} líneas)\n\n"
253
- f"Instrucciones:\n"
254
- f"- Incluye suficiente contexto (3 líneas) para anclar cada hunk.\n"
255
- f"- Conserva el estilo existente (indentación, comillas).\n"
256
- f"- Modifica SOLO lo necesario; no reorganices el resto del archivo.\n\n"
257
- f"Contenido actual completo:\n```{lenguaje}\n{contenido_actual}\n```\n\n"
258
- f"Devuelve ÚNICAMENTE el diff unificado, sin explicaciones ni rodeos."
259
- )
260
- respuesta = sc._enviar_al_proveedor(
261
- proveedor, modelo, [{"role": "user", "content": prompt}])
262
- diff_limpio = respuesta
263
- if "--- " in diff_limpio and "+++ " in diff_limpio:
264
- idx_inicio = diff_limpio.find("--- ")
265
- diff_limpio = diff_limpio[idx_inicio:]
266
- if "```" in diff_limpio:
267
- diff_limpio = diff_limpio[:diff_limpio.find("```")]
268
- if "--- " in diff_limpio and "+++ " in diff_limpio and "@@" in diff_limpio:
269
- # v3.3.0: validación previa + resolución de conflictos.
270
- return self.aplicar_parche(diff_limpio, directorio,
271
- contenido_esperado=contenido_actual)
292
+ max_val = max(1, int(max_intentos_validacion))
293
+
294
+ error_msj = ""
295
+ for intento in range(1, max_val + 1):
296
+ prompt = (
297
+ f"Genera un parche unificado (unified diff) que modifique el archivo "
298
+ f"para cumplir con la tarea con precisión máxima.\n"
299
+ f"El parche debe ser aplicable con 'patch -p1' o 'git apply' "
300
+ f"(encabezados --- a/{archivo} y +++ b/{archivo}).\n\n"
301
+ f"Tarea: {mensaje}\n"
302
+ f"Archivo: {archivo} (lenguaje: {lenguaje}, {num_lineas} líneas)\n\n"
303
+ f"Instrucciones:\n"
304
+ f"- Incluye suficiente contexto (3 líneas) para anclar cada hunk.\n"
305
+ f"- Conserva el estilo existente (indentación, comillas).\n"
306
+ f"- Modifica SOLO lo necesario; no reorganices el resto del archivo.\n"
307
+ )
308
+ if error_msj:
309
+ prompt += (
310
+ f"\nTu intento anterior produjo un error de sintaxis que debes "
311
+ f"corregir:\n{error_msj}\n\n"
312
+ f"Mantén el objetivo de la tarea pero arregla ese error.\n"
313
+ )
314
+ prompt += (
315
+ f"\nContenido actual completo:\n```{lenguaje}\n{contenido_actual}\n```\n\n"
316
+ f"Devuelve ÚNICAMENTE el diff unificado, sin explicaciones ni rodeos."
317
+ )
318
+ respuesta = sc._enviar_al_proveedor(
319
+ proveedor, modelo, [{"role": "user", "content": prompt}])
320
+ diff_limpio = respuesta
321
+ if "--- " in diff_limpio and "+++ " in diff_limpio:
322
+ idx_inicio = diff_limpio.find("--- ")
323
+ diff_limpio = diff_limpio[idx_inicio:]
324
+ if "```" in diff_limpio:
325
+ diff_limpio = diff_limpio[:diff_limpio.find("```")]
326
+ if not ("--- " in diff_limpio and "+++ " in diff_limpio
327
+ and "@@" in diff_limpio):
328
+ return False
329
+
330
+ if not validar:
331
+ # v3.3.0: validación previa + resolución de conflictos.
332
+ return self.aplicar_parche(diff_limpio, directorio,
333
+ contenido_esperado=contenido_actual)
334
+
335
+ # v3.4.0: validar la sintaxis del contenido resultante.
336
+ preview, aplicados = self._aplicar_parche_preview(
337
+ diff_limpio, contenido_actual)
338
+ if aplicados == 0:
339
+ # No se pudo reproducir en memoria → se omite la validación.
340
+ return self.aplicar_parche(diff_limpio, directorio,
341
+ contenido_esperado=contenido_actual)
342
+
343
+ sc.info(f"Validando sintaxis de {archivo}...")
344
+ exito, err = sc._validar_sintaxis(archivo, preview, directorio)
345
+ if exito:
346
+ sc.exito("Sintaxis válida.")
347
+ return self.aplicar_parche(diff_limpio, directorio,
348
+ contenido_esperado=contenido_actual)
349
+
350
+ error_msj = err
351
+ if intento < max_val:
352
+ sc.error(
353
+ f"Error de sintaxis: {err}. "
354
+ f"Reintentando ({intento}/{max_val})...")
355
+ else:
356
+ sc.error(
357
+ f"No se pudo validar tras {max_val} intentos. "
358
+ f"Edición cancelada.")
359
+ return False
272
360
  return False
273
361
 
274
362
  def _aplicar_modo_sobrescribir(self, archivo: str, mensaje: str,
275
363
  contenido_actual: str, modelo: Optional[str],
276
- directorio: str) -> bool:
364
+ directorio: str, validar: bool = True,
365
+ max_intentos_validacion: int = 3) -> bool:
277
366
  """Sobrescribe `archivo` con el código completo que devuelve el proveedor."""
278
367
  import snapcontext as sc
279
368
 
280
369
  pref = sc.cargar_configuracion()
281
370
  proveedor = pref.get("provider") or sc.PROVEEDOR_DEFECTO
282
371
  lenguaje = sc._lenguaje_archivo(archivo, contenido_actual) or "?"
283
- prompt = (
284
- f"Modifica el siguiente archivo ({lenguaje}) para cumplir con la "
285
- f"tarea, conservando el estilo existente y cambiando solo lo necesario.\n\n"
286
- f"Tarea: {mensaje}\n"
287
- f"Archivo: {archivo} (lenguaje: {lenguaje})\n\n"
288
- f"Contenido actual completo:\n```{lenguaje}\n{contenido_actual}\n```\n\n"
289
- f"Devuelve ÚNICAMENTE el código completo resultante, sin explicaciones ni markdown."
290
- )
291
- nuevo_contenido = sc._enviar_al_proveedor(
292
- proveedor, modelo, [{"role": "user", "content": prompt}])
293
- if nuevo_contenido.startswith("```"):
294
- lineas = nuevo_contenido.splitlines()
295
- if len(lineas) >= 2 and lineas[-1].startswith("```"):
296
- nuevo_contenido = "\n".join(lineas[1:-1])
372
+ max_val = max(1, int(max_intentos_validacion))
373
+
374
+ error_msj = ""
375
+ nuevo_contenido = ""
376
+ for intento in range(1, max_val + 1):
377
+ prompt = (
378
+ f"Modifica el siguiente archivo ({lenguaje}) para cumplir con la "
379
+ f"tarea, conservando el estilo existente y cambiando solo lo necesario.\n\n"
380
+ f"Tarea: {mensaje}\n"
381
+ f"Archivo: {archivo} (lenguaje: {lenguaje})\n"
382
+ )
383
+ if error_msj:
384
+ prompt += (
385
+ f"\nTu intento anterior produjo un error de sintaxis que debes "
386
+ f"corregir:\n{error_msj}\n\n"
387
+ f"Mantén el objetivo de la tarea pero arregla ese error.\n"
388
+ )
389
+ prompt += (
390
+ f"\nContenido actual completo:\n```{lenguaje}\n{contenido_actual}\n```\n\n"
391
+ f"Devuelve ÚNICAMENTE el código completo resultante, sin explicaciones ni markdown."
392
+ )
393
+ respuesta = sc._enviar_al_proveedor(
394
+ proveedor, modelo, [{"role": "user", "content": prompt}])
395
+ nuevo_contenido = respuesta
396
+ if nuevo_contenido.startswith("```"):
397
+ lineas = nuevo_contenido.splitlines()
398
+ if len(lineas) >= 2 and lineas[-1].startswith("```"):
399
+ nuevo_contenido = "\n".join(lineas[1:-1])
400
+
401
+ if not validar:
402
+ break
403
+
404
+ sc.info(f"Validando sintaxis de {archivo}...")
405
+ exito, err = sc._validar_sintaxis(archivo, nuevo_contenido, directorio)
406
+ if exito:
407
+ sc.exito("Sintaxis válida.")
408
+ break
409
+ error_msj = err
410
+ if intento < max_val:
411
+ sc.error(
412
+ f"Error de sintaxis: {err}. "
413
+ f"Reintentando ({intento}/{max_val})...")
414
+ else:
415
+ sc.error(
416
+ f"No se pudo validar tras {max_val} intentos. "
417
+ f"Edición cancelada.")
418
+ return False
419
+
297
420
  return self.sobrescribir(archivo, nuevo_contenido, directorio)
298
421
 
299
422
  def ejecutar(
@@ -303,6 +426,8 @@ class AgenteEditorPropio:
303
426
  directorio: str = ".",
304
427
  modo_edicion: str = "auto",
305
428
  modelo: Optional[str] = None,
429
+ validar: bool = True,
430
+ max_intentos_validacion: int = 3,
306
431
  ) -> bool:
307
432
  """Aplica las modificaciones para `archivos` con el `mensaje` especificado.
308
433
 
@@ -353,10 +478,14 @@ class AgenteEditorPropio:
353
478
  str(raiz), modelo)
354
479
  elif estrategia == "parche":
355
480
  conseguido = self._aplicar_modo_parche(
356
- arch, mensaje, contenido_actual, modelo, str(raiz))
481
+ arch, mensaje, contenido_actual, modelo, str(raiz),
482
+ validar=validar,
483
+ max_intentos_validacion=max_intentos_validacion)
357
484
  elif estrategia == "sobrescribir":
358
485
  conseguido = self._aplicar_modo_sobrescribir(
359
- arch, mensaje, contenido_actual, modelo, str(raiz))
486
+ arch, mensaje, contenido_actual, modelo, str(raiz),
487
+ validar=validar,
488
+ max_intentos_validacion=max_intentos_validacion)
360
489
  else:
361
490
  conseguido = False
362
491
  except Exception as exc:
@@ -190,6 +190,9 @@ class Orquestador:
190
190
  directorio=str(raiz),
191
191
  modo_edicion=modo_ed,
192
192
  modelo=getattr(args, "modelo", None),
193
+ validar=getattr(args, "validar", True),
194
+ max_intentos_validacion=getattr(
195
+ args, "max_intentos_validacion", 3),
193
196
  )
194
197
  self._emitir_tipo("editor", accion="fin", ok=ok)
195
198
  else:
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
  # Nombre verificado disponible en PyPI (18/08/2026): pypi.org/pypi/snapcontext → 404.
7
7
  # Alternativas si algún día estuviera ocupado: "snapcontext-cli" o "snapcontext-tool".
8
8
  name = "snapcontext"
9
- version = "3.3.0"
9
+ version = "3.4.0"
10
10
  description = "SnapContext: Asistente de IA con contexto automático para desarrollo"
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.9"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snapcontext
3
- Version: 3.3.0
3
+ Version: 3.4.0
4
4
  Summary: SnapContext: Asistente de IA con contexto automático para desarrollo
5
5
  Author-email: SnapContext Contributors <tu@email.com>
6
6
  License-Expression: MIT
@@ -45,7 +45,7 @@ Dynamic: license-file
45
45
 
46
46
  # SnapContext
47
47
 
48
- ![v2.1.0](https://img.shields.io/badge/version-2.1.0-blue.svg)
48
+ ![v3.4.0](https://img.shields.io/badge/version-3.4.0-blue.svg)
49
49
  [![PyPI](https://badge.fury.io/py/snapcontext.svg)](https://pypi.org/project/snapcontext/)
50
50
  [![CI](https://img.shields.io/github/actions/workflow/status/NicolasBruna24/snapcontext/ci.yml?branch=main&label=tests)](https://github.com/NicolasBruña24/snapcontext/actions)
51
51
  ![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue.svg)
@@ -199,6 +199,37 @@ snapcontext "corregir tipado en login" --editor propio --modo-edicion parche
199
199
 
200
200
  # Refactorización estructural con AST
201
201
  snapcontext "renombrar la variable 'usuario' a 'cuenta'" --editor propio --modo-edicion ast
202
+
203
+ # Validación de sintaxis desactivada (comportamiento previo a v3.4.0)
204
+ snapcontext "corregir tipado" --editor propio --no-validar-sintaxis
205
+ ```
206
+
207
+ ### Validación de sintaxis antes de guardar (v3.4.0)
208
+
209
+ Cuando uses `--editor propio`, SnapContext **valida la sintaxis** del código que
210
+ ha generado la IA **antes de escribirlo en disco**. Si la validación falla, el
211
+ cambio se rechaza y se le envía el error al proveedor para que lo corrija,
212
+ repitiéndose hasta `--max-intentos-validacion` intentos (por defecto 3). Si tras
213
+ esos intentos el código sigue roto, se **cancela la edición** y el archivo queda
214
+ intacto.
215
+
216
+ - Se detecta el lenguaje del archivo y se usa el validador adecuado:
217
+ Python (`py_compile`), JavaScript/TypeScript (`node --check`), Dart
218
+ (`dart analyze` → `dart format`), Go (`go build -n` → `gofmt -e`), Rust
219
+ (`rustc --parse-only`), Java (`javac -Xlint:none`), C/C++ (`gcc`/`clang
220
+ -fsyntax-only`). Si no hay validador o comando disponible, se omite la
221
+ validación (solo aviso en logs con `--depurar`).
222
+ - La validación se hace sobre un **archivo temporal**, nunca toca el original.
223
+ - Flag `--validar` (por defecto activado) / `--no-validar-sintaxis` para
224
+ desactivarla. Nota: el nombre usa `--no-validar-sintaxis` porque
225
+ `--no-validar` ya es alias de `--iniciar-proyecto`.
226
+
227
+ ```bash
228
+ # Desactivar la validación por completo (comportamiento anterior)
229
+ snapcontext "..." --editor propio --no-validar-sintaxis
230
+
231
+ # Ajustar el número de reintentos
232
+ snapcontext "..." --editor propio --max-intentos-validacion 5
202
233
  ```
203
234
 
204
235
  ## 🧭 Modos y alias
@@ -1013,6 +1044,12 @@ locales y te deja elegir con las flechas:
1013
1044
  | `--url-defecto` | `http://localhost:5000` | URL para abrir el navegador si Flutter no reporta una |
1014
1045
  | `--comando-test` | `"flutter test"` | Comando del bucle de pruebas |
1015
1046
  | `--max-iteraciones` | `3` | Iteraciones máximas del bucle |
1047
+ | `--validar` | on | Valida la sintaxis del código antes de guardar en el editor propio |
1048
+ | `--no-validar-sintaxis` | off | Desactiva la validación de sintaxis del editor propio |
1049
+ | `--max-intentos-validacion` | `3` | Intentos de validación de sintaxis antes de cancelar la edición |
1050
+
1051
+ > **Nota:** el flag negativo de validación de sintaxis se llama `--no-validar-sintaxis`
1052
+ > porque `--no-validar` ya existe como alias de `--iniciar-proyecto`.
1016
1053
 
1017
1054
  ---
1018
1055
 
@@ -33,6 +33,7 @@ tests/test_plan_012.py
33
33
  tests/test_plan_mcp_230.py
34
34
  tests/test_v010.py
35
35
  tests/test_validacion_130.py
36
+ tests/test_validacion_sintaxis_340.py
36
37
  tests/test_vscode_016.py
37
38
  tests/test_vscode_ts_320.py
38
39
  tests/test_web_120.py
@@ -56,7 +56,7 @@ import unicodedata
56
56
  import warnings
57
57
  import webbrowser
58
58
  from pathlib import Path
59
- from typing import Any, List, Optional, Union
59
+ from typing import Any, List, Optional, Tuple, Union
60
60
  from urllib.parse import urlparse
61
61
 
62
62
  # `google-generativeai` es la dependencia del proveedor por defecto (Gemini).
@@ -111,7 +111,7 @@ except ImportError: # pragma: no cover
111
111
  import concurrent.futures
112
112
  from concurrent.futures import ThreadPoolExecutor
113
113
 
114
- VERSION = "3.3.0"
114
+ VERSION = "3.4.0"
115
115
 
116
116
  # v3.1.0 — Claves de API reconocidas para el modo por defecto (offline).
117
117
  CLAVES_API_CONOCIDAS = (
@@ -301,6 +301,7 @@ MAX_ARCHIVOS_DEFECTO = 3 # archivos que recibe Aider
301
301
  MAX_CANDIDATOS_DEFECTO = 80 # candidatos que se envían al selector IA
302
302
  MAX_ITERACIONES_TEST_DEFECTO = 3
303
303
  COMANDO_TEST_DEFECTO = "flutter test"
304
+ MAX_INTENTOS_VALIDACION = 3 # reintentos de validación del editor propio
304
305
 
305
306
  # ---------------------------------------------------------------------------
306
307
  # Proveedores de IA para la selección de archivos
@@ -1732,6 +1733,127 @@ def _editor_sobrescribir(archivo: str, contenido: str,
1732
1733
  return False
1733
1734
 
1734
1735
 
1736
+ def _comandos_validacion(lenguaje: str, archivo_tmp: str) -> List[List[str]]:
1737
+ """Comandos de validación sintáctica para ``lenguaje``.
1738
+
1739
+ Devuelve una lista de candidatos (cada uno un argv con ``archivo_tmp`` ya
1740
+ resuelto). Cuando un lenguaje admite un validador de reserva (p. ej.
1741
+ ``dart analyze`` → ``dart format``), se incluyen en orden de preferencia.
1742
+ Devuelve ``[]`` si no existe validador para el lenguaje.
1743
+ """
1744
+ if not lenguaje:
1745
+ return []
1746
+ if lenguaje in ("python", "py", "python3"):
1747
+ return [[sys.executable, "-m", "py_compile", archivo_tmp]]
1748
+ if lenguaje in ("javascript", "typescript", "tsx", "js", "ts", "node",
1749
+ "jsx", "mjs", "cjs", "mts", "cts"):
1750
+ return [["node", "--check", archivo_tmp]]
1751
+ if lenguaje == "dart":
1752
+ return [["dart", "analyze", archivo_tmp],
1753
+ ["dart", "format", "--output=none", archivo_tmp]]
1754
+ if lenguaje == "go":
1755
+ return [["go", "build", "-n", archivo_tmp],
1756
+ ["gofmt", "-e", archivo_tmp]]
1757
+ if lenguaje in ("rust", "rs"):
1758
+ return [["rustc", "--parse-only", archivo_tmp]]
1759
+ if lenguaje in ("java",):
1760
+ return [["javac", "-Xlint:none", archivo_tmp]]
1761
+ if lenguaje in ("c", "h"):
1762
+ return [["gcc", "-fsyntax-only", archivo_tmp],
1763
+ ["clang", "-fsyntax-only", archivo_tmp]]
1764
+ if lenguaje in ("cpp", "cc", "cxx", "hpp", "hh", "hxx", "c++"):
1765
+ return [["g++", "-fsyntax-only", archivo_tmp],
1766
+ ["clang++", "-fsyntax-only", archivo_tmp]]
1767
+ return []
1768
+
1769
+
1770
+ def _validar_sintaxis(archivo: str, contenido: str,
1771
+ directorio: str = ".") -> Tuple[bool, str]:
1772
+ """Valida la sintaxis de ``contenido`` como si fuese el de ``archivo``.
1773
+
1774
+ Escribe el ``contenido`` en un archivo temporal (siempre conserva la
1775
+ extensión del archivo original para que el parser/linter lo reconozca) y
1776
+ ejecuta el comando de validación correspondiente al lenguaje detectado con
1777
+ ``_lenguaje_archivo``. Nunca toca el archivo original.
1778
+
1779
+ Devuelve ``(exito, mensaje_error)``:
1780
+ - ``(True, "")`` si la validación pasó, o si no hay validador / comando
1781
+ disponible (se omite la validación).
1782
+ - ``(False, mensaje)`` si el validador rechazó el contenido o hubo timeout.
1783
+ """
1784
+ lenguaje = _lenguaje_archivo(archivo, contenido) or ""
1785
+ if not lenguaje:
1786
+ depurar(
1787
+ f"[validar-sintaxis] Sin validador para '{archivo}' (lenguaje "
1788
+ f"desconocido); se omite la validación."
1789
+ )
1790
+ return True, ""
1791
+
1792
+ suffix = Path(archivo).suffix or ".txt"
1793
+ archivo_tmp = ""
1794
+ try:
1795
+ fd, archivo_tmp = tempfile.mkstemp(
1796
+ suffix=suffix, prefix=".snapcontext_validacion_")
1797
+ with os.fdopen(fd, "w", encoding="utf-8") as fh:
1798
+ fh.write(contenido or "")
1799
+
1800
+ comandos = _comandos_validacion(lenguaje, archivo_tmp)
1801
+ if not comandos:
1802
+ depurar(
1803
+ f"[validar-sintaxis] Sin validador para '{lenguaje}'; "
1804
+ "se omite la validación."
1805
+ )
1806
+ return True, ""
1807
+
1808
+ for cmd in comandos:
1809
+ binario = cmd[0]
1810
+ if shutil.which(binario) is None:
1811
+ depurar(
1812
+ f"[validar-sintaxis] Comando '{binario}' no disponible; "
1813
+ "se busca alternativa."
1814
+ )
1815
+ continue
1816
+ try:
1817
+ proc = subprocess.run(
1818
+ cmd, capture_output=True, text=True, timeout=60,
1819
+ cwd=directorio or ".",
1820
+ )
1821
+ except subprocess.TimeoutExpired:
1822
+ error(
1823
+ f"[validar-sintaxis] El comando '{binario}' excedió el "
1824
+ f"tiempo límite al validar '{archivo}'."
1825
+ )
1826
+ return False, (
1827
+ f"el comando '{binario}' excedió el tiempo límite de validación"
1828
+ )
1829
+ except (OSError, ValueError):
1830
+ continue # no se pudo lanzar → probar candidato siguiente
1831
+
1832
+ if proc.returncode == 0:
1833
+ return True, ""
1834
+ # Código de salida != 0 ⇒ error sintáctico (o validador fallo).
1835
+ salida = (proc.stderr or "").strip()
1836
+ if not salida:
1837
+ salida = (proc.stdout or "").strip()
1838
+ mensaje = salida or (
1839
+ f"el validador '{binario}' falló (código {proc.returncode})"
1840
+ )
1841
+ return False, mensaje
1842
+
1843
+ # Ningún candidato disponible → se omite la validación.
1844
+ depurar(
1845
+ f"[validar-sintaxis] Ningún validador disponible para '{lenguaje}'; "
1846
+ "se omite la validación."
1847
+ )
1848
+ return True, ""
1849
+ finally:
1850
+ if archivo_tmp:
1851
+ try:
1852
+ os.unlink(archivo_tmp)
1853
+ except OSError:
1854
+ pass
1855
+
1856
+
1735
1857
  def _generar_parche(original: str, nuevo: str, ruta_archivo: str) -> str:
1736
1858
  """Genera un parche unificado (unified diff) entre `original` y `nuevo`.
1737
1859
 
@@ -4082,6 +4204,9 @@ def _ejecutar_paso_plan(paso: dict, args: argparse.Namespace,
4082
4204
  directorio=str(ruta_raiz),
4083
4205
  modo_edicion=modo_ed,
4084
4206
  modelo=getattr(args, "modelo", None),
4207
+ validar=getattr(args, "validar", True),
4208
+ max_intentos_validacion=getattr(
4209
+ args, "max_intentos_validacion", MAX_INTENTOS_VALIDACION),
4085
4210
  )
4086
4211
  return (todo_ok, f"EditorPropio sobre {len(seleccion)} archivo(s)")
4087
4212
 
@@ -6842,7 +6967,8 @@ def _pintar(texto: str, clave: str) -> str:
6842
6967
  # Categorías en orden de aparición; cada opción se muestra una sola vez.
6843
6968
  CATEGORIAS_AYUDA = (
6844
6969
  ("Modos de ejecución",
6845
- ("--plan", "--auto", "--editor", "--modo-edicion", "--chat", "--web", "--web-puerto", "--demo",
6970
+ ("--plan", "--auto", "--editor", "--modo-edicion", "--validar", "--no-validar-sintaxis", "--max-intentos-validacion",
6971
+ "--chat", "--web", "--web-puerto", "--demo",
6846
6972
  "--init", "--init-claude", "--historial", "--historial-limpiar",
6847
6973
  "--diagnostico", "--reparar", "--bienvenida")),
6848
6974
  ("Selección de archivos",
@@ -7346,6 +7472,26 @@ def crear_parser() -> argparse.ArgumentParser:
7346
7472
  "'sobrescribir' (sobrescritura completa del archivo) o 'ast' "
7347
7473
  "(edición basada en el árbol sintáctico con fallback a sobrescritura).",
7348
7474
  )
7475
+ parser.add_argument(
7476
+ "--validar", dest="validar", action="store_const", const=True,
7477
+ default=True,
7478
+ help="Valida la sintaxis del código antes de guardar en el editor propio "
7479
+ "(por defecto activado).",
7480
+ )
7481
+ parser.add_argument(
7482
+ "--no-validar-sintaxis", dest="validar", action="store_const",
7483
+ const=False,
7484
+ help="Desactiva la validación de sintaxis en el editor propio "
7485
+ "(comportamiento previo a v3.4.0). Nota: su nombre no es "
7486
+ "'--no-validar' porque ese alias ya está reservado por "
7487
+ "--iniciar-proyecto.",
7488
+ )
7489
+ parser.add_argument(
7490
+ "--max-intentos-validacion", type=int,
7491
+ default=MAX_INTENTOS_VALIDACION, metavar="N",
7492
+ help=f"Intentos máximos de validación de sintaxis antes de cancelar la "
7493
+ f"edición (por defecto: {MAX_INTENTOS_VALIDACION}).",
7494
+ )
7349
7495
  # Aprendizaje autónomo / memoria avanzada (v3.0.0)
7350
7496
  parser.add_argument(
7351
7497
  "--daemon", action="store_true",
@@ -183,7 +183,7 @@ class TestFlagsAutoCli(unittest.TestCase):
183
183
  self.assertTrue(args.plan)
184
184
 
185
185
  def test_version_es_1_2_0(self):
186
- self.assertEqual(sc.VERSION, "3.3.0")
186
+ self.assertEqual(sc.VERSION, "3.4.0")
187
187
 
188
188
 
189
189
  if __name__ == "__main__":
@@ -35,7 +35,7 @@ class TestFlagsCli310(unittest.TestCase):
35
35
  ).bienvenida)
36
36
 
37
37
  def test_version_es_3_1_0(self):
38
- self.assertEqual(sc.VERSION, "3.3.0")
38
+ self.assertEqual(sc.VERSION, "3.4.0")
39
39
 
40
40
 
41
41
  class TestApiKeyDetection(unittest.TestCase):
@@ -98,7 +98,7 @@ class TestFlagsEdicion(unittest.TestCase):
98
98
  self.assertEqual(parser.parse_args(["--modo-edicion", "sobrescribir", "c"]).modo_edicion, "sobrescribir")
99
99
 
100
100
  def test_version_es_2_1_0(self):
101
- self.assertEqual(sc.VERSION, "3.3.0")
101
+ self.assertEqual(sc.VERSION, "3.4.0")
102
102
 
103
103
 
104
104
  if __name__ == "__main__":
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
- """Tests del editor propio v3.3.0.
3
+ """Tests del editor propio v3.3.0 (base) — validado bajo v3.4.0.
4
4
 
5
5
  Cubre el refinamiento multi-lenguaje del AST, el manejo de conflictos en
6
6
  parches (validación previa, resolución incremental) y la integración con
@@ -174,7 +174,7 @@ class TestFlagsEdicionAST(unittest.TestCase):
174
174
  self.assertEqual(args2.modo_edicion, "auto")
175
175
 
176
176
  def test_version_es_2_2_0(self):
177
- self.assertEqual(sc.VERSION, "3.3.0")
177
+ self.assertEqual(sc.VERSION, "3.4.0")
178
178
 
179
179
 
180
180
  if __name__ == "__main__":
@@ -84,7 +84,7 @@ class TestFlagsEditor(unittest.TestCase):
84
84
  self.assertIsInstance(orch.agente_editor_propio, AgenteEditorPropio)
85
85
 
86
86
  def test_version_es_2_0_0(self):
87
- self.assertEqual(sc.VERSION, "3.3.0")
87
+ self.assertEqual(sc.VERSION, "3.4.0")
88
88
 
89
89
 
90
90
  if __name__ == "__main__":
@@ -14,7 +14,7 @@ import snapcontext as sc # noqa: E402
14
14
 
15
15
  class TestVersion(unittest.TestCase):
16
16
  def test_version_150_coherente(self):
17
- self.assertEqual(sc.VERSION, "3.3.0")
17
+ self.assertEqual(sc.VERSION, "3.4.0")
18
18
 
19
19
 
20
20
  class TestEstaticoWeb(unittest.TestCase):
@@ -17,7 +17,7 @@ import snapcontext as sc # noqa: E402
17
17
 
18
18
  class TestVersion(unittest.TestCase):
19
19
  def test_version_170_coherente(self):
20
- self.assertEqual(sc.VERSION, "3.3.0")
20
+ self.assertEqual(sc.VERSION, "3.4.0")
21
21
 
22
22
 
23
23
  class TestEstructuraJetBrains(unittest.TestCase):
@@ -217,7 +217,7 @@ class TestFormatoYAutoContexto(BaseMCP):
217
217
 
218
218
  class TestVersionMCPCli(BaseMCP):
219
219
  def test_version_es_1_2_0(self):
220
- self.assertEqual(sc.VERSION, "3.3.0")
220
+ self.assertEqual(sc.VERSION, "3.4.0")
221
221
 
222
222
 
223
223
  if __name__ == "__main__":
@@ -248,7 +248,7 @@ class TestChatComandosGrafo(unittest.TestCase):
248
248
 
249
249
  class TestVersion(unittest.TestCase):
250
250
  def test_version_140_coherente(self):
251
- self.assertEqual(sc.VERSION, "3.3.0")
251
+ self.assertEqual(sc.VERSION, "3.4.0")
252
252
 
253
253
 
254
254
  if __name__ == "__main__":
@@ -179,7 +179,7 @@ class TestFlagsMemoriaCli(BaseMemoria):
179
179
  ["--init-claude"]).init_claude)
180
180
 
181
181
  def test_version_es_1_2_0(self):
182
- self.assertEqual(sc.VERSION, "3.3.0")
182
+ self.assertEqual(sc.VERSION, "3.4.0")
183
183
 
184
184
 
185
185
  if __name__ == "__main__":
@@ -312,7 +312,7 @@ class TestFlagsCLI(unittest.TestCase):
312
312
  self.assertEqual(args.daemon_intervalo, 24)
313
313
 
314
314
  def test_version_300(self):
315
- self.assertEqual(sc.VERSION, "3.3.0")
315
+ self.assertEqual(sc.VERSION, "3.4.0")
316
316
 
317
317
 
318
318
  if __name__ == "__main__":
@@ -182,7 +182,7 @@ class TestFlagsConfirmarCLI(unittest.TestCase):
182
182
  self.assertFalse(self._parse(["--chat", "--no-confirmar"]).confirmar)
183
183
 
184
184
  def test_version_es_1_2_0(self):
185
- self.assertEqual(sc.VERSION, "3.3.0")
185
+ self.assertEqual(sc.VERSION, "3.4.0")
186
186
 
187
187
 
188
188
  if __name__ == "__main__":
@@ -269,7 +269,7 @@ class TestFlagsPlanCLI(unittest.TestCase):
269
269
  self.assertIsNone(self._parse(["--plan", "x"]).branch)
270
270
 
271
271
  def test_version_es_1_2_0(self):
272
- self.assertEqual(sc.VERSION, "3.3.0")
272
+ self.assertEqual(sc.VERSION, "3.4.0")
273
273
 
274
274
 
275
275
  if __name__ == "__main__":
@@ -210,7 +210,7 @@ class TestEjecutarToolExecuteCommand(unittest.TestCase):
210
210
 
211
211
  class TestVersionYFlags(unittest.TestCase):
212
212
  def test_version_230(self):
213
- self.assertEqual(sc.VERSION, "3.3.0")
213
+ self.assertEqual(sc.VERSION, "3.4.0")
214
214
 
215
215
  def test_flag_paralelo(self):
216
216
  args = sc.crear_parser().parse_args(["--plan", "t", "--paralelo", "3"])
@@ -168,7 +168,7 @@ class TestFlagsCLI(unittest.TestCase):
168
168
  self.assertTrue(args.vista_previa)
169
169
 
170
170
  def test_version_es_1_2_0(self):
171
- self.assertEqual(sc.VERSION, "3.3.0")
171
+ self.assertEqual(sc.VERSION, "3.4.0")
172
172
 
173
173
 
174
174
  class TestComandosAgenteChat(unittest.TestCase):
@@ -126,7 +126,7 @@ class TestIniciarProyecto(unittest.TestCase):
126
126
  class TestVersion(unittest.TestCase):
127
127
  def test_version_130_coherente(self):
128
128
  import snapcontext as sc
129
- self.assertEqual(sc.VERSION, "3.3.0")
129
+ self.assertEqual(sc.VERSION, "3.4.0")
130
130
 
131
131
 
132
132
  if __name__ == "__main__":
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Tests de la validación de sintaxis del Editor Propio (v3.4.0).
4
+
5
+ Cubre ``_validar_sintaxis`` (con mocks de subprocess para distintos lenguajes),
6
+ los flags ``--validar / --no-validar / --max-intentos-validacion``, el flujo de
7
+ reintentos de ``_aplicar_modo_sobrescribir`` y la vista previa de parches.
8
+ """
9
+
10
+ import subprocess
11
+ import shutil
12
+ import tempfile
13
+ import unittest
14
+ from pathlib import Path
15
+ from types import SimpleNamespace
16
+ from unittest import mock
17
+
18
+ import snapcontext as sc
19
+ from agentes import AgenteEditorPropio
20
+
21
+
22
+ class BaseValidacion(unittest.TestCase):
23
+ def setUp(self):
24
+ self.tmp = tempfile.mkdtemp(prefix="sc340_")
25
+ self.raiz = Path(self.tmp).resolve()
26
+
27
+ def tearDown(self):
28
+ shutil.rmtree(self.tmp, ignore_errors=True)
29
+
30
+
31
+ def _proc(codigo, stderr="", stdout=""):
32
+ return SimpleNamespace(returncode=codigo, stderr=stderr, stdout=stdout)
33
+
34
+
35
+ class TestValidarSintaxis(BaseValidacion):
36
+ """_validar_sintaxis con mocks de lenguaje y subprocess."""
37
+
38
+ def test_python_valido_devuelve_true(self):
39
+ with mock.patch.object(sc, "_lenguaje_archivo", return_value="python"), \
40
+ mock.patch("snapcontext.shutil.which", return_value="/a/python"), \
41
+ mock.patch("snapcontext.subprocess.run", return_value=_proc(0)) as run:
42
+ ok, mensaje = sc._validar_sintaxis("modulo.py", "def f(): pass\n", self.tmp)
43
+ self.assertTrue(ok)
44
+ self.assertEqual(mensaje, "")
45
+ cmd = run.call_args[0][0]
46
+ self.assertEqual(cmd[1:3], ["-m", "py_compile"])
47
+
48
+ def test_lenguaje_sin_validador_omite(self):
49
+ with mock.patch.object(sc, "_lenguaje_archivo", return_value="json"):
50
+ ok, mensaje = sc._validar_sintaxis("datos.json", "{}", self.tmp)
51
+ self.assertTrue(ok)
52
+ self.assertEqual(mensaje, "")
53
+
54
+ def test_comando_no_disponible_omite(self):
55
+ with mock.patch.object(sc, "_lenguaje_archivo", return_value="dart"), \
56
+ mock.patch("snapcontext.shutil.which", return_value=None), \
57
+ mock.patch("snapcontext.subprocess.run") as run:
58
+ ok, mensaje = sc._validar_sintaxis("app.dart", "void main(){}\n", self.tmp)
59
+ self.assertTrue(ok)
60
+ self.assertEqual(mensaje, "")
61
+ run.assert_not_called()
62
+
63
+ def test_dart_fallback_analyze_indisponible(self):
64
+ with mock.patch.object(sc, "_lenguaje_archivo", return_value="dart"), \
65
+ mock.patch("snapcontext.shutil.which",
66
+ side_effect=lambda b: b if b == "dart" else None), \
67
+ mock.patch("snapcontext.subprocess.run",
68
+ side_effect=[FileNotFoundError, _proc(0)]) as run:
69
+ ok, mensaje = sc._validar_sintaxis("lib/main.dart", "dart", self.tmp)
70
+ self.assertTrue(ok)
71
+ self.assertEqual(run.call_count, 2)
72
+ self.assertEqual(run.call_args_list[1][0][0][0:2],
73
+ ["dart", "format"])
74
+
75
+ def test_timeout_devuelve_false(self):
76
+ exc = subprocess.TimeoutExpired("cmd", 60)
77
+ with mock.patch.object(sc, "_lenguaje_archivo", return_value="python"), \
78
+ mock.patch("snapcontext.shutil.which", return_value="/a/python"), \
79
+ mock.patch("snapcontext.subprocess.run", side_effect=exc):
80
+ ok, mensaje = sc._validar_sintaxis("modulo.py", "x = 1\n", self.tmp)
81
+ self.assertFalse(ok)
82
+ self.assertIn("tiempo", mensaje.lower())
83
+
84
+ def test_java_y_node_comandos(self):
85
+ with mock.patch.object(sc, "_lenguaje_archivo", return_value="java"), \
86
+ mock.patch("snapcontext.shutil.which", return_value="/a/javac"), \
87
+ mock.patch("snapcontext.subprocess.run", return_value=_proc(0)) as run:
88
+ ok, _ = sc._validar_sintaxis("Main.java", "class Main {}", self.tmp)
89
+ self.assertTrue(ok)
90
+ self.assertEqual(run.call_args[0][0][0], "javac")
91
+
92
+ with mock.patch.object(sc, "_lenguaje_archivo", return_value="javascript"), \
93
+ mock.patch("snapcontext.shutil.which", return_value="/a/node"), \
94
+ mock.patch("snapcontext.subprocess.run", return_value=_proc(0)) as run:
95
+ ok, _ = sc._validar_sintaxis("script.js", "const x = 1;", self.tmp)
96
+ self.assertTrue(ok)
97
+ self.assertEqual(run.call_args[0][0][0], "node")
98
+
99
+ def test_python_invalido_devuelve_false_y_mensaje(self):
100
+ with mock.patch.object(sc, "_lenguaje_archivo", return_value="python"), \
101
+ mock.patch("snapcontext.shutil.which", return_value="/a/python"), \
102
+ mock.patch("snapcontext.subprocess.run",
103
+ return_value=_proc(1, stderr="SyntaxError: invalid syntax")):
104
+ ok, mensaje = sc._validar_sintaxis("modulo.py", "def (\n", self.tmp)
105
+ self.assertFalse(ok)
106
+ self.assertIn("SyntaxError", mensaje)
107
+ class TestFlagsValidacion(unittest.TestCase):
108
+ def test_validar_por_defecto_true(self):
109
+ args = sc.crear_parser().parse_args(["consulta"])
110
+ self.assertTrue(args.validar)
111
+
112
+ def test_no_validar_sintaxis_false(self):
113
+ args = sc.crear_parser().parse_args(["consulta", "--no-validar-sintaxis"])
114
+ self.assertFalse(args.validar)
115
+
116
+ def test_validar_explicito_true(self):
117
+ args = sc.crear_parser().parse_args(["consulta", "--validar"])
118
+ self.assertTrue(args.validar)
119
+
120
+ def test_max_intentos_validacion_defecto_y_personalizado(self):
121
+ args = sc.crear_parser().parse_args(["consulta"])
122
+ self.assertEqual(args.max_intentos_validacion, 3)
123
+ args2 = sc.crear_parser().parse_args(
124
+ ["consulta", "--max-intentos-validacion", "5"])
125
+ self.assertEqual(args2.max_intentos_validacion, 5)
126
+ class TestReintentosSobrescribir(BaseValidacion):
127
+ """Flujo de reintentos de validación en _aplicar_modo_sobrescribir."""
128
+
129
+ def test_falla_y_luego_aprueba(self):
130
+ agente = AgenteEditorPropio()
131
+ archivo = "modulo.py"
132
+ (self.raiz / archivo).write_text("def f(): return 1\n", encoding="utf-8")
133
+ with mock.patch.object(sc, "cargar_configuracion", return_value={}), \
134
+ mock.patch.object(sc, "_enviar_al_proveedor",
135
+ return_value="def f(): return 2\n") as enviar, \
136
+ mock.patch.object(sc, "_validar_sintaxis",
137
+ side_effect=[(False, "SyntaxError"), (True, "")]) as val, \
138
+ mock.patch.object(agente, "sobrescribir", return_value=True) as sob:
139
+ ok = agente._aplicar_modo_sobrescribir(
140
+ archivo, "cambiar retorno", "def f(): return 1\n",
141
+ modelo=None, directorio=str(self.raiz))
142
+ self.assertTrue(ok)
143
+ self.assertEqual(enviar.call_count, 2) # 1ª generación + reintento
144
+ self.assertEqual(val.call_count, 2)
145
+ sob.assert_called_once_with(archivo, "def f(): return 2\n",
146
+ str(self.raiz))
147
+
148
+ def test_agota_intentos_cancela_sin_guardar(self):
149
+ agente = AgenteEditorPropio()
150
+ archivo = "modulo.py"
151
+ (self.raiz / archivo).write_text("x = 1\n", encoding="utf-8")
152
+ with mock.patch.object(sc, "cargar_configuracion", return_value={}), \
153
+ mock.patch.object(sc, "_enviar_al_proveedor",
154
+ return_value="malo()(") as enviar, \
155
+ mock.patch.object(sc, "_validar_sintaxis",
156
+ return_value=(False, "SyntaxError")) as val, \
157
+ mock.patch.object(agente, "sobrescribir") as sob:
158
+ ok = agente._aplicar_modo_sobrescribir(
159
+ archivo, "cambia", "x = 1\n", modelo=None,
160
+ directorio=self.tmp, max_intentos_validacion=3)
161
+ self.assertFalse(ok)
162
+ self.assertEqual(enviar.call_count, 3)
163
+ self.assertEqual(val.call_count, 3)
164
+ sob.assert_not_called()
165
+
166
+ def test_validar_false_no_valida(self):
167
+ agente = AgenteEditorPropio()
168
+ archivo = "modulo.py"
169
+ with mock.patch.object(sc, "cargar_configuracion", return_value={}), \
170
+ mock.patch.object(sc, "_enviar_al_proveedor",
171
+ return_value="x = 1\n") as enviar, \
172
+ mock.patch.object(sc, "_validar_sintaxis") as val, \
173
+ mock.patch.object(agente, "sobrescribir", return_value=True) as sob:
174
+ ok = agente._aplicar_modo_sobrescribir(
175
+ archivo, "cambiar", "", modelo=None, directorio=self.tmp,
176
+ validar=False)
177
+ self.assertTrue(ok)
178
+ self.assertEqual(enviar.call_count, 1)
179
+ val.assert_not_called()
180
+ sob.assert_called_once()
181
+
182
+
183
+ class TestPreviewParche(BaseValidacion):
184
+ def test_preview_aplica_hunk(self):
185
+ agente = AgenteEditorPropio()
186
+ parche = ("--- a/modulo.py\n+++ b/modulo.py\n@@ -1 +1 @@\n"
187
+ "-def fn(): return 1\n+def fn(): return 2\n")
188
+ resultado, aplicados = agente._aplicar_parche_preview(
189
+ parche, "def fn(): return 1\n")
190
+ self.assertEqual(aplicados, 1)
191
+ self.assertIn("def fn(): return 2", resultado)
192
+
193
+
194
+ if __name__ == "__main__":
195
+ unittest.main()
@@ -91,7 +91,7 @@ class TestEstructuraVsCode(unittest.TestCase):
91
91
 
92
92
  class TestVersionCli(unittest.TestCase):
93
93
  def test_version_es_1_2_0(self):
94
- self.assertEqual(sc.VERSION, "3.3.0")
94
+ self.assertEqual(sc.VERSION, "3.4.0")
95
95
 
96
96
 
97
97
  if __name__ == "__main__":
@@ -17,7 +17,7 @@ INDEX = RAIZ / "web" / "static" / "index.html"
17
17
 
18
18
  class TestVersion(unittest.TestCase):
19
19
  def test_version_160_coherente(self):
20
- self.assertEqual(sc.VERSION, "3.3.0")
20
+ self.assertEqual(sc.VERSION, "3.4.0")
21
21
 
22
22
 
23
23
  class TestFrontendV160(unittest.TestCase):
File without changes
File without changes
File without changes
File without changes