snapcontext 4.2.0__tar.gz → 4.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-4.2.0/snapcontext.egg-info → snapcontext-4.3.0}/PKG-INFO +58 -2
- {snapcontext-4.2.0 → snapcontext-4.3.0}/README.md +57 -1
- {snapcontext-4.2.0 → snapcontext-4.3.0}/agentes.py +6 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/pyproject.toml +2 -2
- {snapcontext-4.2.0 → snapcontext-4.3.0/snapcontext.egg-info}/PKG-INFO +58 -2
- {snapcontext-4.2.0 → snapcontext-4.3.0}/snapcontext.egg-info/SOURCES.txt +1 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/snapcontext.py +212 -10
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_auto_017.py +1 -1
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_diagnostico_310.py +1 -1
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_diff_editor_210.py +1 -1
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_editor_ast_220.py +1 -1
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_editor_propio_200.py +1 -1
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_exe_150.py +1 -1
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_jetbrains_170.py +1 -1
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_mcp_014.py +1 -1
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_mcp_140.py +1 -1
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_memoria_015.py +1 -1
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_memoria_300.py +1 -1
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_permisos_013.py +1 -1
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_plan_012.py +1 -1
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_plan_mcp_230.py +1 -1
- snapcontext-4.3.0/tests/test_sandbox_430.py +249 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_v010.py +1 -1
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_validacion_130.py +1 -1
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_vscode_016.py +1 -1
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_web_160.py +1 -1
- {snapcontext-4.2.0 → snapcontext-4.3.0}/LICENSE +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/orquestador.py +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/setup.cfg +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/snapcontext.egg-info/__pycache__/top_level.cpython-313.pyc +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/snapcontext.egg-info/dependency_links.txt +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/snapcontext.egg-info/entry_points.txt +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/snapcontext.egg-info/requires.txt +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/snapcontext.egg-info/top_level.txt +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_agentes.py +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_api_360.py +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_asesor_350.py +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_bienvenida_311.py +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_deteccion.py +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_editor_330.py +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_editor_410.py +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_embeddings_110.py +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_orquestador.py +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_plugins_400.py +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_seguridad_rendimiento_420.py +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_validacion_sintaxis_340.py +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_vscode_ts_320.py +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/tests/test_web_120.py +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/web/__init__.py +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/web/app.py +0 -0
- {snapcontext-4.2.0 → snapcontext-4.3.0}/web/static/index.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: snapcontext
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.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
|
|
@@ -45,7 +45,7 @@ Dynamic: license-file
|
|
|
45
45
|
|
|
46
46
|
# SnapContext
|
|
47
47
|
|
|
48
|
-

|
|
49
49
|
[](https://pypi.org/project/snapcontext/)
|
|
50
50
|
[](https://github.com/NicolasBruña24/snapcontext/actions)
|
|
51
51
|

|
|
@@ -85,6 +85,62 @@ pip install aider-chat # ediciones de código
|
|
|
85
85
|
snapcontext --init # asistente inicial + API key
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
+
## 🐳 Sandboxing con Docker (v4.3.0)
|
|
89
|
+
|
|
90
|
+
Ejecuta los comandos de SnapContext (bucle de pruebas, `execute_command`,
|
|
91
|
+
pasos `ejecutar` del planificador, plugins…) dentro de un **contenedor Docker
|
|
92
|
+
aislado**. Ideal cuando no se confía en el código o necesitas dependencias
|
|
93
|
+
específicas.
|
|
94
|
+
|
|
95
|
+
### Uso básico
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Todo el trabajo de comandos dentro del contenedor
|
|
99
|
+
snapcontext "arreglar el checkout" --test-loop --sandbox
|
|
100
|
+
|
|
101
|
+
# Con imagen personalizada y comando de preparación
|
|
102
|
+
snapcontext "añadir tests" --test-loop --sandbox \
|
|
103
|
+
--sandbox-imagen python:3.11-slim \
|
|
104
|
+
--sandbox-comando "pip install pytest"
|
|
105
|
+
|
|
106
|
+
# En el planificador: pasos "ejecutar" y MCP con comandos usan el sandbox;
|
|
107
|
+
# "editar" y "consultar" siguen en el host.
|
|
108
|
+
snapcontext "migrar a pytest" --plan --sandbox
|
|
109
|
+
|
|
110
|
+
# Imagen por defecto sin flag:
|
|
111
|
+
export SNAPCONTEXT_SANDBOX_IMAGE=ubuntu:22.04 # PowerShell: $env:SNAPCONTEXT_SANDBOX_IMAGE="..."
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Cómo funciona
|
|
115
|
+
|
|
116
|
+
- **Detección**: `_docker_disponible()` comprueba `docker --version` (binario)
|
|
117
|
+
y `docker info` (daemon activo).
|
|
118
|
+
- **Ejecución**: cada comando se lanza como
|
|
119
|
+
`docker run --rm -v "<proyecto>:/workspace" -w /workspace -e GEMINI_API_KEY … <imagen> sh -c "<comando>"`.
|
|
120
|
+
- **Montaje**: el directorio del proyecto se monta en `/workspace`.
|
|
121
|
+
- **Variables de entorno**: las claves (`*_API_KEY`, `OLLAMA_URL`, …) se pasan
|
|
122
|
+
al contenedor automáticamente.
|
|
123
|
+
- **Preparación**: `--sandbox-comando` antepone un comando de setup
|
|
124
|
+
(`apt update && apt install -y …`) antes del comando principal.
|
|
125
|
+
|
|
126
|
+
### Qué corre dentro y fuera del sandbox
|
|
127
|
+
|
|
128
|
+
| Dentro del contenedor | Fuera (host) |
|
|
129
|
+
| --- | --- |
|
|
130
|
+
| Bucle de pruebas (`--test-loop`) | Herramientas de solo lectura (`grep`, `read_file`, `list_files`, `ast`) |
|
|
131
|
+
| MCP `execute_command` (+ background) | Pasos `editar` y `consultar` del planificador |
|
|
132
|
+
| Pasos `ejecutar` del planificador | Selección de contexto, chat con la IA |
|
|
133
|
+
| Plugins y herramientas de usuario | |
|
|
134
|
+
|
|
135
|
+
### Errores y logs
|
|
136
|
+
|
|
137
|
+
- Cada comando sandboxeado registra `ℹ [sandbox] Ejecutando en contenedor: …`.
|
|
138
|
+
- Si un comando falla dentro del contenedor se muestra su salida y código como
|
|
139
|
+
siempre.
|
|
140
|
+
- Si Docker no está disponible y usaste `--sandbox` explícitamente,
|
|
141
|
+
SnapContext falla con un mensaje claro; sin el flag nunca cambia nada
|
|
142
|
+
(compatibilidad total).
|
|
143
|
+
|
|
88
144
|
## 🧰 Instalación y onboarding sin fricción (v3.1.x)
|
|
89
145
|
|
|
90
146
|
Objetivo: instalar SnapContext y empezar a usarlo en menos de 5 minutos.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SnapContext
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|
[](https://pypi.org/project/snapcontext/)
|
|
5
5
|
[](https://github.com/NicolasBruña24/snapcontext/actions)
|
|
6
6
|

|
|
@@ -40,6 +40,62 @@ pip install aider-chat # ediciones de código
|
|
|
40
40
|
snapcontext --init # asistente inicial + API key
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
## 🐳 Sandboxing con Docker (v4.3.0)
|
|
44
|
+
|
|
45
|
+
Ejecuta los comandos de SnapContext (bucle de pruebas, `execute_command`,
|
|
46
|
+
pasos `ejecutar` del planificador, plugins…) dentro de un **contenedor Docker
|
|
47
|
+
aislado**. Ideal cuando no se confía en el código o necesitas dependencias
|
|
48
|
+
específicas.
|
|
49
|
+
|
|
50
|
+
### Uso básico
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Todo el trabajo de comandos dentro del contenedor
|
|
54
|
+
snapcontext "arreglar el checkout" --test-loop --sandbox
|
|
55
|
+
|
|
56
|
+
# Con imagen personalizada y comando de preparación
|
|
57
|
+
snapcontext "añadir tests" --test-loop --sandbox \
|
|
58
|
+
--sandbox-imagen python:3.11-slim \
|
|
59
|
+
--sandbox-comando "pip install pytest"
|
|
60
|
+
|
|
61
|
+
# En el planificador: pasos "ejecutar" y MCP con comandos usan el sandbox;
|
|
62
|
+
# "editar" y "consultar" siguen en el host.
|
|
63
|
+
snapcontext "migrar a pytest" --plan --sandbox
|
|
64
|
+
|
|
65
|
+
# Imagen por defecto sin flag:
|
|
66
|
+
export SNAPCONTEXT_SANDBOX_IMAGE=ubuntu:22.04 # PowerShell: $env:SNAPCONTEXT_SANDBOX_IMAGE="..."
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Cómo funciona
|
|
70
|
+
|
|
71
|
+
- **Detección**: `_docker_disponible()` comprueba `docker --version` (binario)
|
|
72
|
+
y `docker info` (daemon activo).
|
|
73
|
+
- **Ejecución**: cada comando se lanza como
|
|
74
|
+
`docker run --rm -v "<proyecto>:/workspace" -w /workspace -e GEMINI_API_KEY … <imagen> sh -c "<comando>"`.
|
|
75
|
+
- **Montaje**: el directorio del proyecto se monta en `/workspace`.
|
|
76
|
+
- **Variables de entorno**: las claves (`*_API_KEY`, `OLLAMA_URL`, …) se pasan
|
|
77
|
+
al contenedor automáticamente.
|
|
78
|
+
- **Preparación**: `--sandbox-comando` antepone un comando de setup
|
|
79
|
+
(`apt update && apt install -y …`) antes del comando principal.
|
|
80
|
+
|
|
81
|
+
### Qué corre dentro y fuera del sandbox
|
|
82
|
+
|
|
83
|
+
| Dentro del contenedor | Fuera (host) |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| Bucle de pruebas (`--test-loop`) | Herramientas de solo lectura (`grep`, `read_file`, `list_files`, `ast`) |
|
|
86
|
+
| MCP `execute_command` (+ background) | Pasos `editar` y `consultar` del planificador |
|
|
87
|
+
| Pasos `ejecutar` del planificador | Selección de contexto, chat con la IA |
|
|
88
|
+
| Plugins y herramientas de usuario | |
|
|
89
|
+
|
|
90
|
+
### Errores y logs
|
|
91
|
+
|
|
92
|
+
- Cada comando sandboxeado registra `ℹ [sandbox] Ejecutando en contenedor: …`.
|
|
93
|
+
- Si un comando falla dentro del contenedor se muestra su salida y código como
|
|
94
|
+
siempre.
|
|
95
|
+
- Si Docker no está disponible y usaste `--sandbox` explícitamente,
|
|
96
|
+
SnapContext falla con un mensaje claro; sin el flag nunca cambia nada
|
|
97
|
+
(compatibilidad total).
|
|
98
|
+
|
|
43
99
|
## 🧰 Instalación y onboarding sin fricción (v3.1.x)
|
|
44
100
|
|
|
45
101
|
Objetivo: instalar SnapContext y empezar a usarlo en menos de 5 minutos.
|
|
@@ -702,6 +702,12 @@ class AgenteTester:
|
|
|
702
702
|
import snapcontext as sc
|
|
703
703
|
|
|
704
704
|
sc.depurar(f"[AgenteTester] Ejecutando pruebas: {' '.join(comando)}")
|
|
705
|
+
# v4.3.0: con --sandbox activo las pruebas corren en el contenedor.
|
|
706
|
+
if sc.sandbox_activo():
|
|
707
|
+
codigo, stdout, stderr = sc._ejecutar_pruebas_argv(
|
|
708
|
+
comando, directorio)
|
|
709
|
+
return subprocess.CompletedProcess(
|
|
710
|
+
comando, codigo, stdout=stdout, stderr=stderr)
|
|
705
711
|
return subprocess.run(
|
|
706
712
|
comando, cwd=directorio, capture_output=True, text=True
|
|
707
713
|
)
|
|
@@ -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 = "4.
|
|
9
|
+
version = "4.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"
|
|
@@ -89,4 +89,4 @@ packages = ["web"]
|
|
|
89
89
|
|
|
90
90
|
[tool.setuptools.package-data]
|
|
91
91
|
# El estático de la interfaz web debe viajar dentro del paquete instalado.
|
|
92
|
-
"web" = ["static/index.html"]
|
|
92
|
+
"web" = ["static/index.html"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: snapcontext
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.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
|
|
@@ -45,7 +45,7 @@ Dynamic: license-file
|
|
|
45
45
|
|
|
46
46
|
# SnapContext
|
|
47
47
|
|
|
48
|
-

|
|
49
49
|
[](https://pypi.org/project/snapcontext/)
|
|
50
50
|
[](https://github.com/NicolasBruña24/snapcontext/actions)
|
|
51
51
|

|
|
@@ -85,6 +85,62 @@ pip install aider-chat # ediciones de código
|
|
|
85
85
|
snapcontext --init # asistente inicial + API key
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
+
## 🐳 Sandboxing con Docker (v4.3.0)
|
|
89
|
+
|
|
90
|
+
Ejecuta los comandos de SnapContext (bucle de pruebas, `execute_command`,
|
|
91
|
+
pasos `ejecutar` del planificador, plugins…) dentro de un **contenedor Docker
|
|
92
|
+
aislado**. Ideal cuando no se confía en el código o necesitas dependencias
|
|
93
|
+
específicas.
|
|
94
|
+
|
|
95
|
+
### Uso básico
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Todo el trabajo de comandos dentro del contenedor
|
|
99
|
+
snapcontext "arreglar el checkout" --test-loop --sandbox
|
|
100
|
+
|
|
101
|
+
# Con imagen personalizada y comando de preparación
|
|
102
|
+
snapcontext "añadir tests" --test-loop --sandbox \
|
|
103
|
+
--sandbox-imagen python:3.11-slim \
|
|
104
|
+
--sandbox-comando "pip install pytest"
|
|
105
|
+
|
|
106
|
+
# En el planificador: pasos "ejecutar" y MCP con comandos usan el sandbox;
|
|
107
|
+
# "editar" y "consultar" siguen en el host.
|
|
108
|
+
snapcontext "migrar a pytest" --plan --sandbox
|
|
109
|
+
|
|
110
|
+
# Imagen por defecto sin flag:
|
|
111
|
+
export SNAPCONTEXT_SANDBOX_IMAGE=ubuntu:22.04 # PowerShell: $env:SNAPCONTEXT_SANDBOX_IMAGE="..."
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Cómo funciona
|
|
115
|
+
|
|
116
|
+
- **Detección**: `_docker_disponible()` comprueba `docker --version` (binario)
|
|
117
|
+
y `docker info` (daemon activo).
|
|
118
|
+
- **Ejecución**: cada comando se lanza como
|
|
119
|
+
`docker run --rm -v "<proyecto>:/workspace" -w /workspace -e GEMINI_API_KEY … <imagen> sh -c "<comando>"`.
|
|
120
|
+
- **Montaje**: el directorio del proyecto se monta en `/workspace`.
|
|
121
|
+
- **Variables de entorno**: las claves (`*_API_KEY`, `OLLAMA_URL`, …) se pasan
|
|
122
|
+
al contenedor automáticamente.
|
|
123
|
+
- **Preparación**: `--sandbox-comando` antepone un comando de setup
|
|
124
|
+
(`apt update && apt install -y …`) antes del comando principal.
|
|
125
|
+
|
|
126
|
+
### Qué corre dentro y fuera del sandbox
|
|
127
|
+
|
|
128
|
+
| Dentro del contenedor | Fuera (host) |
|
|
129
|
+
| --- | --- |
|
|
130
|
+
| Bucle de pruebas (`--test-loop`) | Herramientas de solo lectura (`grep`, `read_file`, `list_files`, `ast`) |
|
|
131
|
+
| MCP `execute_command` (+ background) | Pasos `editar` y `consultar` del planificador |
|
|
132
|
+
| Pasos `ejecutar` del planificador | Selección de contexto, chat con la IA |
|
|
133
|
+
| Plugins y herramientas de usuario | |
|
|
134
|
+
|
|
135
|
+
### Errores y logs
|
|
136
|
+
|
|
137
|
+
- Cada comando sandboxeado registra `ℹ [sandbox] Ejecutando en contenedor: …`.
|
|
138
|
+
- Si un comando falla dentro del contenedor se muestra su salida y código como
|
|
139
|
+
siempre.
|
|
140
|
+
- Si Docker no está disponible y usaste `--sandbox` explícitamente,
|
|
141
|
+
SnapContext falla con un mensaje claro; sin el flag nunca cambia nada
|
|
142
|
+
(compatibilidad total).
|
|
143
|
+
|
|
88
144
|
## 🧰 Instalación y onboarding sin fricción (v3.1.x)
|
|
89
145
|
|
|
90
146
|
Objetivo: instalar SnapContext y empezar a usarlo en menos de 5 minutos.
|
|
@@ -39,6 +39,7 @@ Open-source y pensado para ser fácil de extender (ver ejecutar_bucle_test).
|
|
|
39
39
|
|
|
40
40
|
import argparse
|
|
41
41
|
import ast
|
|
42
|
+
import contextlib
|
|
42
43
|
import difflib
|
|
43
44
|
import fnmatch
|
|
44
45
|
import json
|
|
@@ -111,7 +112,7 @@ except ImportError: # pragma: no cover
|
|
|
111
112
|
import concurrent.futures
|
|
112
113
|
from concurrent.futures import ThreadPoolExecutor
|
|
113
114
|
|
|
114
|
-
VERSION = "4.
|
|
115
|
+
VERSION = "4.3.0"
|
|
115
116
|
|
|
116
117
|
# v3.1.0 — Claves de API reconocidas para el modo por defecto (offline).
|
|
117
118
|
CLAVES_API_CONOCIDAS = (
|
|
@@ -2532,7 +2533,9 @@ def ejecutar_bucle_test(consulta: str, archivos: List[str], directorio: str,
|
|
|
2532
2533
|
"""
|
|
2533
2534
|
if not comando_test:
|
|
2534
2535
|
raise RuntimeError("El comando de pruebas está vacío (--comando-test).")
|
|
2535
|
-
|
|
2536
|
+
# v4.3.0: en sandbox el binario vive dentro del contenedor; la comprobación
|
|
2537
|
+
# de PATH del host no aplica.
|
|
2538
|
+
if not _SANDBOX_ACTIVO and shutil.which(comando_test[0]) is None:
|
|
2536
2539
|
raise RuntimeError(
|
|
2537
2540
|
f"No se encontró el comando de pruebas '{comando_test[0]}'. "
|
|
2538
2541
|
"Ajusta --comando-test."
|
|
@@ -2556,9 +2559,16 @@ def ejecutar_bucle_test(consulta: str, archivos: List[str], directorio: str,
|
|
|
2556
2559
|
|
|
2557
2560
|
solicitud = " ".join(comando_test)
|
|
2558
2561
|
info(f"Ejecutando pruebas: {solicitud}")
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
+
# v4.3.0: con --sandbox las pruebas corren dentro del contenedor.
|
|
2563
|
+
if _SANDBOX_ACTIVO:
|
|
2564
|
+
codigo, stdout, stderr = _ejecutar_pruebas_argv(
|
|
2565
|
+
comando_test, directorio)
|
|
2566
|
+
resultado = subprocess.CompletedProcess(
|
|
2567
|
+
comando_test, codigo, stdout=stdout, stderr=stderr)
|
|
2568
|
+
else:
|
|
2569
|
+
resultado = subprocess.run(
|
|
2570
|
+
comando_test, cwd=directorio, capture_output=True, text=True
|
|
2571
|
+
)
|
|
2562
2572
|
if resultado.returncode == 0:
|
|
2563
2573
|
exito(f"¡Pruebas superadas en la iteración {iteracion}!")
|
|
2564
2574
|
return True
|
|
@@ -3213,6 +3223,11 @@ def _ejecutar_comando(comando: str, directorio: str = ".",
|
|
|
3213
3223
|
raiz = Path(directorio).expanduser()
|
|
3214
3224
|
if not raiz.is_dir():
|
|
3215
3225
|
return (-1, "", f"El directorio no existe: {raiz}")
|
|
3226
|
+
# v4.3.0: con --sandbox activo el comando corre dentro del contenedor.
|
|
3227
|
+
if _SANDBOX_ACTIVO:
|
|
3228
|
+
info(f"[sandbox] Ejecutando en contenedor: {comando}")
|
|
3229
|
+
comando = _envolver_sandbox(comando, str(raiz))
|
|
3230
|
+
raiz = Path.cwd() # docker se lanza desde el host; el mount ya es absoluto
|
|
3216
3231
|
try:
|
|
3217
3232
|
proc = subprocess.run(
|
|
3218
3233
|
comando,
|
|
@@ -3231,6 +3246,155 @@ def _ejecutar_comando(comando: str, directorio: str = ".",
|
|
|
3231
3246
|
except OSError as exc:
|
|
3232
3247
|
return (-1, "", f"Error ejecutando '{comando}': {exc}")
|
|
3233
3248
|
|
|
3249
|
+
|
|
3250
|
+
# ---------------------------------------------------------------------------
|
|
3251
|
+
# 🐳 Sandboxing con Docker (v4.3.0)
|
|
3252
|
+
# ---------------------------------------------------------------------------
|
|
3253
|
+
# Imagen por defecto del sandbox (ligera, con Python y herramientas comunes).
|
|
3254
|
+
# Puede sobrescribirse con --sandbox-imagen o SNAPCONTEXT_SANDBOX_IMAGE.
|
|
3255
|
+
SANDBOX_IMAGEN_DEFECTO = "python:3.11-slim"
|
|
3256
|
+
SANDBOX_DIR_TRABAJO = "/workspace"
|
|
3257
|
+
# Nombres/patrones de variables de entorno que se pasan al contenedor.
|
|
3258
|
+
_SANDBOX_VARS_CLAVE = ("GEMINI_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY",
|
|
3259
|
+
"DEEPSEEK_API_KEY", "GROQ_API_KEY", "OLLAMA_URL",
|
|
3260
|
+
"SNAPCONTEXT_SANDBOX_IMAGE")
|
|
3261
|
+
|
|
3262
|
+
# Estado global del sandbox (lo activa _activar_sandbox desde main()).
|
|
3263
|
+
_SANDBOX_ACTIVO: bool = False
|
|
3264
|
+
_SANDBOX_IMAGEN: str = SANDBOX_IMAGEN_DEFECTO
|
|
3265
|
+
_SANDBOX_COMANDO_PREP: Optional[str] = None
|
|
3266
|
+
|
|
3267
|
+
|
|
3268
|
+
def _docker_disponible() -> bool:
|
|
3269
|
+
"""Comprueba que Docker está instalado Y que el daemon está en ejecución.
|
|
3270
|
+
|
|
3271
|
+
- ``docker --version`` existe en el PATH.
|
|
3272
|
+
- ``docker info`` responde sin error (daemon activo).
|
|
3273
|
+
|
|
3274
|
+
Nunca lanza excepciones; devuelve ``False`` ante cualquier problema.
|
|
3275
|
+
"""
|
|
3276
|
+
if shutil.which("docker") is None:
|
|
3277
|
+
return False
|
|
3278
|
+
try:
|
|
3279
|
+
proc = subprocess.run(
|
|
3280
|
+
["docker", "info"], capture_output=True, text=True,
|
|
3281
|
+
timeout=30, creationflags=(
|
|
3282
|
+
subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0),
|
|
3283
|
+
)
|
|
3284
|
+
return proc.returncode == 0
|
|
3285
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
3286
|
+
return False
|
|
3287
|
+
|
|
3288
|
+
|
|
3289
|
+
def _sandbox_imagen_resuelta(explicita: Optional[str] = None) -> str:
|
|
3290
|
+
"""Resuelve la imagen del sandbox: flag > variable de entorno > defecto."""
|
|
3291
|
+
return (explicita or os.environ.get("SNAPCONTEXT_SANDBOX_IMAGE", "").strip()
|
|
3292
|
+
or SANDBOX_IMAGEN_DEFECTO)
|
|
3293
|
+
|
|
3294
|
+
|
|
3295
|
+
def _activar_sandbox(imagen: Optional[str] = None,
|
|
3296
|
+
comando_prep: Optional[str] = None,
|
|
3297
|
+
estricto: bool = True) -> bool:
|
|
3298
|
+
"""Activa el sandbox global si Docker está disponible.
|
|
3299
|
+
|
|
3300
|
+
Args:
|
|
3301
|
+
imagen: imagen Docker (--sandbox-imagen o env por defecto).
|
|
3302
|
+
comando_prep: comando de preparación previo (--sandbox-comando).
|
|
3303
|
+
estricto: si es ``True`` (``--sandbox`` explícito) y Docker no está
|
|
3304
|
+
disponible lanza ``RuntimeError``; si es ``False`` solo avisa y
|
|
3305
|
+
continúa sin sandbox.
|
|
3306
|
+
|
|
3307
|
+
Devuelve ``True`` si el sandbox quedó activo.
|
|
3308
|
+
"""
|
|
3309
|
+
global _SANDBOX_ACTIVO, _SANDBOX_IMAGEN, _SANDBOX_COMANDO_PREP
|
|
3310
|
+
if not _docker_disponible():
|
|
3311
|
+
mensaje = (
|
|
3312
|
+
"--sandbox solicitado pero Docker no está disponible "
|
|
3313
|
+
"(¿instalado? ¿el daemon está en ejecución?). "
|
|
3314
|
+
"Instala Docker Desktop o inicia el servicio 'docker'."
|
|
3315
|
+
)
|
|
3316
|
+
_SANDBOX_ACTIVO = False
|
|
3317
|
+
if estricto:
|
|
3318
|
+
raise RuntimeError(mensaje)
|
|
3319
|
+
aviso(mensaje + "\n → Se continúa SIN sandbox.")
|
|
3320
|
+
return False
|
|
3321
|
+
_SANDBOX_ACTIVO = True
|
|
3322
|
+
_SANDBOX_IMAGEN = _sandbox_imagen_resuelta(imagen)
|
|
3323
|
+
_SANDBOX_COMANDO_PREP = (comando_prep or "").strip() or None
|
|
3324
|
+
exito(f"🐳 Sandbox activo (imagen: {_SANDBOX_IMAGEN}, "
|
|
3325
|
+
f"directorio montado en {SANDBOX_DIR_TRABAJO}).")
|
|
3326
|
+
return True
|
|
3327
|
+
|
|
3328
|
+
|
|
3329
|
+
def _desactivar_sandbox() -> None:
|
|
3330
|
+
"""Desactiva el sandbox global (vuelve al comportamiento normal)."""
|
|
3331
|
+
global _SANDBOX_ACTIVO, _SANDBOX_COMANDO_PREP
|
|
3332
|
+
_SANDBOX_ACTIVO = False
|
|
3333
|
+
_SANDBOX_COMANDO_PREP = None
|
|
3334
|
+
|
|
3335
|
+
|
|
3336
|
+
def sandbox_activo() -> bool:
|
|
3337
|
+
"""Indica si el sandbox Docker está activo."""
|
|
3338
|
+
return _SANDBOX_ACTIVO
|
|
3339
|
+
|
|
3340
|
+
|
|
3341
|
+
def _envolver_sandbox(comando: str, directorio: str = ".") -> str:
|
|
3342
|
+
"""Envuelve ``comando`` en un ``docker run`` dentro del sandbox.
|
|
3343
|
+
|
|
3344
|
+
Genera algo como::
|
|
3345
|
+
|
|
3346
|
+
docker run --rm -v "<dir>:/workspace" -w /workspace \
|
|
3347
|
+
-e GEMINI_API_KEY ... <imagen> sh -c "<comando>"
|
|
3348
|
+
|
|
3349
|
+
Si hay comando de preparación (--sandbox-comando), se antepone con
|
|
3350
|
+
``&&``. Sin sandbox activo devuelve ``comando`` tal cual.
|
|
3351
|
+
"""
|
|
3352
|
+
if not _SANDBOX_ACTIVO:
|
|
3353
|
+
return comando
|
|
3354
|
+
raiz = Path(directorio).expanduser().resolve()
|
|
3355
|
+
partes = ["docker", "run", "--rm",
|
|
3356
|
+
"-v", f"{raiz}:{SANDBOX_DIR_TRABAJO}",
|
|
3357
|
+
"-w", SANDBOX_DIR_TRABAJO]
|
|
3358
|
+
# Pasar las variables de entorno relevantes del host al contenedor.
|
|
3359
|
+
vistas = set()
|
|
3360
|
+
for nombre in list(os.environ):
|
|
3361
|
+
incluir = nombre in _SANDBOX_VARS_CLAVE or nombre.endswith("_API_KEY")
|
|
3362
|
+
if incluir and nombre not in vistas:
|
|
3363
|
+
vistas.add(nombre)
|
|
3364
|
+
partes.extend(["-e", nombre])
|
|
3365
|
+
partes.append(_SANDBOX_IMAGEN)
|
|
3366
|
+
if _SANDBOX_COMANDO_PREP:
|
|
3367
|
+
comando = f"{_SANDBOX_COMANDO_PREP} && ({comando})"
|
|
3368
|
+
partes.extend(["sh", "-c", comando])
|
|
3369
|
+
return shlex.join(partes)
|
|
3370
|
+
|
|
3371
|
+
|
|
3372
|
+
def _ejecutar_pruebas_argv(comando: List[str], directorio: str) -> tuple:
|
|
3373
|
+
"""Ejecuta un comando de pruebas (lista argv) respetando el sandbox.
|
|
3374
|
+
|
|
3375
|
+
Devuelve ``(codigo_retorno, stdout, stderr)`` como :func:`_ejecutar_comando`.
|
|
3376
|
+
"""
|
|
3377
|
+
if not comando:
|
|
3378
|
+
return (-1, "", "El comando de pruebas está vacío.")
|
|
3379
|
+
return _ejecutar_comando(" ".join(comando), directorio, timeout=1800)
|
|
3380
|
+
|
|
3381
|
+
|
|
3382
|
+
@contextlib.contextmanager
|
|
3383
|
+
def _sandbox_pausado():
|
|
3384
|
+
"""Desactiva temporalmente el sandbox (herramientas de solo lectura).
|
|
3385
|
+
|
|
3386
|
+
Las herramientas MCP que no modifican el sistema (grep, git_status,
|
|
3387
|
+
git_diff...) se ejecutan fuera del contenedor para mayor velocidad.
|
|
3388
|
+
"""
|
|
3389
|
+
global _SANDBOX_ACTIVO
|
|
3390
|
+
previo = _SANDBOX_ACTIVO
|
|
3391
|
+
_SANDBOX_ACTIVO = False
|
|
3392
|
+
try:
|
|
3393
|
+
yield
|
|
3394
|
+
finally:
|
|
3395
|
+
_SANDBOX_ACTIVO = previo
|
|
3396
|
+
|
|
3397
|
+
|
|
3234
3398
|
# --- Procesos en segundo plano para execute_command (v2.3.0) -----------------
|
|
3235
3399
|
_PROCESOS_FONDO: dict = {} # pid → estado (para ejecución en background)
|
|
3236
3400
|
|
|
@@ -3245,6 +3409,11 @@ def _lanzar_proceso_fondo(comando: str, directorio: str = ".",
|
|
|
3245
3409
|
raiz = Path(directorio).expanduser()
|
|
3246
3410
|
if not raiz.is_dir():
|
|
3247
3411
|
return {"ok": False, "error": f"El directorio no existe: {raiz}"}
|
|
3412
|
+
# v4.3.0: los procesos en segundo plano también respetan --sandbox.
|
|
3413
|
+
if _SANDBOX_ACTIVO:
|
|
3414
|
+
info(f"[sandbox] Ejecutando en contenedor (background): {comando}")
|
|
3415
|
+
comando = _envolver_sandbox(comando, str(raiz))
|
|
3416
|
+
raiz = Path.cwd()
|
|
3248
3417
|
try:
|
|
3249
3418
|
if capture_output:
|
|
3250
3419
|
proc = subprocess.Popen(
|
|
@@ -6292,7 +6461,11 @@ def _tool_grep(patron: str, directorio: str = ".",
|
|
|
6292
6461
|
comando = f'grep -rn -i -m 5 "{patron}" .'
|
|
6293
6462
|
else:
|
|
6294
6463
|
comando = f'findstr /s /n /i "{patron}" *.py *.dart *.js *.ts *.go *.rs'
|
|
6295
|
-
codigo, stdout, stderr =
|
|
6464
|
+
codigo, stdout, stderr = (None, None, None)
|
|
6465
|
+
# v4.3.0: grep es de solo lectura → corre fuera del sandbox.
|
|
6466
|
+
with _sandbox_pausado():
|
|
6467
|
+
codigo, stdout, stderr = _ejecutar_comando(comando, directorio,
|
|
6468
|
+
timeout=60)
|
|
6296
6469
|
lineas = [l for l in (stdout or "").splitlines() if l.strip()]
|
|
6297
6470
|
return {"ok": codigo == 0 or bool(lineas),
|
|
6298
6471
|
"buscador": herramienta, "total": len(lineas),
|
|
@@ -6381,8 +6554,9 @@ def _tool_git_status(directorio: str = ".") -> dict:
|
|
|
6381
6554
|
return {"ok": False, "error": f"'{directorio}' no es un repositorio git"}
|
|
6382
6555
|
_, rama, _ = _ejecutar_comando("git rev-parse --abbrev-ref HEAD",
|
|
6383
6556
|
directorio, timeout=15)
|
|
6384
|
-
|
|
6385
|
-
|
|
6557
|
+
with _sandbox_pausado(): # v4.3.0: solo lectura → fuera del sandbox
|
|
6558
|
+
codigo, salida, _ = _ejecutar_comando("git status --porcelain",
|
|
6559
|
+
directorio, timeout=30)
|
|
6386
6560
|
modificados = [l.strip() for l in (salida or "").splitlines() if l.strip()]
|
|
6387
6561
|
return {"ok": codigo == 0, "rama": (rama or "").strip(),
|
|
6388
6562
|
"cambios": modificados, "total_cambios": len(modificados)}
|
|
@@ -6396,7 +6570,9 @@ def _tool_git_diff(directorio: str = ".", archivo: Optional[str] = None,
|
|
|
6396
6570
|
comando = "git diff HEAD"
|
|
6397
6571
|
if archivo:
|
|
6398
6572
|
comando += f' -- "{archivo}"'
|
|
6399
|
-
|
|
6573
|
+
with _sandbox_pausado(): # v4.3.0: solo lectura → fuera del sandbox
|
|
6574
|
+
codigo, salida, stderr = _ejecutar_comando(comando, directorio,
|
|
6575
|
+
timeout=60)
|
|
6400
6576
|
lineas = (salida or "").splitlines()
|
|
6401
6577
|
return {"ok": codigo == 0, "archivo": archivo,
|
|
6402
6578
|
"total_lineas": len(lineas),
|
|
@@ -7580,7 +7756,8 @@ CATEGORIAS_AYUDA = (
|
|
|
7580
7756
|
("--git-commit", "--no-git-commit", "--branch")),
|
|
7581
7757
|
("Planificador y bucles",
|
|
7582
7758
|
("--paralelo", "--max-intentos", "--test-loop", "--server-loop",
|
|
7583
|
-
"--manual-loop", "--comando-test", "--
|
|
7759
|
+
"--manual-loop", "--comando-test", "--sandbox", "--sandbox-imagen",
|
|
7760
|
+
"--sandbox-comando", "--dispositivo", "--url-defecto",
|
|
7584
7761
|
"--max-iteraciones")),
|
|
7585
7762
|
("Otros",
|
|
7586
7763
|
("consulta", "--directorio", "--aider-opciones", "--depurar",
|
|
@@ -8517,6 +8694,23 @@ def crear_parser() -> argparse.ArgumentParser:
|
|
|
8517
8694
|
"--comando-test", default=COMANDO_TEST_DEFECTO,
|
|
8518
8695
|
help='Comando de pruebas del bucle (por defecto: "flutter test").',
|
|
8519
8696
|
)
|
|
8697
|
+
# v4.3.0: sandbox opcional con Docker.
|
|
8698
|
+
parser.add_argument(
|
|
8699
|
+
"--sandbox", action="store_true",
|
|
8700
|
+
help="(v4.3.0) Ejecuta comandos y pruebas dentro de un contenedor "
|
|
8701
|
+
"Docker aislado (monta el proyecto en /workspace). Si Docker no "
|
|
8702
|
+
"está disponible, falla con error claro.",
|
|
8703
|
+
)
|
|
8704
|
+
parser.add_argument(
|
|
8705
|
+
"--sandbox-imagen", dest="sandbox_imagen", default=None,
|
|
8706
|
+
help="Imagen Docker del sandbox (por defecto: python:3.11-slim o "
|
|
8707
|
+
"SNAPCONTEXT_SANDBOX_IMAGE).",
|
|
8708
|
+
)
|
|
8709
|
+
parser.add_argument(
|
|
8710
|
+
"--sandbox-comando", dest="sandbox_comando", default=None,
|
|
8711
|
+
help='Comando de preparación dentro del contenedor antes del comando '
|
|
8712
|
+
'principal (ej.: "apt update && apt install -y make").',
|
|
8713
|
+
)
|
|
8520
8714
|
parser.add_argument(
|
|
8521
8715
|
"--max-iteraciones", type=int, default=MAX_ITERACIONES_TEST_DEFECTO,
|
|
8522
8716
|
help="Máximo de iteraciones del bucle de pruebas.",
|
|
@@ -9497,6 +9691,14 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
9497
9691
|
# para todos los modos (chat, planificador, ...).
|
|
9498
9692
|
global CONFIRMAR_ACCIONES
|
|
9499
9693
|
CONFIRMAR_ACCIONES = getattr(args, "confirmar", True)
|
|
9694
|
+
# v4.3.0: activar el sandbox Docker si se pidió --sandbox. Es lo
|
|
9695
|
+
# primero para que TODO comando posterior ya nazca en el contenedor
|
|
9696
|
+
# (bucle de pruebas, MCP execute_command, pasos "ejecutar" del plan).
|
|
9697
|
+
if getattr(args, "sandbox", False):
|
|
9698
|
+
_activar_sandbox(
|
|
9699
|
+
imagen=getattr(args, "sandbox_imagen", None),
|
|
9700
|
+
comando_prep=getattr(args, "sandbox_comando", None),
|
|
9701
|
+
estricto=True)
|
|
9500
9702
|
# v3.1.1: --bienvenida explícito ejecuta el tutorial y marca el
|
|
9501
9703
|
# primer uso como completado (por si quiere volver a verlo).
|
|
9502
9704
|
if getattr(args, "bienvenida", False):
|
|
@@ -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, "4.
|
|
101
|
+
self.assertEqual(sc.VERSION, "4.3.0")
|
|
102
102
|
|
|
103
103
|
|
|
104
104
|
if __name__ == "__main__":
|
|
@@ -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, "4.
|
|
177
|
+
self.assertEqual(sc.VERSION, "4.3.0")
|
|
178
178
|
|
|
179
179
|
|
|
180
180
|
if __name__ == "__main__":
|
|
@@ -86,7 +86,7 @@ class TestFlagsEditor(unittest.TestCase):
|
|
|
86
86
|
self.assertIsInstance(orch.agente_editor_propio, AgenteEditorPropio)
|
|
87
87
|
|
|
88
88
|
def test_version_es_2_0_0(self):
|
|
89
|
-
self.assertEqual(sc.VERSION, "4.
|
|
89
|
+
self.assertEqual(sc.VERSION, "4.3.0")
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
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, "4.
|
|
20
|
+
self.assertEqual(sc.VERSION, "4.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, "4.
|
|
251
|
+
self.assertEqual(sc.VERSION, "4.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, "4.
|
|
185
|
+
self.assertEqual(sc.VERSION, "4.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, "4.
|
|
272
|
+
self.assertEqual(sc.VERSION, "4.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, "4.
|
|
213
|
+
self.assertEqual(sc.VERSION, "4.3.0")
|
|
214
214
|
|
|
215
215
|
def test_flag_paralelo(self):
|
|
216
216
|
args = sc.crear_parser().parse_args(["--plan", "t", "--paralelo", "3"])
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Tests de la v4.3.0: sandbox opcional con Docker."""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
import unittest
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from unittest import mock
|
|
12
|
+
|
|
13
|
+
RAIZ = Path(__file__).resolve().parent.parent
|
|
14
|
+
sys.path.insert(0, str(RAIZ))
|
|
15
|
+
|
|
16
|
+
import snapcontext as sc # noqa: E402
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _args_base(**extra) -> argparse.Namespace:
|
|
20
|
+
base = {"consulta": None, "depurar": False, "auto": False,
|
|
21
|
+
"confirmar": True, "modelo": None, "test_loop": False,
|
|
22
|
+
"aider_opciones": "", "comando_test": "pytest"}
|
|
23
|
+
base.update(extra or {})
|
|
24
|
+
return argparse.Namespace(**base)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestDeteccionDocker(unittest.TestCase):
|
|
28
|
+
"""Detección de Docker: presencia, ausencia y daemon parado."""
|
|
29
|
+
|
|
30
|
+
def test_sin_binario_en_path_no_disponible(self):
|
|
31
|
+
with mock.patch.object(sc.shutil, "which", return_value=None):
|
|
32
|
+
self.assertFalse(sc._docker_disponible())
|
|
33
|
+
|
|
34
|
+
def test_docker_info_ok_disponible(self):
|
|
35
|
+
proc = mock.Mock(returncode=0)
|
|
36
|
+
with mock.patch.object(sc.shutil, "which", return_value="docker"), \
|
|
37
|
+
mock.patch.object(sc.subprocess, "run", return_value=proc):
|
|
38
|
+
self.assertTrue(sc._docker_disponible())
|
|
39
|
+
|
|
40
|
+
def test_docker_daemon_parado_no_disponible(self):
|
|
41
|
+
proc = mock.Mock(returncode=1)
|
|
42
|
+
with mock.patch.object(sc.shutil, "which", return_value="docker"), \
|
|
43
|
+
mock.patch.object(sc.subprocess, "run", return_value=proc):
|
|
44
|
+
self.assertFalse(sc._docker_disponible())
|
|
45
|
+
|
|
46
|
+
def test_error_al_llamar_docker_tolerado(self):
|
|
47
|
+
with mock.patch.object(sc.shutil, "which", return_value="docker"), \
|
|
48
|
+
mock.patch.object(sc.subprocess, "run",
|
|
49
|
+
side_effect=OSError("boom")):
|
|
50
|
+
self.assertFalse(sc._docker_disponible())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestActivacionSandbox(unittest.TestCase):
|
|
54
|
+
def setUp(self):
|
|
55
|
+
sc._desactivar_sandbox()
|
|
56
|
+
|
|
57
|
+
def tearDown(self):
|
|
58
|
+
sc._desactivar_sandbox()
|
|
59
|
+
|
|
60
|
+
def test_activacion_estricta_falla_sin_docker(self):
|
|
61
|
+
with mock.patch.object(sc, "_docker_disponible", return_value=False):
|
|
62
|
+
with self.assertRaises(RuntimeError):
|
|
63
|
+
sc._activar_sandbox(estricto=True)
|
|
64
|
+
self.assertFalse(sc.sandbox_activo())
|
|
65
|
+
|
|
66
|
+
def test_activacion_no_estricta_avisa_y_continua(self):
|
|
67
|
+
with mock.patch.object(sc, "_docker_disponible", return_value=False), \
|
|
68
|
+
mock.patch.object(sc, "aviso") as fake_aviso:
|
|
69
|
+
self.assertFalse(sc._activar_sandbox(estricto=False))
|
|
70
|
+
fake_aviso.assert_called_once()
|
|
71
|
+
self.assertFalse(sc.sandbox_activo())
|
|
72
|
+
|
|
73
|
+
def test_activacion_con_docker(self):
|
|
74
|
+
with mock.patch.object(sc, "_docker_disponible", return_value=True):
|
|
75
|
+
self.assertTrue(sc._activar_sandbox(imagen="ubuntu:22.04",
|
|
76
|
+
comando_prep="apt update"))
|
|
77
|
+
self.assertTrue(sc.sandbox_activo())
|
|
78
|
+
self.assertEqual(sc._SANDBOX_IMAGEN, "ubuntu:22.04")
|
|
79
|
+
self.assertEqual(sc._SANDBOX_COMANDO_PREP, "apt update")
|
|
80
|
+
|
|
81
|
+
def test_imagen_por_variable_de_entorno(self):
|
|
82
|
+
env = {"SNAPCONTEXT_SANDBOX_IMAGE": "python:3.12"}
|
|
83
|
+
with mock.patch.dict(os.environ, env, clear=False):
|
|
84
|
+
self.assertEqual(sc._sandbox_imagen_resuelta(), "python:3.12")
|
|
85
|
+
# El flag explícito tiene prioridad sobre la variable.
|
|
86
|
+
self.assertEqual(sc._sandbox_imagen_resuelta("alpine"),
|
|
87
|
+
"alpine")
|
|
88
|
+
|
|
89
|
+
def test_flag_cli_parseado(self):
|
|
90
|
+
args = sc.crear_parser().parse_args(
|
|
91
|
+
["--sandbox", "--sandbox-imagen", "ubuntu:22.04",
|
|
92
|
+
"--sandbox-comando", "apt update", "consulta"])
|
|
93
|
+
self.assertTrue(args.sandbox)
|
|
94
|
+
self.assertEqual(args.sandbox_imagen, "ubuntu:22.04")
|
|
95
|
+
self.assertEqual(args.sandbox_comando, "apt update")
|
|
96
|
+
# Sin flags → False (compatibilidad).
|
|
97
|
+
args2 = sc.crear_parser().parse_args(["consulta"])
|
|
98
|
+
self.assertFalse(args2.sandbox)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class TestEnvolverSandbox(unittest.TestCase):
|
|
102
|
+
def setUp(self):
|
|
103
|
+
sc._desactivar_sandbox()
|
|
104
|
+
self.dir_tmp = tempfile.mkdtemp()
|
|
105
|
+
|
|
106
|
+
def tearDown(self):
|
|
107
|
+
sc._desactivar_sandbox()
|
|
108
|
+
|
|
109
|
+
def test_sin_sandbox_devuelve_comando_intacto(self):
|
|
110
|
+
self.assertEqual(sc._envolver_sandbox("pytest", "."), "pytest")
|
|
111
|
+
|
|
112
|
+
def test_envuelve_con_mount_y_workdir(self):
|
|
113
|
+
sc._SANDBOX_ACTIVO = True
|
|
114
|
+
sc._SANDBOX_IMAGEN = "python:3.11-slim"
|
|
115
|
+
sc._SANDBOX_COMANDO_PREP = None
|
|
116
|
+
envuelto = sc._envolver_sandbox("pytest -q", self.dir_tmp)
|
|
117
|
+
self.assertIn("docker run --rm", envuelto)
|
|
118
|
+
self.assertIn("-w /workspace", envuelto)
|
|
119
|
+
self.assertIn("python:3.11-slim", envuelto)
|
|
120
|
+
self.assertIn("pytest -q", envuelto)
|
|
121
|
+
|
|
122
|
+
def test_comando_prep_antepuesto(self):
|
|
123
|
+
sc._SANDBOX_ACTIVO = True
|
|
124
|
+
sc._SANDBOX_IMAGEN = "python:3.11-slim"
|
|
125
|
+
sc._SANDBOX_COMANDO_PREP = "pip install pytest"
|
|
126
|
+
envuelto = sc._envolver_sandbox("pytest", self.dir_tmp)
|
|
127
|
+
self.assertIn("pip install pytest && (pytest)", envuelto)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TestEjecucionEnSandbox(unittest.TestCase):
|
|
131
|
+
def setUp(self):
|
|
132
|
+
sc._desactivar_sandbox()
|
|
133
|
+
|
|
134
|
+
def tearDown(self):
|
|
135
|
+
sc._desactivar_sandbox()
|
|
136
|
+
|
|
137
|
+
def _activar(self):
|
|
138
|
+
sc._SANDBOX_ACTIVO = True
|
|
139
|
+
sc._SANDBOX_IMAGEN = "python:3.11-slim"
|
|
140
|
+
sc._SANDBOX_COMANDO_PREP = None
|
|
141
|
+
|
|
142
|
+
def test_ejecutar_comando_usa_docker_run(self):
|
|
143
|
+
self._activar()
|
|
144
|
+
with mock.patch.object(sc.subprocess, "run") as fake_run:
|
|
145
|
+
fake_run.return_value = mock.Mock(returncode=0, stdout="hola",
|
|
146
|
+
stderr="")
|
|
147
|
+
codigo, stdout, _ = sc._ejecutar_comando("echo hola", ".")
|
|
148
|
+
self.assertEqual(codigo, 0)
|
|
149
|
+
self.assertIn("hola", stdout)
|
|
150
|
+
comando_lanzado = fake_run.call_args[0][0]
|
|
151
|
+
self.assertIn("docker run --rm", comando_lanzado)
|
|
152
|
+
self.assertIn("echo hola", comando_lanzado)
|
|
153
|
+
|
|
154
|
+
def test_ejecutar_comando_error_se_propaga_igual(self):
|
|
155
|
+
self._activar()
|
|
156
|
+
with mock.patch.object(sc.subprocess, "run") as fake_run:
|
|
157
|
+
fake_run.return_value = mock.Mock(returncode=5, stdout="",
|
|
158
|
+
stderr="boom")
|
|
159
|
+
codigo, _, stderr = sc._ejecutar_comando("exit 5", ".")
|
|
160
|
+
self.assertEqual(codigo, 5)
|
|
161
|
+
self.assertIn("boom", stderr)
|
|
162
|
+
|
|
163
|
+
def test_ejecutar_pruebas_argv_en_sandbox(self):
|
|
164
|
+
self._activar()
|
|
165
|
+
with mock.patch.object(sc.subprocess, "run") as fake_run:
|
|
166
|
+
fake_run.return_value = mock.Mock(returncode=0, stdout="", stderr="")
|
|
167
|
+
codigo, _, _ = sc._ejecutar_pruebas_argv(["pytest", "-q"], ".")
|
|
168
|
+
self.assertEqual(codigo, 0)
|
|
169
|
+
self.assertIn("pytest -q", fake_run.call_args[0][0])
|
|
170
|
+
|
|
171
|
+
def test_mcp_execute_command_en_sandbox(self):
|
|
172
|
+
self._activar()
|
|
173
|
+
with mock.patch.object(sc.subprocess, "run") as fake_run:
|
|
174
|
+
fake_run.return_value = mock.Mock(returncode=0, stdout="ok",
|
|
175
|
+
stderr="")
|
|
176
|
+
res = sc._tool_execute_command("echo ok", ".")
|
|
177
|
+
self.assertTrue(res["ok"])
|
|
178
|
+
self.assertIn("docker run --rm", fake_run.call_args[0][0])
|
|
179
|
+
|
|
180
|
+
def test_herramientas_lectura_no_usan_docker(self):
|
|
181
|
+
# grep es de solo lectura: no pasa por docker (usa su propia función).
|
|
182
|
+
self._activar()
|
|
183
|
+
tmp = tempfile.mkdtemp()
|
|
184
|
+
(Path(tmp) / "a.py").write_text("def hola():\n pass\n",
|
|
185
|
+
encoding="utf-8")
|
|
186
|
+
with mock.patch.object(sc.subprocess, "run") as fake_run:
|
|
187
|
+
fake_run.return_value = mock.Mock(returncode=0, stdout="a.py:1:x",
|
|
188
|
+
stderr="")
|
|
189
|
+
res = sc._tool_grep("hola", tmp)
|
|
190
|
+
# Solo lectura → se ejecuta en el host, SIN envolver en docker.
|
|
191
|
+
lanzado = fake_run.call_args[0][0]
|
|
192
|
+
self.assertNotIn("docker", str(lanzado))
|
|
193
|
+
self.assertTrue(res["ok"])
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class TestIntegracionPlanYBucle(unittest.TestCase):
|
|
197
|
+
def setUp(self):
|
|
198
|
+
sc._desactivar_sandbox()
|
|
199
|
+
self.dir_tmp = tempfile.mkdtemp()
|
|
200
|
+
# Comando real no destructivo según plataforma.
|
|
201
|
+
self.comando = ("cmd /c echo hola"
|
|
202
|
+
if os.name == "nt" else "echo hola")
|
|
203
|
+
|
|
204
|
+
def tearDown(self):
|
|
205
|
+
sc._desactivar_sandbox()
|
|
206
|
+
|
|
207
|
+
def _activar_mock_docker(self):
|
|
208
|
+
sc._SANDBOX_ACTIVO = True
|
|
209
|
+
sc._SANDBOX_IMAGEN = "python:3.11-slim"
|
|
210
|
+
sc._SANDBOX_COMANDO_PREP = None
|
|
211
|
+
|
|
212
|
+
def test_paso_plan_ejecutar_en_sandbox(self):
|
|
213
|
+
self._activar_mock_docker()
|
|
214
|
+
with mock.patch.object(sc, "_confirmar_accion", return_value=True), \
|
|
215
|
+
mock.patch.object(sc.subprocess, "run") as fake_run:
|
|
216
|
+
fake_run.return_value = mock.Mock(returncode=0, stdout="hola",
|
|
217
|
+
stderr="")
|
|
218
|
+
ok, detalle = sc._ejecutar_paso_plan(
|
|
219
|
+
{"accion": "ejecutar", "descripcion": "paso",
|
|
220
|
+
"comando": self.comando},
|
|
221
|
+
_args_base(), self.dir_tmp)
|
|
222
|
+
self.assertTrue(ok)
|
|
223
|
+
self.assertIn("docker run --rm", fake_run.call_args[0][0])
|
|
224
|
+
|
|
225
|
+
def test_bucle_test_saltara_check_host_en_sandbox(self):
|
|
226
|
+
# Con sandbox activo NO se exige el binario en el PATH del host.
|
|
227
|
+
self._activar_mock_docker()
|
|
228
|
+
with mock.patch.object(sc.shutil, "which",
|
|
229
|
+
return_value=None) as fake_which, \
|
|
230
|
+
mock.patch.object(sc, "ejecutar_aider"), \
|
|
231
|
+
mock.patch.object(sc, "_ejecutar_pruebas_argv",
|
|
232
|
+
return_value=(0, "ok", "")):
|
|
233
|
+
self.assertTrue(sc.ejecutar_bucle_test(
|
|
234
|
+
"tarea", ["a.py"], ".", "", ["binario-inexistente"], 1))
|
|
235
|
+
fake_which.assert_not_called()
|
|
236
|
+
|
|
237
|
+
def test_agente_tester_en_sandbox(self):
|
|
238
|
+
from agentes import AgenteTester
|
|
239
|
+
self._activar_mock_docker()
|
|
240
|
+
with mock.patch.object(sc.subprocess, "run") as fake_run:
|
|
241
|
+
fake_run.return_value = mock.Mock(returncode=0, stdout="", stderr="")
|
|
242
|
+
res = AgenteTester().ejecutar_pruebas(["pytest"], ".")
|
|
243
|
+
self.assertEqual(res.returncode, 0)
|
|
244
|
+
self.assertIn("docker run --rm", fake_run.call_args[0][0])
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
if __name__ == "__main__":
|
|
248
|
+
unittest.main()
|
|
249
|
+
|
|
@@ -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, "4.
|
|
171
|
+
self.assertEqual(sc.VERSION, "4.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, "4.
|
|
129
|
+
self.assertEqual(sc.VERSION, "4.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, "4.
|
|
20
|
+
self.assertEqual(sc.VERSION, "4.3.0")
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class TestFrontendV160(unittest.TestCase):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{snapcontext-4.2.0 → snapcontext-4.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|