snapcontext 3.2.0__tar.gz → 3.3.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.
- {snapcontext-3.2.0/snapcontext.egg-info → snapcontext-3.3.0}/PKG-INFO +13 -1
- {snapcontext-3.2.0 → snapcontext-3.3.0}/README.md +12 -0
- {snapcontext-3.2.0 → snapcontext-3.3.0}/agentes.py +58 -11
- {snapcontext-3.2.0 → snapcontext-3.3.0}/pyproject.toml +1 -1
- {snapcontext-3.2.0 → snapcontext-3.3.0/snapcontext.egg-info}/PKG-INFO +13 -1
- {snapcontext-3.2.0 → snapcontext-3.3.0}/snapcontext.egg-info/SOURCES.txt +1 -0
- {snapcontext-3.2.0 → snapcontext-3.3.0}/snapcontext.py +366 -12
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_auto_017.py +1 -1
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_diagnostico_310.py +1 -1
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_diff_editor_210.py +3 -1
- snapcontext-3.3.0/tests/test_editor_330.py +347 -0
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_editor_ast_220.py +1 -1
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_editor_propio_200.py +1 -1
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_exe_150.py +1 -1
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_jetbrains_170.py +1 -1
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_mcp_014.py +1 -1
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_mcp_140.py +1 -1
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_memoria_015.py +1 -1
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_memoria_300.py +1 -1
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_permisos_013.py +1 -1
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_plan_012.py +1 -1
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_plan_mcp_230.py +1 -1
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_v010.py +1 -1
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_validacion_130.py +1 -1
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_vscode_016.py +1 -1
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_web_160.py +1 -1
- {snapcontext-3.2.0 → snapcontext-3.3.0}/LICENSE +0 -0
- {snapcontext-3.2.0 → snapcontext-3.3.0}/orquestador.py +0 -0
- {snapcontext-3.2.0 → snapcontext-3.3.0}/setup.cfg +0 -0
- {snapcontext-3.2.0 → snapcontext-3.3.0}/snapcontext.egg-info/__pycache__/top_level.cpython-313.pyc +0 -0
- {snapcontext-3.2.0 → snapcontext-3.3.0}/snapcontext.egg-info/dependency_links.txt +0 -0
- {snapcontext-3.2.0 → snapcontext-3.3.0}/snapcontext.egg-info/entry_points.txt +0 -0
- {snapcontext-3.2.0 → snapcontext-3.3.0}/snapcontext.egg-info/requires.txt +0 -0
- {snapcontext-3.2.0 → snapcontext-3.3.0}/snapcontext.egg-info/top_level.txt +0 -0
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_agentes.py +0 -0
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_bienvenida_311.py +0 -0
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_deteccion.py +0 -0
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_embeddings_110.py +0 -0
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_orquestador.py +0 -0
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_vscode_ts_320.py +0 -0
- {snapcontext-3.2.0 → snapcontext-3.3.0}/tests/test_web_120.py +0 -0
- {snapcontext-3.2.0 → snapcontext-3.3.0}/web/__init__.py +0 -0
- {snapcontext-3.2.0 → snapcontext-3.3.0}/web/app.py +0 -0
- {snapcontext-3.2.0 → snapcontext-3.3.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
|
+
Version: 3.3.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
|
|
@@ -143,6 +143,18 @@ La extensión vive en `vscode/` y está escrita en TypeScript:
|
|
|
143
143
|
- Empaquetar: `vscode/scripts/empaquetar.ps1` / `.sh` (compilan antes de
|
|
144
144
|
ejecutar `vsce package`).
|
|
145
145
|
|
|
146
|
+
### Editor propio refinado (v3.3.0)
|
|
147
|
+
- **Detección de lenguaje robusta**: extensión + heurística por contenido
|
|
148
|
+
(shebang, patrones de código) para proyectos mixtos.
|
|
149
|
+
- **Manejo de conflictos en parches**: validación previa del contenido del
|
|
150
|
+
archivo y resolución automática línea a línea si `git apply`/`patch`
|
|
151
|
+
fallan (con aviso de aplicación parcial); ya no se sobrescribe a ciegas.
|
|
152
|
+
- **Prompt de edición mejorado**: lenguaje, tamaño, resumen AST con
|
|
153
|
+
posiciones y reglas de precisión (estilo conservado, cambios mínimos).
|
|
154
|
+
- **Aprendizaje de patrones**: las ediciones exitosas se guardan como skills
|
|
155
|
+
(`editor-<patrón>`) y su estrategia se reutiliza automáticamente en
|
|
156
|
+
tareas similares.
|
|
157
|
+
|
|
146
158
|
## 🚀 Inicio rápido (5 minutos)
|
|
147
159
|
|
|
148
160
|
```bash
|
|
@@ -98,6 +98,18 @@ La extensión vive en `vscode/` y está escrita en TypeScript:
|
|
|
98
98
|
- Empaquetar: `vscode/scripts/empaquetar.ps1` / `.sh` (compilan antes de
|
|
99
99
|
ejecutar `vsce package`).
|
|
100
100
|
|
|
101
|
+
### Editor propio refinado (v3.3.0)
|
|
102
|
+
- **Detección de lenguaje robusta**: extensión + heurística por contenido
|
|
103
|
+
(shebang, patrones de código) para proyectos mixtos.
|
|
104
|
+
- **Manejo de conflictos en parches**: validación previa del contenido del
|
|
105
|
+
archivo y resolución automática línea a línea si `git apply`/`patch`
|
|
106
|
+
fallan (con aviso de aplicación parcial); ya no se sobrescribe a ciegas.
|
|
107
|
+
- **Prompt de edición mejorado**: lenguaje, tamaño, resumen AST con
|
|
108
|
+
posiciones y reglas de precisión (estilo conservado, cambios mínimos).
|
|
109
|
+
- **Aprendizaje de patrones**: las ediciones exitosas se guardan como skills
|
|
110
|
+
(`editor-<patrón>`) y su estrategia se reutiliza automáticamente en
|
|
111
|
+
tareas similares.
|
|
112
|
+
|
|
101
113
|
## 🚀 Inicio rápido (5 minutos)
|
|
102
114
|
|
|
103
115
|
```bash
|
|
@@ -181,15 +181,22 @@ class AgenteEditorPropio:
|
|
|
181
181
|
self,
|
|
182
182
|
parche: str,
|
|
183
183
|
directorio: str = ".",
|
|
184
|
+
contenido_esperado: Optional[str] = None,
|
|
184
185
|
) -> bool:
|
|
185
|
-
"""Aplica un parche unificado en ``directorio
|
|
186
|
+
"""Aplica un parche unificado en ``directorio`` (v3.3.0).
|
|
187
|
+
|
|
188
|
+
Con ``contenido_esperado`` se valida antes que el archivo coincida
|
|
189
|
+
con el usado para generar el parche y, si hay conflicto, se intenta
|
|
190
|
+
la resolución incremental línea a línea.
|
|
186
191
|
|
|
187
192
|
Devuelve ``True`` si se aplicó con éxito, ``False`` si falló.
|
|
188
193
|
"""
|
|
189
194
|
import snapcontext as sc
|
|
190
195
|
|
|
191
196
|
sc.depurar(f"[AgenteEditorPropio] Aplicando parche en '{directorio}'")
|
|
192
|
-
return sc.
|
|
197
|
+
return sc._aplicar_parche_con_resolucion(
|
|
198
|
+
parche, directorio=directorio,
|
|
199
|
+
contenido_esperado=contenido_esperado)
|
|
193
200
|
|
|
194
201
|
def _cadena_modos(self, archivo: str, mensaje: str,
|
|
195
202
|
modo_edicion: str) -> List[str]:
|
|
@@ -233,12 +240,21 @@ class AgenteEditorPropio:
|
|
|
233
240
|
|
|
234
241
|
pref = sc.cargar_configuracion()
|
|
235
242
|
proveedor = pref.get("provider") or sc.PROVEEDOR_DEFECTO
|
|
243
|
+
lenguaje = sc._lenguaje_archivo(archivo, contenido_actual) or "?"
|
|
244
|
+
num_lineas = (contenido_actual.count("\n") + 1
|
|
245
|
+
if contenido_actual else 0)
|
|
236
246
|
prompt = (
|
|
237
|
-
f"Genera un parche unificado (unified diff) que modifique el archivo
|
|
238
|
-
f"
|
|
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"
|
|
239
251
|
f"Tarea: {mensaje}\n"
|
|
240
|
-
f"Archivo: {archivo}\n\n"
|
|
241
|
-
f"
|
|
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"
|
|
242
258
|
f"Devuelve ÚNICAMENTE el diff unificado, sin explicaciones ni rodeos."
|
|
243
259
|
)
|
|
244
260
|
respuesta = sc._enviar_al_proveedor(
|
|
@@ -250,7 +266,9 @@ class AgenteEditorPropio:
|
|
|
250
266
|
if "```" in diff_limpio:
|
|
251
267
|
diff_limpio = diff_limpio[:diff_limpio.find("```")]
|
|
252
268
|
if "--- " in diff_limpio and "+++ " in diff_limpio and "@@" in diff_limpio:
|
|
253
|
-
|
|
269
|
+
# v3.3.0: validación previa + resolución de conflictos.
|
|
270
|
+
return self.aplicar_parche(diff_limpio, directorio,
|
|
271
|
+
contenido_esperado=contenido_actual)
|
|
254
272
|
return False
|
|
255
273
|
|
|
256
274
|
def _aplicar_modo_sobrescribir(self, archivo: str, mensaje: str,
|
|
@@ -261,11 +279,13 @@ class AgenteEditorPropio:
|
|
|
261
279
|
|
|
262
280
|
pref = sc.cargar_configuracion()
|
|
263
281
|
proveedor = pref.get("provider") or sc.PROVEEDOR_DEFECTO
|
|
282
|
+
lenguaje = sc._lenguaje_archivo(archivo, contenido_actual) or "?"
|
|
264
283
|
prompt = (
|
|
265
|
-
f"Modifica el siguiente archivo para cumplir con la
|
|
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"
|
|
266
286
|
f"Tarea: {mensaje}\n"
|
|
267
|
-
f"Archivo: {archivo}\n\n"
|
|
268
|
-
f"Contenido actual:\n
|
|
287
|
+
f"Archivo: {archivo} (lenguaje: {lenguaje})\n\n"
|
|
288
|
+
f"Contenido actual completo:\n```{lenguaje}\n{contenido_actual}\n```\n\n"
|
|
269
289
|
f"Devuelve ÚNICAMENTE el código completo resultante, sin explicaciones ni markdown."
|
|
270
290
|
)
|
|
271
291
|
nuevo_contenido = sc._enviar_al_proveedor(
|
|
@@ -298,6 +318,18 @@ class AgenteEditorPropio:
|
|
|
298
318
|
raiz = Path(directorio).resolve()
|
|
299
319
|
todo_ok = True
|
|
300
320
|
|
|
321
|
+
# v3.3.0 — Integración con skills: si ya existe un patrón de edición
|
|
322
|
+
# aprendido para esta tarea, se prioriza su estrategia (sin pasar por
|
|
323
|
+
# el proveedor de IA cuando el skill lo permita).
|
|
324
|
+
estrategia_aprendida = None
|
|
325
|
+
try:
|
|
326
|
+
estrategia_aprendida = sc._skill_editor_estrategia(mensaje)
|
|
327
|
+
if estrategia_aprendida:
|
|
328
|
+
sc.info("[skills] Reutilizando patrón de edición aprendido: "
|
|
329
|
+
f"'{estrategia_aprendida}'.")
|
|
330
|
+
except Exception as exc:
|
|
331
|
+
sc.depurar(f"[skills] Búsqueda de skill falló: {exc}")
|
|
332
|
+
|
|
301
333
|
for arch in archivos:
|
|
302
334
|
camino = (raiz / arch).resolve()
|
|
303
335
|
contenido_actual = ""
|
|
@@ -309,6 +341,10 @@ class AgenteEditorPropio:
|
|
|
309
341
|
pass
|
|
310
342
|
|
|
311
343
|
cadena = self._cadena_modos(arch, mensaje, modo_edicion)
|
|
344
|
+
# El skill aprendido se coloca el primero en la cadena.
|
|
345
|
+
if estrategia_aprendida and estrategia_aprendida in cadena:
|
|
346
|
+
cadena.remove(estrategia_aprendida)
|
|
347
|
+
cadena.insert(0, estrategia_aprendida)
|
|
312
348
|
conseguido = False
|
|
313
349
|
for estrategia in cadena:
|
|
314
350
|
try:
|
|
@@ -329,7 +365,18 @@ class AgenteEditorPropio:
|
|
|
329
365
|
conseguido = False
|
|
330
366
|
if conseguido:
|
|
331
367
|
break
|
|
332
|
-
|
|
368
|
+
|
|
369
|
+
if conseguido:
|
|
370
|
+
# v3.3.0 — Aprendizaje: guardar/reforzar el patrón de edición.
|
|
371
|
+
patron = sc._editor_clasificar_tarea(mensaje)
|
|
372
|
+
sid = sc._skill_editor_guardar(
|
|
373
|
+
mensaje, arch, patron, estrategia=estrategia)
|
|
374
|
+
if sid is not None:
|
|
375
|
+
try:
|
|
376
|
+
sc._skill_registrar_exito(sid)
|
|
377
|
+
except Exception as exc:
|
|
378
|
+
sc.depurar(f"[skills] Refuerzo falló: {exc}")
|
|
379
|
+
else:
|
|
333
380
|
sc.error(f"[AgenteEditorPropio] No se pudo editar '{arch}'.")
|
|
334
381
|
todo_ok = False
|
|
335
382
|
|
|
@@ -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.
|
|
9
|
+
version = "3.3.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
|
+
Version: 3.3.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
|
|
@@ -143,6 +143,18 @@ La extensión vive en `vscode/` y está escrita en TypeScript:
|
|
|
143
143
|
- Empaquetar: `vscode/scripts/empaquetar.ps1` / `.sh` (compilan antes de
|
|
144
144
|
ejecutar `vsce package`).
|
|
145
145
|
|
|
146
|
+
### Editor propio refinado (v3.3.0)
|
|
147
|
+
- **Detección de lenguaje robusta**: extensión + heurística por contenido
|
|
148
|
+
(shebang, patrones de código) para proyectos mixtos.
|
|
149
|
+
- **Manejo de conflictos en parches**: validación previa del contenido del
|
|
150
|
+
archivo y resolución automática línea a línea si `git apply`/`patch`
|
|
151
|
+
fallan (con aviso de aplicación parcial); ya no se sobrescribe a ciegas.
|
|
152
|
+
- **Prompt de edición mejorado**: lenguaje, tamaño, resumen AST con
|
|
153
|
+
posiciones y reglas de precisión (estilo conservado, cambios mínimos).
|
|
154
|
+
- **Aprendizaje de patrones**: las ediciones exitosas se guardan como skills
|
|
155
|
+
(`editor-<patrón>`) y su estrategia se reutiliza automáticamente en
|
|
156
|
+
tareas similares.
|
|
157
|
+
|
|
146
158
|
## 🚀 Inicio rápido (5 minutos)
|
|
147
159
|
|
|
148
160
|
```bash
|
|
@@ -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.
|
|
114
|
+
VERSION = "3.3.0"
|
|
115
115
|
|
|
116
116
|
# v3.1.0 — Claves de API reconocidas para el modo por defecto (offline).
|
|
117
117
|
CLAVES_API_CONOCIDAS = (
|
|
@@ -1802,6 +1802,214 @@ def _aplicar_parche(parche: str, directorio: str = ".") -> bool:
|
|
|
1802
1802
|
os.remove(temp_path)
|
|
1803
1803
|
except OSError:
|
|
1804
1804
|
pass
|
|
1805
|
+
|
|
1806
|
+
|
|
1807
|
+
# ---------------------------------------------------------------------------
|
|
1808
|
+
# Manejo de conflictos y aplicación incremental (v3.3.0)
|
|
1809
|
+
# ---------------------------------------------------------------------------
|
|
1810
|
+
def _ruta_del_parche(parche: str) -> Optional[str]:
|
|
1811
|
+
"""Extrae la ruta del archivo objetivo del encabezado del parche.
|
|
1812
|
+
|
|
1813
|
+
Acepta encabezados ``--- a/ruta`` / ``+++ b/ruta`` y variantes sin
|
|
1814
|
+
prefijo. Devuelve None si no se encuentra.
|
|
1815
|
+
"""
|
|
1816
|
+
for linea in (parche or "").splitlines():
|
|
1817
|
+
if linea.startswith("+++ "):
|
|
1818
|
+
ruta = linea[4:].strip().split("\t")[0]
|
|
1819
|
+
if ruta.startswith("b/"):
|
|
1820
|
+
ruta = ruta[2:]
|
|
1821
|
+
return ruta or None
|
|
1822
|
+
if linea.startswith("--- "):
|
|
1823
|
+
candidata = linea[4:].strip().split("\t")[0]
|
|
1824
|
+
if candidata.startswith("a/"):
|
|
1825
|
+
candidata = candidata[2:]
|
|
1826
|
+
if candidata and candidata not in ("/dev/null",):
|
|
1827
|
+
return candidata
|
|
1828
|
+
return None
|
|
1829
|
+
|
|
1830
|
+
|
|
1831
|
+
def _validar_parche_previo(parche: str, directorio: str,
|
|
1832
|
+
contenido_esperado: Optional[str]) -> tuple:
|
|
1833
|
+
"""Verifica que el archivo coincide con lo usado para generar el parche.
|
|
1834
|
+
|
|
1835
|
+
Evita conflictos por cambios concurrentes: si el contenido actual del
|
|
1836
|
+
archivo difiere del que se pasó al proveedor, aplicar a ciegas corrompería
|
|
1837
|
+
la edición. Devuelve ``(ok, detalle)``.
|
|
1838
|
+
"""
|
|
1839
|
+
if contenido_esperado is None:
|
|
1840
|
+
return True, "sin validación (no hay contenido de referencia)"
|
|
1841
|
+
ruta = _ruta_del_parche(parche)
|
|
1842
|
+
if not ruta:
|
|
1843
|
+
return True, "parche sin encabezado reconocible; se omite la validación"
|
|
1844
|
+
destino = Path(directorio or ".").resolve() / ruta
|
|
1845
|
+
if not destino.is_file():
|
|
1846
|
+
return False, f"el archivo '{ruta}' ya no existe"
|
|
1847
|
+
try:
|
|
1848
|
+
actual = destino.read_text(encoding="utf-8", errors="replace")
|
|
1849
|
+
except OSError as exc:
|
|
1850
|
+
return False, f"no se pudo leer '{ruta}': {exc}"
|
|
1851
|
+
if actual != contenido_esperado:
|
|
1852
|
+
return False, (f"'{ruta}' cambió desde que se generó el parche "
|
|
1853
|
+
"(posible cambio concurrente)")
|
|
1854
|
+
return True, "el archivo coincide con la referencia"
|
|
1855
|
+
|
|
1856
|
+
|
|
1857
|
+
def _parsear_hunks(parche: str) -> List[tuple]:
|
|
1858
|
+
"""Divide un diff unificado en hunks ``(linea_inicio_original, cambios)``.
|
|
1859
|
+
|
|
1860
|
+
``cambios`` es una lista de ``(marca, texto)`` con marca ' ', '-' o '+'.
|
|
1861
|
+
Se omiten los hunks sin líneas modificadas. Devuelve [] si no hay ninguno.
|
|
1862
|
+
"""
|
|
1863
|
+
hunks: List[tuple] = []
|
|
1864
|
+
hunk_actual: Optional[List[tuple]] = None
|
|
1865
|
+
inicio_orig = 0
|
|
1866
|
+
for linea in (parche or "").splitlines(keepends=True):
|
|
1867
|
+
texto = linea.rstrip("\r\n")
|
|
1868
|
+
m = re.match(r"@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@", texto)
|
|
1869
|
+
if m:
|
|
1870
|
+
if hunk_actual:
|
|
1871
|
+
hunks.append((inicio_orig, hunk_actual))
|
|
1872
|
+
inicio_orig = int(m.group(1))
|
|
1873
|
+
hunk_actual = []
|
|
1874
|
+
continue
|
|
1875
|
+
if hunk_actual is None:
|
|
1876
|
+
continue # encabezados ---/+++/ruido
|
|
1877
|
+
if texto.startswith("+"):
|
|
1878
|
+
hunk_actual.append(("+", texto[1:]))
|
|
1879
|
+
elif texto.startswith("-"):
|
|
1880
|
+
hunk_actual.append(("-", texto[1:]))
|
|
1881
|
+
else:
|
|
1882
|
+
hunk_actual.append((" ", texto[1:] if texto else ""))
|
|
1883
|
+
if hunk_actual:
|
|
1884
|
+
hunks.append((inicio_orig, hunk_actual))
|
|
1885
|
+
return [(i, l) for i, l in hunks if any(marca != " " for marca, _ in l)]
|
|
1886
|
+
|
|
1887
|
+
|
|
1888
|
+
def _aplicar_hunks_incremental(parche: str, directorio: str) -> bool:
|
|
1889
|
+
"""Resolución automática de conflictos: aplica el parche línea a línea.
|
|
1890
|
+
|
|
1891
|
+
Estrategia puramente Python (sin git/patch): para cada hunk busca el
|
|
1892
|
+
bloque original con tolerancia a desfases (posiciones cercanas + búsqueda
|
|
1893
|
+
difusa por contexto con difflib) y aplica solo las líneas modificadas.
|
|
1894
|
+
Los hunks irresolubles se omiten con aviso; se escribe el resultado si al
|
|
1895
|
+
menos un hunk se aplicó, siempre con copia de seguridad previa.
|
|
1896
|
+
|
|
1897
|
+
Devuelve True si se aplicó algún cambio.
|
|
1898
|
+
"""
|
|
1899
|
+
ruta = _ruta_del_parche(parche)
|
|
1900
|
+
if not ruta:
|
|
1901
|
+
aviso("[EditorPropio] No se pudo deducir el archivo del parche.")
|
|
1902
|
+
return False
|
|
1903
|
+
destino = Path(directorio or ".").resolve() / ruta
|
|
1904
|
+
if not destino.is_file():
|
|
1905
|
+
aviso(f"[EditorPropio] El archivo del parche no existe: {ruta}")
|
|
1906
|
+
return False
|
|
1907
|
+
try:
|
|
1908
|
+
original = destino.read_text(encoding="utf-8", errors="replace")
|
|
1909
|
+
except OSError as exc:
|
|
1910
|
+
error(f"[EditorPropio] No se pudo leer '{ruta}': {exc}")
|
|
1911
|
+
return False
|
|
1912
|
+
|
|
1913
|
+
resultado = original.splitlines()
|
|
1914
|
+
hunks = _parsear_hunks(parche)
|
|
1915
|
+
if not hunks:
|
|
1916
|
+
aviso("[EditorPropio] El parche no contiene hunks aplicables.")
|
|
1917
|
+
return False
|
|
1918
|
+
|
|
1919
|
+
def _coincide(desde: int, bloque: List[tuple]) -> bool:
|
|
1920
|
+
"""True si ``resultado[desde:]`` encaja con las líneas no-'+'."""
|
|
1921
|
+
idx = desde
|
|
1922
|
+
for marca, texto in bloque:
|
|
1923
|
+
if marca == "+":
|
|
1924
|
+
continue
|
|
1925
|
+
if idx >= len(resultado) or resultado[idx] != texto:
|
|
1926
|
+
return False
|
|
1927
|
+
idx += 1
|
|
1928
|
+
return True
|
|
1929
|
+
|
|
1930
|
+
def _n_borrar(bloque: List[tuple]) -> int:
|
|
1931
|
+
return sum(1 for marca, _ in bloque if marca != "+")
|
|
1932
|
+
|
|
1933
|
+
aplicados = 0
|
|
1934
|
+
desplazamiento = 0 # acumulado por hunks previos
|
|
1935
|
+
for inicio_orig, cambios in hunks:
|
|
1936
|
+
base = max(inicio_orig - 1 + desplazamiento, 0)
|
|
1937
|
+
n_borrados = _n_borrar(cambios)
|
|
1938
|
+
|
|
1939
|
+
posicion = -1
|
|
1940
|
+
# 1) Coincidencia exacta cerca de la posición declarada.
|
|
1941
|
+
offsets = sorted({0, 1, -1, 2, -2, 3, -3, 5, -5, 10, -10, 20, -20})
|
|
1942
|
+
for delta in offsets:
|
|
1943
|
+
candidato = base + delta
|
|
1944
|
+
if 0 <= candidato <= len(resultado) and \
|
|
1945
|
+
_coincide(candidato, cambios):
|
|
1946
|
+
posicion = candidato
|
|
1947
|
+
break
|
|
1948
|
+
# 2) Búsqueda difusa por líneas de contexto con difflib.
|
|
1949
|
+
if posicion < 0:
|
|
1950
|
+
contexto_idx = [i for i, (m, _) in enumerate(cambios)
|
|
1951
|
+
if m == " " and _.strip()]
|
|
1952
|
+
if contexto_idx:
|
|
1953
|
+
primera_ctx = cambios[contexto_idx[0]][1].strip()
|
|
1954
|
+
for idx_linea, linea in enumerate(resultado):
|
|
1955
|
+
if linea.strip() == primera_ctx:
|
|
1956
|
+
candidato = idx_linea - contexto_idx[0]
|
|
1957
|
+
if _coincide(max(candidato, 0), cambios):
|
|
1958
|
+
posicion = max(candidato, 0)
|
|
1959
|
+
break
|
|
1960
|
+
if posicion < 0 or not _coincide(posicion, cambios):
|
|
1961
|
+
aviso(f"[EditorPropio] Hunk en línea {inicio_orig} no aplicable; "
|
|
1962
|
+
"se omite (resolución automática).")
|
|
1963
|
+
continue
|
|
1964
|
+
|
|
1965
|
+
nuevo_bloque = [texto for marca, texto in cambios if marca != "-"]
|
|
1966
|
+
resultado[posicion:posicion + n_borrados] = nuevo_bloque
|
|
1967
|
+
desplazamiento += len(nuevo_bloque) - n_borrados
|
|
1968
|
+
aplicados += 1
|
|
1969
|
+
|
|
1970
|
+
if not aplicados:
|
|
1971
|
+
aviso("[EditorPropio] Ningún hunk pudo aplicarse; archivo intacto.")
|
|
1972
|
+
return False
|
|
1973
|
+
if aplicados < len(hunks):
|
|
1974
|
+
aviso(f"[EditorPropio] Aplicación parcial: {aplicados}/{len(hunks)} "
|
|
1975
|
+
"hunks. Revisa el resultado antes de confirmar.")
|
|
1976
|
+
|
|
1977
|
+
exito(f"[EditorPropio] Parche aplicado con resolución incremental "
|
|
1978
|
+
f"(línea a línea) sobre '{ruta}'.")
|
|
1979
|
+
return _editor_sobrescribir(ruta, "\n".join(resultado) + "\n",
|
|
1980
|
+
directorio=directorio)
|
|
1981
|
+
|
|
1982
|
+
|
|
1983
|
+
def _aplicar_parche_con_resolucion(parche: str, directorio: str = ".",
|
|
1984
|
+
contenido_esperado: Optional[str] = None
|
|
1985
|
+
) -> bool:
|
|
1986
|
+
"""Aplica un parche con validación previa y resolución de conflictos.
|
|
1987
|
+
|
|
1988
|
+
Flujo (v3.3.0):
|
|
1989
|
+
1. Validación previa: si se pasa ``contenido_esperado`` (el contenido
|
|
1990
|
+
usado para generar el parche), comprueba que el archivo actual
|
|
1991
|
+
coincida para evitar conflictos concurrentes.
|
|
1992
|
+
2. Intento estándar: ``git apply`` → ``patch -p1``.
|
|
1993
|
+
3. Resolución automática: aplicación incremental línea a línea.
|
|
1994
|
+
4. Si todo falla, avisa para resolución manual (ya no sobrescribe a
|
|
1995
|
+
ciegas).
|
|
1996
|
+
"""
|
|
1997
|
+
ok_validacion, detalle = _validar_parche_previo(
|
|
1998
|
+
parche, directorio, contenido_esperado)
|
|
1999
|
+
if not ok_validacion:
|
|
2000
|
+
aviso(f"[EditorPropio] Validación previa fallida: {detalle}. "
|
|
2001
|
+
"Se intentará la resolución automática.")
|
|
2002
|
+
elif contenido_esperado is not None:
|
|
2003
|
+
depurar(f"[EditorPropio] Validación previa OK: {detalle}")
|
|
2004
|
+
|
|
2005
|
+
if _aplicar_parche(parche, directorio=directorio):
|
|
2006
|
+
return True
|
|
2007
|
+
info("[EditorPropio] Conflicto detectado; probando resolución "
|
|
2008
|
+
"incremental (línea a línea)...")
|
|
2009
|
+
return _aplicar_hunks_incremental(parche, directorio)
|
|
2010
|
+
|
|
2011
|
+
|
|
2012
|
+
|
|
1805
2013
|
# ---------------------------------------------------------------------------
|
|
1806
2014
|
# Editor propio (Fase 3 — Edición basada en AST) — v2.2.0
|
|
1807
2015
|
# ---------------------------------------------------------------------------
|
|
@@ -1873,8 +2081,8 @@ def _resumen_ast(contenido: str, ruta: str) -> dict:
|
|
|
1873
2081
|
Devuelve un dict con ``ok``, ``motor``, ``lenguaje`` y una proyección simple
|
|
1874
2082
|
(funciones/clases/imports/variables/llamadas). Nunca lanza excepciones.
|
|
1875
2083
|
"""
|
|
1876
|
-
lenguaje =
|
|
1877
|
-
if _es_extension_python(ruta):
|
|
2084
|
+
lenguaje = _lenguaje_archivo(ruta, contenido) or ""
|
|
2085
|
+
if _es_extension_python(ruta) or lenguaje == "python":
|
|
1878
2086
|
return _resumen_ast_python(contenido)
|
|
1879
2087
|
if tree_sitter is not None and _ts_lang is not None and lenguaje:
|
|
1880
2088
|
try:
|
|
@@ -2096,12 +2304,21 @@ def _editor_ast(archivo: str, tarea: str, directorio: str = ".",
|
|
|
2096
2304
|
return False
|
|
2097
2305
|
|
|
2098
2306
|
proveedor = proveedor or cargar_configuracion().get("provider") or PROVEEDOR_DEFECTO
|
|
2307
|
+
lenguaje = resumen.get("lenguaje") or _lenguaje_archivo(ruta_posix,
|
|
2308
|
+
contenido) or "?"
|
|
2309
|
+
num_lineas = contenido.count("\n") + 1
|
|
2099
2310
|
prompt = (
|
|
2100
|
-
f"Vas a modificar un archivo comprendiendo su estructura sintáctica
|
|
2311
|
+
f"Vas a modificar un archivo comprendiendo su estructura sintáctica "
|
|
2312
|
+
f"(AST). Objetivo: precisión máxima y cambios mínimos.\n\n"
|
|
2101
2313
|
f"Tarea: {tarea}\n"
|
|
2102
|
-
f"Archivo: {ruta_posix}\n\n"
|
|
2103
|
-
f"Resumen del AST
|
|
2104
|
-
f"
|
|
2314
|
+
f"Archivo: {ruta_posix} (lenguaje: {lenguaje}, {num_lineas} líneas)\n\n"
|
|
2315
|
+
f"Resumen del AST (símbolos disponibles y sus posiciones):\n"
|
|
2316
|
+
f"{_formatear_resumen_ast(resumen, ruta_posix)}\n\n"
|
|
2317
|
+
f"Contenido actual completo:\n```\n{contenido}\n```\n\n"
|
|
2318
|
+
f"Reglas de edición:\n"
|
|
2319
|
+
f"- Conserva el estilo existente (indentación, comillas, convenciones).\n"
|
|
2320
|
+
f"- Modifica SOLO lo necesario para la tarea; no reorganices el resto.\n"
|
|
2321
|
+
f"- Usa los símbolos del resumen del AST para anclar tus cambios.\n\n"
|
|
2105
2322
|
f"Responde ÚNICAMENTE con una lista JSON de operaciones de edición, por ejemplo:\n"
|
|
2106
2323
|
f'[{{"tipo": "renombrar", "nombre": "viejo", "nuevo": "nuevo"}}]\n'
|
|
2107
2324
|
f'O, si prefieres devolver el código completo resultante:\n'
|
|
@@ -4684,6 +4901,92 @@ def _skill_obtener(skill_id: int) -> Optional[dict]:
|
|
|
4684
4901
|
return skill
|
|
4685
4902
|
|
|
4686
4903
|
|
|
4904
|
+
# ─── Skills del editor propio: patrones de edición (v3.3.0) ────────────────
|
|
4905
|
+
_EDITOR_PATRONES = (
|
|
4906
|
+
("renombrar", re.compile(
|
|
4907
|
+
r"\b(renombrar|rename|cambia(r| el nombre)( de| la)? (la )?(función|"
|
|
4908
|
+
r"funcion|variable|clase|método|metodo))\b", re.IGNORECASE)),
|
|
4909
|
+
("añadir_import", re.compile(
|
|
4910
|
+
r"\b(a[nñ]ad(i|ir)|importar|import|agregar)\s+(el\s+|la\s+)?"
|
|
4911
|
+
r"(import|módulo|modulo|librería|libreria|paquete)\b", re.IGNORECASE)),
|
|
4912
|
+
("refactorizar_clase", re.compile(
|
|
4913
|
+
r"\b(refactoriza(r)?|reestructura|rdivide|extraer\s+clase|"
|
|
4914
|
+
r"reorganiza(r)?)\b.*\b(clase|class|módulo|modulo)\b", re.IGNORECASE)),
|
|
4915
|
+
("añadir_funcion", re.compile(
|
|
4916
|
+
r"\b(a[nñ]ade?|a[nñ]adir|crear?|agrega(r)?)\s+(una?\s+)?"
|
|
4917
|
+
r"(función|funcion|función nueva|nueva funci|nuevo m[ée]todo|"
|
|
4918
|
+
r"m[ée]todo)\b", re.IGNORECASE)),
|
|
4919
|
+
("corregir_error", re.compile(
|
|
4920
|
+
r"\b(arregla|r|corrige|fix|bug|error|fallo|excepci[oó]n)\b",
|
|
4921
|
+
re.IGNORECASE)),
|
|
4922
|
+
)
|
|
4923
|
+
|
|
4924
|
+
|
|
4925
|
+
def _editor_clasificar_tarea(tarea: str) -> str:
|
|
4926
|
+
"""Clasifica una tarea de edición en un patrón conocido (v3.3.0).
|
|
4927
|
+
|
|
4928
|
+
Devuelve uno de: 'renombrar', 'añadir_import', 'refactorizar_clase',
|
|
4929
|
+
'añadir_funcion', 'corregir_error' o 'general'.
|
|
4930
|
+
"""
|
|
4931
|
+
texto = (tarea or "").strip()
|
|
4932
|
+
if not texto:
|
|
4933
|
+
return "general"
|
|
4934
|
+
for patron, regex in _EDITOR_PATRONES:
|
|
4935
|
+
if regex.search(texto):
|
|
4936
|
+
return patron
|
|
4937
|
+
return "general"
|
|
4938
|
+
|
|
4939
|
+
|
|
4940
|
+
def _skill_editor_guardar(tarea: str, archivo: str, patron: str,
|
|
4941
|
+
estrategia: str = "parche") -> Optional[int]:
|
|
4942
|
+
"""Guarda/actualiza un skill con el patrón de edición exitoso (v3.3.0).
|
|
4943
|
+
|
|
4944
|
+
Idempotente por nombre (`editor-<patrón>`): si ya existe se actualiza.
|
|
4945
|
+
Nunca lanza excepciones (los errores de memoria solo avisan).
|
|
4946
|
+
"""
|
|
4947
|
+
try:
|
|
4948
|
+
return _skill_guardar(
|
|
4949
|
+
nombre=f"editor-{patron}",
|
|
4950
|
+
consulta=tarea or f"editar {archivo}",
|
|
4951
|
+
pasos=[{
|
|
4952
|
+
"descripcion": (f"Edición '{patron}' aplicada con éxito "
|
|
4953
|
+
f"sobre {archivo}"),
|
|
4954
|
+
"accion": "editor_propio",
|
|
4955
|
+
"estrategia": estrategia,
|
|
4956
|
+
}],
|
|
4957
|
+
contexto={"archivo": archivo, "patron": patron,
|
|
4958
|
+
"estrategia": estrategia},
|
|
4959
|
+
descripcion=f"Patrón de edición del editor propio: {patron}")
|
|
4960
|
+
except Exception as exc: # pragma: no cover
|
|
4961
|
+
depurar(f"[skills-editor] No se pudo guardar el skill: {exc}")
|
|
4962
|
+
return None
|
|
4963
|
+
|
|
4964
|
+
|
|
4965
|
+
def _skill_editor_estrategia(tarea: str, umbral: float = 0.6) -> Optional[str]:
|
|
4966
|
+
"""Busca un skill de edición previo y devuelve su estrategia (v3.3.0).
|
|
4967
|
+
|
|
4968
|
+
Permite que el editor propio aplique directamente la estrategia que ya
|
|
4969
|
+
funcionó para tareas similares, sin pasar por el proveedor de IA.
|
|
4970
|
+
Solo se aceptan skills de editor no archivados y con confiabilidad >= 0.6.
|
|
4971
|
+
"""
|
|
4972
|
+
try:
|
|
4973
|
+
skill = _skill_buscar(f"editor {(tarea or '').strip()}", umbral=umbral)
|
|
4974
|
+
except Exception as exc:
|
|
4975
|
+
depurar(f"[skills-editor] Búsqueda falló: {exc}")
|
|
4976
|
+
return None
|
|
4977
|
+
if not skill or not str(skill.get("nombre", "")).startswith("editor-"):
|
|
4978
|
+
return None
|
|
4979
|
+
if float(skill.get("confiabilidad") or 0) < 0.6:
|
|
4980
|
+
return None
|
|
4981
|
+
for paso in skill.get("pasos") or []:
|
|
4982
|
+
estrategia = paso.get("estrategia")
|
|
4983
|
+
if estrategia in ("parche", "sobrescribir", "ast"):
|
|
4984
|
+
depurar(f"[skills-editor] Reutilizando estrategia "
|
|
4985
|
+
f"'{estrategia}' del skill #{skill.get('id')}.")
|
|
4986
|
+
return estrategia
|
|
4987
|
+
return None
|
|
4988
|
+
|
|
4989
|
+
|
|
4687
4990
|
def _skill_listar(incluir_archivados: bool = False,
|
|
4688
4991
|
solo_confiables: bool = False) -> List[dict]:
|
|
4689
4992
|
"""Lista skills ordenados por confiabilidad descendente."""
|
|
@@ -5457,18 +5760,69 @@ def _lenguaje_tree_sitter(ruta: str) -> Optional[str]:
|
|
|
5457
5760
|
"""Adivina el nombre de gramática tree-sitter para ``ruta``."""
|
|
5458
5761
|
extension = Path(ruta).suffix.lower().lstrip(".")
|
|
5459
5762
|
mapa = {
|
|
5460
|
-
"py": "python", "js": "javascript", "jsx": "javascript",
|
|
5461
|
-
"mjs": "javascript", "
|
|
5763
|
+
"py": "python", "pyi": "python", "js": "javascript", "jsx": "javascript",
|
|
5764
|
+
"mjs": "javascript", "cjs": "javascript", "ts": "typescript",
|
|
5765
|
+
"mts": "typescript", "cts": "typescript", "tsx": "tsx",
|
|
5462
5766
|
"dart": "dart", "go": "go", "rs": "rust", "java": "java",
|
|
5463
5767
|
"kt": "kotlin", "kts": "kotlin", "swift": "swift", "c": "c",
|
|
5464
|
-
"h": "c", "cpp": "cpp", "cc": "cpp", "
|
|
5465
|
-
"
|
|
5768
|
+
"h": "c", "cpp": "cpp", "cc": "cpp", "cxx": "cpp", "hpp": "cpp",
|
|
5769
|
+
"hh": "cpp", "hxx": "cpp", "cs": "c_sharp", "rb": "ruby",
|
|
5770
|
+
"php": "php", "sh": "bash", "bash": "bash", "zsh": "bash",
|
|
5466
5771
|
"json": "json", "yaml": "yaml", "yml": "yaml", "toml": "toml",
|
|
5467
|
-
"html": "html", "css": "css", "
|
|
5772
|
+
"html": "html", "htm": "html", "css": "css", "scss": "css",
|
|
5773
|
+
"md": "markdown", "scala": "scala", "lua": "lua", "sql": "sql",
|
|
5774
|
+
"ex": "elixir", "exs": "elixir", "zig": "zig", "hs": "haskell",
|
|
5775
|
+
"r": "r", "vue": "vue", "svelte": "svelte",
|
|
5468
5776
|
}
|
|
5469
5777
|
return mapa.get(extension)
|
|
5470
5778
|
|
|
5471
5779
|
|
|
5780
|
+
# ─── Detección de lenguaje por contenido (v3.3.0) ──────────────────────────
|
|
5781
|
+
_PATRONES_LENGUAJE_CONTENIDO = (
|
|
5782
|
+
(re.compile(r"^#!.*\bpython\S*", re.MULTILINE), "python"),
|
|
5783
|
+
(re.compile(r"^#!.*\b(bash|sh|zsh)\b", re.MULTILINE), "bash"),
|
|
5784
|
+
(re.compile(r"^#!.*\bnode\b", re.MULTILINE), "javascript"),
|
|
5785
|
+
(re.compile(r"^package\s+\w+", re.MULTILINE), "go"),
|
|
5786
|
+
(re.compile(r"<\?php", re.IGNORECASE), "php"),
|
|
5787
|
+
(re.compile(r"^\s*def\s+\w+\s*\(.*\)\s*:", re.MULTILINE), "python"),
|
|
5788
|
+
(re.compile(r"\bfunc(?:tion)?\s+\w*\s*\(", re.MULTILINE), "javascript"),
|
|
5789
|
+
(re.compile(r"^\s*(public|private)?\s*class\s+\w+", re.MULTILINE), "java"),
|
|
5790
|
+
(re.compile(r"\bfn\s+\w+\s*\(", re.MULTILINE), "rust"),
|
|
5791
|
+
)
|
|
5792
|
+
|
|
5793
|
+
|
|
5794
|
+
def _detectar_lenguaje_contenido(contenido: str) -> Optional[str]:
|
|
5795
|
+
"""Intenta adivinar el lenguaje a partir del contenido del archivo.
|
|
5796
|
+
|
|
5797
|
+
Se usa como refuerzo cuando la extensión es ambigua o desconocida
|
|
5798
|
+
(p. ej. scripts sin extensión, archivos generados, proyectos mixtos).
|
|
5799
|
+
"""
|
|
5800
|
+
if not contenido:
|
|
5801
|
+
return None
|
|
5802
|
+
muestra = contenido[:4096]
|
|
5803
|
+
for patron, lenguaje in _PATRONES_LENGUAJE_CONTENIDO:
|
|
5804
|
+
if patron.search(muestra):
|
|
5805
|
+
return lenguaje
|
|
5806
|
+
return None
|
|
5807
|
+
|
|
5808
|
+
|
|
5809
|
+
def _lenguaje_archivo(ruta: str,
|
|
5810
|
+
contenido: Optional[str] = None) -> Optional[str]:
|
|
5811
|
+
"""Detecta el lenguaje de ``ruta`` combinando extensión y contenido.
|
|
5812
|
+
|
|
5813
|
+
Prioridad: extensión conocida → heurística de contenido → None.
|
|
5814
|
+
Más robusto que la detección solo por extensión en proyectos mixtos.
|
|
5815
|
+
"""
|
|
5816
|
+
por_extension = _lenguaje_tree_sitter(ruta)
|
|
5817
|
+
if por_extension:
|
|
5818
|
+
return por_extension
|
|
5819
|
+
if contenido is None:
|
|
5820
|
+
leido = _leer_archivo(ruta)
|
|
5821
|
+
contenido = leido or ""
|
|
5822
|
+
return _detectar_lenguaje_contenido(contenido)
|
|
5823
|
+
|
|
5824
|
+
|
|
5825
|
+
|
|
5472
5826
|
def _extraer_simbolos_ts(arbol, lenguaje: str) -> dict:
|
|
5473
5827
|
"""Recorre el árbol tree-sitter y extrae funciones/clases/imports/llamadas."""
|
|
5474
5828
|
funciones: List[dict] = []
|
|
@@ -79,6 +79,8 @@ class TestDiffYParches(unittest.TestCase):
|
|
|
79
79
|
codigo_completo = "def fn(): return 2\n"
|
|
80
80
|
|
|
81
81
|
with mock.patch.object(sc, "_enviar_al_proveedor", side_effect=["no es un diff", codigo_completo]), \
|
|
82
|
+
mock.patch.object(sc, "_skill_editor_estrategia", return_value=None), \
|
|
83
|
+
mock.patch.object(sc, "_skill_editor_guardar", return_value=None), \
|
|
82
84
|
mock.patch.object(agente, "sobrescribir", return_value=True) as sob_mock:
|
|
83
85
|
ok = agente.ejecutar([archivo], "cambiar retorno a 2", directorio=str(self.raiz), modo_edicion="auto")
|
|
84
86
|
self.assertTrue(ok)
|
|
@@ -96,7 +98,7 @@ class TestFlagsEdicion(unittest.TestCase):
|
|
|
96
98
|
self.assertEqual(parser.parse_args(["--modo-edicion", "sobrescribir", "c"]).modo_edicion, "sobrescribir")
|
|
97
99
|
|
|
98
100
|
def test_version_es_2_1_0(self):
|
|
99
|
-
self.assertEqual(sc.VERSION, "3.
|
|
101
|
+
self.assertEqual(sc.VERSION, "3.3.0")
|
|
100
102
|
|
|
101
103
|
|
|
102
104
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Tests del editor propio v3.3.0.
|
|
4
|
+
|
|
5
|
+
Cubre el refinamiento multi-lenguaje del AST, el manejo de conflictos en
|
|
6
|
+
parches (validación previa, resolución incremental) y la integración con
|
|
7
|
+
el sistema de skills.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
import unittest
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from unittest import mock
|
|
17
|
+
|
|
18
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
19
|
+
|
|
20
|
+
import snapcontext as sc
|
|
21
|
+
from agentes import AgenteEditorPropio
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestClasificacionTareas(unittest.TestCase):
|
|
25
|
+
"""_editor_clasificar_tarea: patrones de edición reconocidos."""
|
|
26
|
+
|
|
27
|
+
def test_renombrar(self):
|
|
28
|
+
self.assertEqual(sc._editor_clasificar_tarea(
|
|
29
|
+
"renombrar la función calcular_total"), "renombrar")
|
|
30
|
+
|
|
31
|
+
def test_import(self):
|
|
32
|
+
self.assertEqual(sc._editor_clasificar_tarea(
|
|
33
|
+
"añadir import de typing"), "añadir_import")
|
|
34
|
+
|
|
35
|
+
def test_refactorizar(self):
|
|
36
|
+
self.assertEqual(sc._editor_clasificar_tarea(
|
|
37
|
+
"refactorizar la clase Carrito"), "refactorizar_clase")
|
|
38
|
+
|
|
39
|
+
def test_funcion_nueva(self):
|
|
40
|
+
self.assertEqual(sc._editor_clasificar_tarea(
|
|
41
|
+
"crear una función de validación"), "añadir_funcion")
|
|
42
|
+
|
|
43
|
+
def test_corregir_error(self):
|
|
44
|
+
self.assertEqual(sc._editor_clasificar_tarea(
|
|
45
|
+
"corrige el bug del checkout"), "corregir_error")
|
|
46
|
+
|
|
47
|
+
def test_general(self):
|
|
48
|
+
self.assertEqual(sc._editor_clasificar_tarea(
|
|
49
|
+
"mejorar rendimiento"), "general")
|
|
50
|
+
self.assertEqual(sc._editor_clasificar_tarea(""), "general")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestDeteccionLenguaje(unittest.TestCase):
|
|
54
|
+
"""Detección multi-lenguaje refinada (extensión + contenido)."""
|
|
55
|
+
|
|
56
|
+
def test_extensiones_nuevas(self):
|
|
57
|
+
self.assertEqual(sc._lenguaje_tree_sitter("a.mts"), "typescript")
|
|
58
|
+
self.assertEqual(sc._lenguaje_tree_sitter("a.cxx"), "cpp")
|
|
59
|
+
self.assertEqual(sc._lenguaje_tree_sitter("a.zsh"), "bash")
|
|
60
|
+
|
|
61
|
+
def test_por_contenido_shebang(self):
|
|
62
|
+
self.assertEqual(sc._detectar_lenguaje_contenido(
|
|
63
|
+
"#!/usr/bin/env python3\nprint(1)\n"), "python")
|
|
64
|
+
self.assertEqual(sc._detectar_lenguaje_contenido(
|
|
65
|
+
"#!/bin/bash\necho hola\n"), "bash")
|
|
66
|
+
|
|
67
|
+
def test_lenguaje_archivo_combina(self):
|
|
68
|
+
self.assertEqual(sc._lenguaje_archivo("script", "#!/bin/bash\n"),
|
|
69
|
+
"bash")
|
|
70
|
+
self.assertEqual(sc._lenguaje_archivo("main.go"), "go")
|
|
71
|
+
self.assertIsNone(sc._lenguaje_archivo("datos.xyz", "contenido"))
|
|
72
|
+
|
|
73
|
+
def test_resumen_ast_python_sin_extension(self):
|
|
74
|
+
resumen = sc._resumen_ast("def hola():\n pass\n", "sin_nombre")
|
|
75
|
+
self.assertTrue(resumen["ok"])
|
|
76
|
+
self.assertEqual(resumen["motor"], "ast")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TestRutaYValidacionParche(unittest.TestCase):
|
|
80
|
+
"""Extracción de ruta del parche y validación previa."""
|
|
81
|
+
|
|
82
|
+
PARCH = ("--- a/modulo.py\n+++ b/modulo.py\n@@ -1 +1 @@\n-viejo\n+nuevo\n")
|
|
83
|
+
|
|
84
|
+
def setUp(self):
|
|
85
|
+
self.tmp = tempfile.mkdtemp(prefix="sc330_")
|
|
86
|
+
|
|
87
|
+
def tearDown(self):
|
|
88
|
+
shutil.rmtree(self.tmp, ignore_errors=True)
|
|
89
|
+
|
|
90
|
+
def test_ruta_del_parche(self):
|
|
91
|
+
self.assertEqual(sc._ruta_del_parche(self.PARCH), "modulo.py")
|
|
92
|
+
sin_prefijo = self.PARCH.replace("b/", "").replace("a/", "")
|
|
93
|
+
self.assertEqual(sc._ruta_del_parche(sin_prefijo), "modulo.py")
|
|
94
|
+
self.assertIsNone(sc._ruta_del_parche(""))
|
|
95
|
+
self.assertIsNone(sc._ruta_del_parche("sin encabezados"))
|
|
96
|
+
|
|
97
|
+
def test_validacion_sin_referencia_ok(self):
|
|
98
|
+
ok, _ = sc._validar_parche_previo(self.PARCH, self.tmp, None)
|
|
99
|
+
self.assertTrue(ok)
|
|
100
|
+
|
|
101
|
+
def test_validacion_coincide(self):
|
|
102
|
+
(Path(self.tmp) / "modulo.py").write_text("viejo\n", encoding="utf-8")
|
|
103
|
+
ok, _ = sc._validar_parche_previo(self.PARCH, self.tmp, "viejo\n")
|
|
104
|
+
self.assertTrue(ok)
|
|
105
|
+
|
|
106
|
+
def test_validacion_detecta_cambio_concurrente(self):
|
|
107
|
+
(Path(self.tmp) / "modulo.py").write_text("otra cosa\n",
|
|
108
|
+
encoding="utf-8")
|
|
109
|
+
ok, detalle = sc._validar_parche_previo(self.PARCH, self.tmp,
|
|
110
|
+
"viejo\n")
|
|
111
|
+
self.assertFalse(ok)
|
|
112
|
+
self.assertIn("concurrente", detalle)
|
|
113
|
+
|
|
114
|
+
def test_validacion_archivo_inexistente(self):
|
|
115
|
+
ok, detalle = sc._validar_parche_previo(self.PARCH, self.tmp,
|
|
116
|
+
"viejo\n")
|
|
117
|
+
self.assertFalse(ok)
|
|
118
|
+
self.assertIn("no existe", detalle)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class TestParsearHunks(unittest.TestCase):
|
|
122
|
+
def test_hunks_basicos(self):
|
|
123
|
+
parche = ("--- a/f.py\n+++ b/f.py\n"
|
|
124
|
+
"@@ -1,3 +1,3 @@\n ctx\n-old\n+new\n ctx2\n")
|
|
125
|
+
hunks = sc._parsear_hunks(parche)
|
|
126
|
+
self.assertEqual(len(hunks), 1)
|
|
127
|
+
inicio, cambios = hunks[0]
|
|
128
|
+
self.assertEqual(inicio, 1)
|
|
129
|
+
self.assertEqual(cambios, [(" ", "ctx"), ("-", "old"),
|
|
130
|
+
("+", "new"), (" ", "ctx2")])
|
|
131
|
+
|
|
132
|
+
def test_sin_hunks(self):
|
|
133
|
+
self.assertEqual(sc._parsear_hunks(""), [])
|
|
134
|
+
self.assertEqual(sc._parsear_hunks("--- a/f.py\n+++ b/f.py\n"), [])
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class TestAplicacionIncremental(unittest.TestCase):
|
|
138
|
+
"""Resolución automática línea a línea sobre archivos reales."""
|
|
139
|
+
|
|
140
|
+
def setUp(self):
|
|
141
|
+
self.tmp = tempfile.mkdtemp(prefix="sc330inc_")
|
|
142
|
+
|
|
143
|
+
def tearDown(self):
|
|
144
|
+
shutil.rmtree(self.tmp, ignore_errors=True)
|
|
145
|
+
|
|
146
|
+
def _escribir(self, nombre: str, contenido: str) -> str:
|
|
147
|
+
(Path(self.tmp) / nombre).write_text(contenido, encoding="utf-8")
|
|
148
|
+
return nombre
|
|
149
|
+
|
|
150
|
+
def test_aplica_parche_limpio_linea_a_linea(self):
|
|
151
|
+
original = "linea 1\nlinea 2\nlinea 3\nlinea 4\nlinea 5\n"
|
|
152
|
+
nuevo = "linea 1\nlinea DOS\nlinea 3\nlinea CUATRO\nlinea 5\n"
|
|
153
|
+
nombre = self._escribir("f.py", original)
|
|
154
|
+
parche = sc._generar_parche(original, nuevo, nombre)
|
|
155
|
+
self.assertTrue(sc._aplicar_hunks_incremental(parche, self.tmp))
|
|
156
|
+
resultado = (Path(self.tmp) / nombre).read_text(encoding="utf-8")
|
|
157
|
+
self.assertEqual(resultado, nuevo)
|
|
158
|
+
|
|
159
|
+
def test_aplica_con_desfase_de_lineas(self):
|
|
160
|
+
# El archivo tiene 2 líneas extra al principio: git apply fallaría,
|
|
161
|
+
# pero la resolución incremental encuentra el bloque por desfase.
|
|
162
|
+
original = "linea 1\nlinea 2\nlinea 3\nlinea 4\nlinea 5\n"
|
|
163
|
+
nuevo = "linea 1\nlinea 2\nlinea 3\nlinea CUATRO\nlinea 5\n"
|
|
164
|
+
nombre = self._escribir("g.py",
|
|
165
|
+
"extra A\nextra B\n" + original)
|
|
166
|
+
parche = sc._generar_parche(original, nuevo, nombre)
|
|
167
|
+
self.assertTrue(sc._aplicar_hunks_incremental(parche, self.tmp))
|
|
168
|
+
resultado = (Path(self.tmp) / nombre).read_text(encoding="utf-8")
|
|
169
|
+
self.assertEqual(
|
|
170
|
+
resultado,
|
|
171
|
+
"extra A\nextra B\nlinea 1\nlinea 2\nlinea 3\n"
|
|
172
|
+
"linea CUATRO\nlinea 5\n")
|
|
173
|
+
|
|
174
|
+
def test_falla_si_nada_coincide(self):
|
|
175
|
+
self._escribir("h.py", "totalmente distinto\notra cosa\n")
|
|
176
|
+
parche = ("--- a/h.py\n+++ b/h.py\n@@ -1 +1 @@\n-viejo\n+nuevo\n")
|
|
177
|
+
self.assertFalse(sc._aplicar_hunks_incremental(parche, self.tmp))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TestResolucionConConflictos(unittest.TestCase):
|
|
181
|
+
"""_aplicar_parche_con_resolucion: flujo completo de resolución."""
|
|
182
|
+
|
|
183
|
+
def setUp(self):
|
|
184
|
+
self.tmp = tempfile.mkdtemp(prefix="sc330res_")
|
|
185
|
+
self.parche = ("--- a/m.py\n+++ b/m.py\n@@ -1 +1 @@\n-viejo\n+nuevo\n")
|
|
186
|
+
(Path(self.tmp) / "m.py").write_text("viejo\n", encoding="utf-8")
|
|
187
|
+
|
|
188
|
+
def tearDown(self):
|
|
189
|
+
shutil.rmtree(self.tmp, ignore_errors=True)
|
|
190
|
+
|
|
191
|
+
def test_git_apply_exitoso_no_requiere_resolucion(self):
|
|
192
|
+
with mock.patch.object(sc, "_aplicar_parche", return_value=True), \
|
|
193
|
+
mock.patch.object(sc, "_aplicar_hunks_incremental") as inc:
|
|
194
|
+
self.assertTrue(sc._aplicar_parche_con_resolucion(
|
|
195
|
+
self.parche, self.tmp))
|
|
196
|
+
inc.assert_not_called()
|
|
197
|
+
|
|
198
|
+
def test_conflicto_cae_a_resolucion_incremental(self):
|
|
199
|
+
with mock.patch.object(sc, "_aplicar_parche", return_value=False), \
|
|
200
|
+
mock.patch.object(sc, "_aplicar_hunks_incremental",
|
|
201
|
+
return_value=True) as inc:
|
|
202
|
+
self.assertTrue(sc._aplicar_parche_con_resolucion(
|
|
203
|
+
self.parche, self.tmp))
|
|
204
|
+
inc.assert_called_once_with(self.parche, self.tmp)
|
|
205
|
+
|
|
206
|
+
def test_validacion_previa_avisa_pero_intenta(self):
|
|
207
|
+
# Contenido esperado distinto del actual → aviso y reintento igual.
|
|
208
|
+
with mock.patch.object(sc, "_aplicar_parche", return_value=True), \
|
|
209
|
+
mock.patch.object(sc, "aviso") as avisos:
|
|
210
|
+
ok = sc._aplicar_parche_con_resolucion(
|
|
211
|
+
self.parche, self.tmp, contenido_esperado="otro\n")
|
|
212
|
+
self.assertTrue(ok)
|
|
213
|
+
self.assertTrue(any("Validación previa fallida" in str(c.args[0])
|
|
214
|
+
for c in avisos.call_args_list))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class TestSkillsEditor(unittest.TestCase):
|
|
218
|
+
"""Integración del editor propio con el sistema de skills."""
|
|
219
|
+
|
|
220
|
+
def test_guardar_skill_editor_usa_nombre_idempotente(self):
|
|
221
|
+
with mock.patch.object(sc, "_skill_guardar",
|
|
222
|
+
return_value=42) as guardar:
|
|
223
|
+
sid = sc._skill_editor_guardar("renombrar la función x",
|
|
224
|
+
"modulo.py", "renombrar",
|
|
225
|
+
estrategia="ast")
|
|
226
|
+
self.assertEqual(sid, 42)
|
|
227
|
+
kwargs = guardar.call_args.kwargs
|
|
228
|
+
self.assertEqual(kwargs["nombre"], "editor-renombrar")
|
|
229
|
+
self.assertEqual(kwargs["pasos"][0]["estrategia"], "ast")
|
|
230
|
+
self.assertEqual(kwargs["contexto"]["archivo"], "modulo.py")
|
|
231
|
+
|
|
232
|
+
def test_guardar_skill_nunca_lanza(self):
|
|
233
|
+
with mock.patch.object(sc, "_skill_guardar",
|
|
234
|
+
side_effect=RuntimeError("db rota")):
|
|
235
|
+
self.assertIsNone(sc._skill_editor_guardar("t", "a.py", "general"))
|
|
236
|
+
|
|
237
|
+
def test_estrategia_desde_skill_valido(self):
|
|
238
|
+
skill = {"id": 7, "nombre": "editor-renombrar",
|
|
239
|
+
"confiabilidad": 0.8,
|
|
240
|
+
"pasos": [{"accion": "editor_propio",
|
|
241
|
+
"estrategia": "parche"}]}
|
|
242
|
+
with mock.patch.object(sc, "_skill_buscar", return_value=skill):
|
|
243
|
+
self.assertEqual(
|
|
244
|
+
sc._skill_editor_estrategia("renombrar la función y"),
|
|
245
|
+
"parche")
|
|
246
|
+
|
|
247
|
+
def test_ignora_skills_que_no_son_del_editor(self):
|
|
248
|
+
skill = {"id": 1, "nombre": "tarea-cualquiera", "confiabilidad": 1.0,
|
|
249
|
+
"pasos": [{"estrategia": "parche"}]}
|
|
250
|
+
with mock.patch.object(sc, "_skill_buscar", return_value=skill):
|
|
251
|
+
self.assertIsNone(sc._skill_editor_estrategia("lo que sea"))
|
|
252
|
+
|
|
253
|
+
def test_ignora_baja_confiabilidad(self):
|
|
254
|
+
skill = {"id": 2, "nombre": "editor-general",
|
|
255
|
+
"confiabilidad": 0.3,
|
|
256
|
+
"pasos": [{"estrategia": "sobrescribir"}]}
|
|
257
|
+
with mock.patch.object(sc, "_skill_buscar", return_value=skill):
|
|
258
|
+
self.assertIsNone(sc._skill_editor_estrategia("lo que sea"))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class TestIntegracionSkillsEnEjecutar(unittest.TestCase):
|
|
262
|
+
"""AgenteEditorPropio.ejecutar reutiliza skills y guarda el patrón."""
|
|
263
|
+
|
|
264
|
+
def setUp(self):
|
|
265
|
+
self.tmp = tempfile.mkdtemp(prefix="sc330skills_")
|
|
266
|
+
|
|
267
|
+
def tearDown(self):
|
|
268
|
+
shutil.rmtree(self.tmp, ignore_errors=True)
|
|
269
|
+
|
|
270
|
+
def test_exito_guarda_patron_y_refuerza(self):
|
|
271
|
+
agente = AgenteEditorPropio()
|
|
272
|
+
archivo = "modulo.py"
|
|
273
|
+
(Path(self.tmp) / archivo).write_text("def fn(): return 1\n",
|
|
274
|
+
encoding="utf-8")
|
|
275
|
+
diff = ("--- a/modulo.py\n+++ b/modulo.py\n@@ -1 +1 @@\n"
|
|
276
|
+
"-def fn(): return 1\n+def fn(): return 2\n")
|
|
277
|
+
with mock.patch.object(sc, "_enviar_al_proveedor",
|
|
278
|
+
return_value=diff), \
|
|
279
|
+
mock.patch.object(agente, "aplicar_parche", return_value=True), \
|
|
280
|
+
mock.patch.object(sc, "_skill_editor_estrategia",
|
|
281
|
+
return_value=None) as buscar, \
|
|
282
|
+
mock.patch.object(sc, "_skill_editor_guardar",
|
|
283
|
+
return_value=9) as guardar, \
|
|
284
|
+
mock.patch.object(sc, "_skill_registrar_exito",
|
|
285
|
+
return_value=0.65) as reforzar:
|
|
286
|
+
ok = agente.ejecutar([archivo], "corrige el retorno",
|
|
287
|
+
directorio=self.tmp, modo_edicion="parche")
|
|
288
|
+
self.assertTrue(ok)
|
|
289
|
+
buscar.assert_called_once()
|
|
290
|
+
guardar.assert_called_once()
|
|
291
|
+
args, _kwargs = guardar.call_args
|
|
292
|
+
self.assertEqual(args[2], "corregir_error") # patrón clasificado
|
|
293
|
+
reforzar.assert_called_once_with(9)
|
|
294
|
+
|
|
295
|
+
def test_skill_aprendido_prioriza_estrategia(self):
|
|
296
|
+
agente = AgenteEditorPropio()
|
|
297
|
+
archivo = "modulo.py"
|
|
298
|
+
(Path(self.tmp) / archivo).write_text("x\n", encoding="utf-8")
|
|
299
|
+
with mock.patch.object(agente, "_cadena_modos",
|
|
300
|
+
return_value=["parche", "sobrescribir"]), \
|
|
301
|
+
mock.patch.object(sc, "_skill_editor_estrategia",
|
|
302
|
+
return_value="sobrescribir"), \
|
|
303
|
+
mock.patch.object(sc, "_skill_editor_guardar",
|
|
304
|
+
return_value=None), \
|
|
305
|
+
mock.patch.object(agente, "_aplicar_modo_sobrescribir",
|
|
306
|
+
return_value=True) as sob, \
|
|
307
|
+
mock.patch.object(agente, "_aplicar_modo_parche") as parche:
|
|
308
|
+
ok = agente.ejecutar([archivo], "cambia algo",
|
|
309
|
+
directorio=self.tmp, modo_edicion="auto")
|
|
310
|
+
self.assertTrue(ok)
|
|
311
|
+
# La estrategia aprendida ('sobrescribir') se ejecutó primero.
|
|
312
|
+
sob.assert_called_once()
|
|
313
|
+
parche.assert_not_called()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class TestPromptEditacionEnriquecido(unittest.TestCase):
|
|
317
|
+
"""El prompt de parche incluye lenguaje, tamaño e instrucciones."""
|
|
318
|
+
|
|
319
|
+
def setUp(self):
|
|
320
|
+
self.tmp = tempfile.mkdtemp(prefix="sc330prompt_")
|
|
321
|
+
|
|
322
|
+
def tearDown(self):
|
|
323
|
+
shutil.rmtree(self.tmp, ignore_errors=True)
|
|
324
|
+
|
|
325
|
+
def test_prompt_parche_contiene_lenguaje_e_instrucciones(self):
|
|
326
|
+
agente = AgenteEditorPropio()
|
|
327
|
+
diff = ("--- a/modulo.py\n+++ b/modulo.py\n@@ -1 +1 @@\n"
|
|
328
|
+
"-def fn(): return 1\n+def fn(): return 2\n")
|
|
329
|
+
with mock.patch.object(sc, "_enviar_al_proveedor",
|
|
330
|
+
return_value=diff) as enviar, \
|
|
331
|
+
mock.patch.object(agente, "aplicar_parche", return_value=True):
|
|
332
|
+
agente._aplicar_modo_parche(
|
|
333
|
+
"modulo.py", "cambiar retorno a 2",
|
|
334
|
+
contenido_actual="def fn(): return 1\n",
|
|
335
|
+
modelo=None, directorio=self.tmp)
|
|
336
|
+
prompt = enviar.call_args[0][2][0]["content"]
|
|
337
|
+
self.assertIn("lenguaje: python", prompt)
|
|
338
|
+
self.assertIn("Instrucciones:", prompt)
|
|
339
|
+
self.assertIn("Conserva el estilo existente", prompt)
|
|
340
|
+
self.assertIn("```python", prompt)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
if __name__ == "__main__":
|
|
344
|
+
unittest.main()
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
|
|
@@ -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.
|
|
177
|
+
self.assertEqual(sc.VERSION, "3.3.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.
|
|
87
|
+
self.assertEqual(sc.VERSION, "3.3.0")
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
if __name__ == "__main__":
|
|
@@ -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.
|
|
20
|
+
self.assertEqual(sc.VERSION, "3.3.0")
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class TestEstructuraJetBrains(unittest.TestCase):
|
|
@@ -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.
|
|
251
|
+
self.assertEqual(sc.VERSION, "3.3.0")
|
|
252
252
|
|
|
253
253
|
|
|
254
254
|
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.
|
|
185
|
+
self.assertEqual(sc.VERSION, "3.3.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.
|
|
272
|
+
self.assertEqual(sc.VERSION, "3.3.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.
|
|
213
|
+
self.assertEqual(sc.VERSION, "3.3.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.
|
|
171
|
+
self.assertEqual(sc.VERSION, "3.3.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.
|
|
129
|
+
self.assertEqual(sc.VERSION, "3.3.0")
|
|
130
130
|
|
|
131
131
|
|
|
132
132
|
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.
|
|
20
|
+
self.assertEqual(sc.VERSION, "3.3.0")
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class TestFrontendV160(unittest.TestCase):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{snapcontext-3.2.0 → snapcontext-3.3.0}/snapcontext.egg-info/__pycache__/top_level.cpython-313.pyc
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|