lanzakit-tester 0.1.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.
@@ -0,0 +1,155 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ lerna-debug.log*
8
+
9
+ # Diagnostic reports (https://nodejs.org/api/report.html)
10
+ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11
+
12
+ # Runtime data
13
+ pids
14
+ *.pid
15
+ *.seed
16
+ *.pid.lock
17
+
18
+ # Directory for instrumented libs generated by jscoverage/JSCover
19
+ lib-cov
20
+
21
+ # Coverage directory used by tools like istanbul
22
+ coverage
23
+ *.lcov
24
+
25
+ # nyc test coverage
26
+ .nyc_output
27
+
28
+ # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29
+ .grunt
30
+
31
+ # Bower dependency directory (https://bower.io/)
32
+ bower_components
33
+
34
+ # node-waf configuration
35
+ .lock-wscript
36
+
37
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
38
+ build/Release
39
+
40
+ # Dependency directories
41
+ node_modules/
42
+ jspm_packages/
43
+
44
+ # Snowpack dependency directory (https://snowpack.dev/)
45
+ web_modules/
46
+
47
+ # TypeScript cache
48
+ *.tsbuildinfo
49
+
50
+ # Optional npm cache directory
51
+ .npm
52
+
53
+ # Optional eslint cache
54
+ .eslintcache
55
+
56
+ # Optional stylelint cache
57
+ .stylelintcache
58
+
59
+ # Optional REPL history
60
+ .node_repl_history
61
+
62
+ # Output of 'npm pack'
63
+ *.tgz
64
+
65
+ # Yarn Integrity file
66
+ .yarn-integrity
67
+
68
+ # dotenv environment variable files
69
+ .env
70
+ .env.*
71
+ !.env.example
72
+
73
+ # parcel-bundler cache (https://parceljs.org/)
74
+ .cache
75
+ .parcel-cache
76
+
77
+ # Next.js build output
78
+ .next
79
+ out
80
+
81
+ # Nuxt.js build / generate output
82
+ .nuxt
83
+ dist
84
+ .output
85
+
86
+ # Gatsby files
87
+ .cache/
88
+ # Comment in the public line in if your project uses Gatsby and not Next.js
89
+ # https://nextjs.org/blog/next-9-1#public-directory-support
90
+ # public
91
+
92
+ # vuepress build output
93
+ .vuepress/dist
94
+
95
+ # vuepress v2.x temp directory
96
+ .temp
97
+
98
+ # Sveltekit cache directory
99
+ .svelte-kit/
100
+
101
+ # vitepress build output
102
+ **/.vitepress/dist
103
+
104
+ # vitepress cache directory
105
+ **/.vitepress/cache
106
+
107
+ # Docusaurus cache and generated files
108
+ .docusaurus
109
+
110
+ # Serverless directories
111
+ .serverless/
112
+
113
+ # FuseBox cache
114
+ .fusebox/
115
+
116
+ # DynamoDB Local files
117
+ .dynamodb/
118
+
119
+ # Firebase cache directory
120
+ .firebase/
121
+
122
+ # TernJS port file
123
+ .tern-port
124
+
125
+ # Stores VSCode versions used for testing VSCode extensions
126
+ .vscode-test
127
+
128
+ # pnpm
129
+ .pnpm-store
130
+
131
+ # yarn v3
132
+ .pnp.*
133
+ .yarn/*
134
+ !.yarn/patches
135
+ !.yarn/plugins
136
+ !.yarn/releases
137
+ !.yarn/sdks
138
+ !.yarn/versions
139
+
140
+ # Vite files
141
+ vite.config.js.timestamp-*
142
+ vite.config.ts.timestamp-*
143
+ .vite/
144
+ # Python
145
+ venv/
146
+ __pycache__/
147
+ *.pyc
148
+ .pytest_cache/
149
+
150
+ # Secrets
151
+ .env
152
+
153
+ # IDE
154
+ .vscode/
155
+ .idea/
@@ -0,0 +1,95 @@
1
+ # Lanzakit Tester
2
+
3
+ Proyecto: MCP server en Python que genera y valida tests pytest con IA.
4
+
5
+ ## Estado actual (noviembre 2026)
6
+
7
+ Paquete Python instalable (`pip install lanzakit-tester`), versión 0.1.0, aún sin publicar en PyPI.
8
+ Estructura `src/lanzakit_tester/` con build backend hatchling y CLI `lanzakit-tester`.
9
+
10
+ Producto funcional con 3 tools:
11
+ - `analyze_python_file`: analiza estructura del código con AST
12
+ - `generate_tests_for_file`: genera tests con Claude API
13
+ - `generate_and_validate_tests`: genera + valida sintaxis + ejecuta pytest
14
+
15
+ Loop end-to-end funciona. 8 tests generados y 8 passed en pruebas reales.
16
+
17
+ ## Stack técnico
18
+
19
+ - Python 3.14
20
+ - MCP SDK v2 (`from mcp.server.mcpserver import MCPServer`)
21
+ - Claude API (modelo: `claude-sonnet-5`)
22
+ - pytest para validación
23
+ - uv como gestor de paquetes
24
+ - venv en `.venv/`
25
+
26
+ ## Comandos clave
27
+
28
+ ```bash
29
+ # Activar entorno
30
+ source .venv/bin/activate
31
+
32
+ # Instalar en modo editable (con deps de desarrollo)
33
+ uv pip install -e ".[dev]"
34
+
35
+ # Verificar CLI
36
+ lanzakit-tester --version
37
+
38
+ # Probar generación de tests
39
+ python -c "from lanzakit_tester import generate_and_validate_tests; print(generate_and_validate_tests('/ruta/archivo.py')['summary'])"
40
+
41
+ # Correr tests manualmente
42
+ pytest examples -v
43
+ ```
44
+
45
+ ## Arquitectura
46
+
47
+ - `pyproject.toml`: fuente de verdad de metadata y dependencias (hatchling)
48
+ - `src/lanzakit_tester/server.py`: MCP server con las 3 tools, constante `MODEL`, `run()`
49
+ - `src/lanzakit_tester/cli.py`: entrypoint argparse (`--version`; sin args arranca MCP por stdio)
50
+ - `src/lanzakit_tester/__init__.py`: expone las 3 tools y `__version__`
51
+ - `examples/`: `hola_claude.py` (hola mundo con Claude API) y su test; no son parte del paquete
52
+ - `.env`: contiene ANTHROPIC_API_KEY (nunca commitear)
53
+
54
+ ## Decisiones tomadas
55
+
56
+ - Modelo: Claude Sonnet 5 (fijo por ahora), definido como constante `MODEL` en `src/lanzakit_tester/server.py`
57
+ - Versión única en `pyproject.toml`; se lee con `importlib.metadata` y se pasa a `MCPServer(version=...)`
58
+ - `max_tokens`: 16384 con thinking desactivado
59
+ - Prompt engineering con delimitadores XML
60
+ - `_clean_code_output()` remueve markdown fences
61
+ - `_run_pytest` usa `sys.executable -m pytest` para encontrar pytest desde cualquier contexto
62
+ - Thinking desactivado para ahorrar tokens (reactivable cuando se quiera)
63
+
64
+ ## Próximos pasos
65
+
66
+ - README público para GitHub
67
+ - Publicar en PyPI como `lanzakit-tester` (el empaquetado ya está listo)
68
+ - Landing en lanzakit.dev con waitlist
69
+ - Lanzamiento en X/LinkedIn + Product Hunt
70
+
71
+ ## Deuda técnica menor
72
+
73
+ No se resuelve antes del lanzamiento (perfeccionismo mata lanzamientos):
74
+
75
+ - `requirements-dev.txt` es redundante con `pip install -e ".[dev]"`
76
+ - `CONTRIBUTING.md` no menciona `pip install -e ".[dev]"`
77
+
78
+ ## Contexto personal
79
+
80
+ Ingeniera full stack en Jalasoft (La Paz, Bolivia).
81
+ Prefiere respuestas concisas y directas.
82
+ Comunica en español.
83
+
84
+ ## Reglas del proyecto
85
+
86
+ - Nunca commitear `.env`
87
+ - Tests en español (comentarios) pero nombres de funciones en inglés
88
+ - Mantener imports ordenados: stdlib → third party → local
89
+ - Modelo API siempre desde constante/config, nunca hardcoded en varias partes
90
+
91
+ ## Pendientes conocidos
92
+
93
+ - Renombrar repo de `starter-kit` a `lanzakit-tester` antes del lanzamiento público
94
+ - Reservar handle @lanzakit en X/Twitter
95
+ - Crear archivos de badges reales cuando se publique en PyPI
@@ -0,0 +1,51 @@
1
+ # Contribuir a Lanzakit Tester
2
+
3
+ ¡Gracias por tu interés! Toda contribución es bienvenida: bugs, ideas, docs y código.
4
+
5
+ ## Reportar un issue
6
+
7
+ 1. Busca primero si ya existe un issue similar.
8
+ 2. Abre uno nuevo e incluye:
9
+ - Qué esperabas que pasara y qué pasó realmente.
10
+ - Pasos para reproducirlo (idealmente con un archivo `.py` mínimo).
11
+ - Versión de Python y sistema operativo.
12
+ - Mensajes de error completos.
13
+
14
+ Nunca pegues tu `ANTHROPIC_API_KEY` ni el contenido de tu `.env`.
15
+
16
+ ## Proponer una feature
17
+
18
+ Abre un issue describiendo el problema que quieres resolver y tu propuesta.
19
+ Conviene acordar el enfoque antes de escribir código, así evitamos trabajo perdido.
20
+
21
+ ## Hacer un Pull Request
22
+
23
+ 1. Haz fork del repositorio.
24
+ 2. Crea una rama desde `main`: `git checkout -b feat/mi-cambio`
25
+ 3. Haz tus cambios y commits pequeños y claros.
26
+ 4. Verifica que los tests pasen: `pytest -v`
27
+ 5. Haz push a tu fork y abre el PR explicando qué cambia y por qué.
28
+
29
+ Mensajes de commit sugeridos: `feat:`, `fix:`, `docs:`, `test:`, `refactor:`.
30
+
31
+ ## Estilo de código
32
+
33
+ - Sigue [PEP 8](https://peps.python.org/pep-0008/).
34
+ - Formatea con [black](https://black.readthedocs.io/) y ordena imports con [isort](https://pycqa.github.io/isort/):
35
+
36
+ ```bash
37
+ black .
38
+ isort .
39
+ ```
40
+
41
+ - Orden de imports: stdlib → third party → local.
42
+
43
+ ## Convenciones del proyecto
44
+
45
+ - Comentarios y docstrings en español; nombres de funciones y variables en inglés.
46
+ - El modelo de la API se define en una sola constante/config, nunca hardcoded en varios lugares.
47
+ - Nunca commitees `.env` ni credenciales.
48
+
49
+ ## Licencia
50
+
51
+ Al contribuir aceptas que tu aporte se distribuya bajo la licencia [MIT](LICENSE) del proyecto.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Raquel Martinez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,179 @@
1
+ Metadata-Version: 2.5
2
+ Name: lanzakit-tester
3
+ Version: 0.1.0
4
+ Summary: MCP server that generates and validates pytest tests using Claude.
5
+ Project-URL: Homepage, https://github.com/lanzakit/starter-kit
6
+ Project-URL: Repository, https://github.com/lanzakit/starter-kit
7
+ Project-URL: Issues, https://github.com/lanzakit/starter-kit/issues
8
+ Author: Raquel Martinez
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai,anthropic,claude,mcp,pytest,testing
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Software Development :: Testing
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: anthropic>=2.0.0
23
+ Requires-Dist: mcp[cli]>=2.0.0
24
+ Requires-Dist: pytest>=8.0.0
25
+ Requires-Dist: python-dotenv>=1.0.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: black; extra == 'dev'
28
+ Requires-Dist: isort; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # Lanzakit Tester
32
+
33
+ ![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue?logo=python&logoColor=white)
34
+ ![License MIT](https://img.shields.io/badge/license-MIT-green)
35
+ <!-- TODO: agregar badge PyPI después del primer release -->
36
+
37
+ **Tests de pytest generados por IA, y verificados antes de que los veas.**
38
+
39
+ Lanzakit Tester es un MCP server que lee tu código Python, genera tests con Claude y los ejecuta para confirmar que compilan y pasan. Dejas de revisar tests que ni siquiera corren.
40
+
41
+ ## Features
42
+
43
+ - 🧪 **Generación de tests con Claude**: analiza tu código y produce tests pytest listos para usar.
44
+ - ✅ **Validación automática**: verifica la sintaxis y ejecuta pytest antes de entregarte el resultado.
45
+ - 🔌 **MCP nativo**: funciona con Claude Desktop y cualquier cliente compatible con MCP.
46
+ - 🌳 **Análisis con AST**: entiende funciones, clases y firmas antes de armar el prompt.
47
+ - 🇪🇸 **Español-first**: comentarios de los tests y documentación en español.
48
+ - 🔓 **Open source**: licencia MIT, sin lock-in.
49
+
50
+ ## Requisitos
51
+
52
+ - Python 3.11+
53
+ - Una API key de Anthropic (`ANTHROPIC_API_KEY`)
54
+ - Acceso al modelo `claude-sonnet-5`
55
+
56
+ ## Instalación
57
+
58
+ ```bash
59
+ pip install lanzakit-tester
60
+ echo "ANTHROPIC_API_KEY=tu_api_key" > .env
61
+ ```
62
+
63
+ > El archivo `.env` contiene tu API key. Nunca lo subas a git.
64
+
65
+ ### Opción A: Vía Claude Desktop (MCP config)
66
+
67
+ Agrega el server a tu `claude_desktop_config.json`:
68
+
69
+ ```json
70
+ {
71
+ "mcpServers": {
72
+ "lanzakit-tester": {
73
+ "command": "lanzakit-tester"
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ Reinicia Claude Desktop y las 3 tools aparecerán disponibles.
80
+
81
+ ### Opción B: Vía Python
82
+
83
+ Sin cliente MCP, puedes invocar las tools directamente desde Python:
84
+
85
+ ```bash
86
+ python -c "from lanzakit_tester import generate_and_validate_tests; print(generate_and_validate_tests('/ruta/a/tu_archivo.py')['summary'])"
87
+ ```
88
+
89
+ ### Para contribuir
90
+
91
+ ```bash
92
+ git clone https://github.com/lanzakit/starter-kit.git
93
+ cd starter-kit
94
+ uv venv && source .venv/bin/activate
95
+ uv pip install -e ".[dev]"
96
+ ```
97
+
98
+ ## Uso
99
+
100
+ Tienes un archivo `hola_claude.py` (ver `examples/hola_claude.py`, versión simplificada):
101
+
102
+ ```python
103
+ import os
104
+
105
+ from anthropic import Anthropic
106
+ from dotenv import load_dotenv
107
+
108
+ load_dotenv()
109
+ client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
110
+
111
+
112
+ def saludar():
113
+ message = client.messages.create(
114
+ model="claude-sonnet-5",
115
+ max_tokens=512,
116
+ messages=[{"role": "user", "content": "Hola Claude"}]
117
+ )
118
+ print(message.content[0].text)
119
+ ```
120
+
121
+ Desde Claude Desktop, pídele:
122
+
123
+ > Genera y valida tests para `/ruta/a/tu_archivo.py`
124
+
125
+ Claude llama a la tool `generate_and_validate_tests` y obtienes un resumen como este:
126
+
127
+ ```text
128
+ ✅ 8 tests generados y pasaron. Guardado en: /ruta/a/test_tu_archivo.py
129
+ ```
130
+
131
+ El archivo `test_tu_archivo.py` queda junto a tu código, listo para correr con `pytest`.
132
+
133
+ ### Tools disponibles
134
+
135
+ | Tool | Qué hace |
136
+ |------|----------|
137
+ | `analyze_python_file` | Analiza la estructura del código con AST. |
138
+ | `generate_tests_for_file` | Genera tests pytest con Claude. |
139
+ | `generate_and_validate_tests` | Genera, valida sintaxis, guarda y ejecuta pytest. |
140
+
141
+ ## Cómo funciona
142
+
143
+ ```text
144
+ código .py
145
+ │
146
+ ▼
147
+ análisis AST ──► prompt a Claude ──► tests generados
148
+ │
149
+ ▼
150
+ resultado ◄── ejecución pytest ◄── validación sintáctica
151
+ ```
152
+
153
+ 1. Lee tu archivo y extrae su estructura con AST.
154
+ 2. Arma un prompt con el código y su contexto, y se lo envía a Claude (`claude-sonnet-5`).
155
+ 3. Limpia la respuesta (por ejemplo, quita markdown fences).
156
+ 4. Valida que los tests tengan sintaxis Python correcta.
157
+ 5. Guarda el archivo y ejecuta pytest.
158
+ 6. Devuelve un resumen: cuántos pasaron, fallaron o dieron error.
159
+
160
+ ## Roadmap
161
+
162
+ - [x] 3 tools funcionales
163
+ - [x] Validación end-to-end
164
+ - [x] Empaquetado como paquete pip
165
+ - [ ] Publicación en PyPI
166
+ - [ ] Landing en lanzakit.dev
167
+ - [ ] Soporte para más frameworks de testing
168
+
169
+ ## Contribuir
170
+
171
+ ¿Encontraste un bug o tienes una idea? Abre un [issue](https://github.com/lanzakit/starter-kit/issues) o envía un PR. Lee [CONTRIBUTING.md](CONTRIBUTING.md) para conocer el flujo de trabajo.
172
+
173
+ ## Licencia
174
+
175
+ Distribuido bajo licencia MIT. Consulta el archivo [LICENSE](LICENSE).
176
+
177
+ ---
178
+
179
+ [@lanzakit](https://x.com/lanzakit) · Construido en Bolivia 🇧🇴 para devs hispanohablantes
@@ -0,0 +1,149 @@
1
+ # Lanzakit Tester
2
+
3
+ ![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue?logo=python&logoColor=white)
4
+ ![License MIT](https://img.shields.io/badge/license-MIT-green)
5
+ <!-- TODO: agregar badge PyPI después del primer release -->
6
+
7
+ **Tests de pytest generados por IA, y verificados antes de que los veas.**
8
+
9
+ Lanzakit Tester es un MCP server que lee tu código Python, genera tests con Claude y los ejecuta para confirmar que compilan y pasan. Dejas de revisar tests que ni siquiera corren.
10
+
11
+ ## Features
12
+
13
+ - 🧪 **Generación de tests con Claude**: analiza tu código y produce tests pytest listos para usar.
14
+ - ✅ **Validación automática**: verifica la sintaxis y ejecuta pytest antes de entregarte el resultado.
15
+ - 🔌 **MCP nativo**: funciona con Claude Desktop y cualquier cliente compatible con MCP.
16
+ - 🌳 **Análisis con AST**: entiende funciones, clases y firmas antes de armar el prompt.
17
+ - 🇪🇸 **Español-first**: comentarios de los tests y documentación en español.
18
+ - 🔓 **Open source**: licencia MIT, sin lock-in.
19
+
20
+ ## Requisitos
21
+
22
+ - Python 3.11+
23
+ - Una API key de Anthropic (`ANTHROPIC_API_KEY`)
24
+ - Acceso al modelo `claude-sonnet-5`
25
+
26
+ ## Instalación
27
+
28
+ ```bash
29
+ pip install lanzakit-tester
30
+ echo "ANTHROPIC_API_KEY=tu_api_key" > .env
31
+ ```
32
+
33
+ > El archivo `.env` contiene tu API key. Nunca lo subas a git.
34
+
35
+ ### Opción A: Vía Claude Desktop (MCP config)
36
+
37
+ Agrega el server a tu `claude_desktop_config.json`:
38
+
39
+ ```json
40
+ {
41
+ "mcpServers": {
42
+ "lanzakit-tester": {
43
+ "command": "lanzakit-tester"
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ Reinicia Claude Desktop y las 3 tools aparecerán disponibles.
50
+
51
+ ### Opción B: Vía Python
52
+
53
+ Sin cliente MCP, puedes invocar las tools directamente desde Python:
54
+
55
+ ```bash
56
+ python -c "from lanzakit_tester import generate_and_validate_tests; print(generate_and_validate_tests('/ruta/a/tu_archivo.py')['summary'])"
57
+ ```
58
+
59
+ ### Para contribuir
60
+
61
+ ```bash
62
+ git clone https://github.com/lanzakit/starter-kit.git
63
+ cd starter-kit
64
+ uv venv && source .venv/bin/activate
65
+ uv pip install -e ".[dev]"
66
+ ```
67
+
68
+ ## Uso
69
+
70
+ Tienes un archivo `hola_claude.py` (ver `examples/hola_claude.py`, versión simplificada):
71
+
72
+ ```python
73
+ import os
74
+
75
+ from anthropic import Anthropic
76
+ from dotenv import load_dotenv
77
+
78
+ load_dotenv()
79
+ client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
80
+
81
+
82
+ def saludar():
83
+ message = client.messages.create(
84
+ model="claude-sonnet-5",
85
+ max_tokens=512,
86
+ messages=[{"role": "user", "content": "Hola Claude"}]
87
+ )
88
+ print(message.content[0].text)
89
+ ```
90
+
91
+ Desde Claude Desktop, pídele:
92
+
93
+ > Genera y valida tests para `/ruta/a/tu_archivo.py`
94
+
95
+ Claude llama a la tool `generate_and_validate_tests` y obtienes un resumen como este:
96
+
97
+ ```text
98
+ ✅ 8 tests generados y pasaron. Guardado en: /ruta/a/test_tu_archivo.py
99
+ ```
100
+
101
+ El archivo `test_tu_archivo.py` queda junto a tu código, listo para correr con `pytest`.
102
+
103
+ ### Tools disponibles
104
+
105
+ | Tool | Qué hace |
106
+ |------|----------|
107
+ | `analyze_python_file` | Analiza la estructura del código con AST. |
108
+ | `generate_tests_for_file` | Genera tests pytest con Claude. |
109
+ | `generate_and_validate_tests` | Genera, valida sintaxis, guarda y ejecuta pytest. |
110
+
111
+ ## Cómo funciona
112
+
113
+ ```text
114
+ código .py
115
+ │
116
+ ▼
117
+ análisis AST ──► prompt a Claude ──► tests generados
118
+ │
119
+ ▼
120
+ resultado ◄── ejecución pytest ◄── validación sintáctica
121
+ ```
122
+
123
+ 1. Lee tu archivo y extrae su estructura con AST.
124
+ 2. Arma un prompt con el código y su contexto, y se lo envía a Claude (`claude-sonnet-5`).
125
+ 3. Limpia la respuesta (por ejemplo, quita markdown fences).
126
+ 4. Valida que los tests tengan sintaxis Python correcta.
127
+ 5. Guarda el archivo y ejecuta pytest.
128
+ 6. Devuelve un resumen: cuántos pasaron, fallaron o dieron error.
129
+
130
+ ## Roadmap
131
+
132
+ - [x] 3 tools funcionales
133
+ - [x] Validación end-to-end
134
+ - [x] Empaquetado como paquete pip
135
+ - [ ] Publicación en PyPI
136
+ - [ ] Landing en lanzakit.dev
137
+ - [ ] Soporte para más frameworks de testing
138
+
139
+ ## Contribuir
140
+
141
+ ¿Encontraste un bug o tienes una idea? Abre un [issue](https://github.com/lanzakit/starter-kit/issues) o envía un PR. Lee [CONTRIBUTING.md](CONTRIBUTING.md) para conocer el flujo de trabajo.
142
+
143
+ ## Licencia
144
+
145
+ Distribuido bajo licencia MIT. Consulta el archivo [LICENSE](LICENSE).
146
+
147
+ ---
148
+
149
+ [@lanzakit](https://x.com/lanzakit) · Construido en Bolivia 🇧🇴 para devs hispanohablantes
@@ -0,0 +1,46 @@
1
+ """
2
+ Primer script: saludar a Claude y verificar que la API funciona.
3
+ Este script es el 'Hola Mundo' del proyecto Lanzakit Tester.
4
+ """
5
+ import os
6
+ from anthropic import Anthropic
7
+ from dotenv import load_dotenv
8
+
9
+ # Cargar variables de entorno desde .env
10
+ load_dotenv()
11
+
12
+ # Inicializar cliente de Claude
13
+ client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
14
+
15
+
16
+ def saludar():
17
+ """Envía un mensaje de prueba a Claude y muestra la respuesta."""
18
+ message = client.messages.create(
19
+ model="claude-sonnet-5",
20
+ max_tokens=512,
21
+ messages=[
22
+ {
23
+ "role": "user",
24
+ "content": (
25
+ "Hola Claude, soy Raquel y estoy construyendo "
26
+ "Lanzakit Tester, un agente de IA para generar tests "
27
+ "de Python. ¿Estás listo para ayudarme en este proyecto? "
28
+ "Responde en 2 líneas."
29
+ ),
30
+ }
31
+ ],
32
+ )
33
+
34
+ print("Respuesta de Claude:")
35
+ print("-" * 50)
36
+ print(message.content[0].text)
37
+ print("-" * 50)
38
+ print(
39
+ f"\nTokens usados: "
40
+ f"{message.usage.input_tokens} input + "
41
+ f"{message.usage.output_tokens} output"
42
+ )
43
+
44
+
45
+ if __name__ == "__main__":
46
+ saludar()
@@ -0,0 +1,157 @@
1
+ """
2
+ Tests para hola_claude.py: verifica la lógica de saludar()
3
+ mockeando la frontera externa (API de Anthropic).
4
+ """
5
+ import os
6
+ from unittest.mock import MagicMock, patch
7
+
8
+ import pytest
9
+
10
+
11
+ # El módulo bajo prueba instancia Anthropic() al importarse (nivel módulo).
12
+ # Por eso mockeamos la clase Anthropic ANTES de importar hola_claude,
13
+ # para evitar llamadas reales o errores por falta de API key.
14
+ @pytest.fixture
15
+ def mock_anthropic_client():
16
+ """Reemplaza la clase Anthropic por un mock antes de importar el módulo."""
17
+ with patch("anthropic.Anthropic") as mock_class:
18
+ mock_instance = MagicMock()
19
+ mock_class.return_value = mock_instance
20
+ yield mock_class, mock_instance
21
+
22
+
23
+ @pytest.fixture
24
+ def hola_claude_module(mock_anthropic_client, monkeypatch):
25
+ """Importa (o recarga) hola_claude con Anthropic ya mockeado."""
26
+ # Aseguramos que exista una API key dummy para que os.getenv no sea None.
27
+ monkeypatch.setenv("ANTHROPIC_API_KEY", "fake-key-for-tests")
28
+
29
+ import importlib
30
+ import sys
31
+
32
+ # Si el módulo ya fue importado por otro test, lo removemos para
33
+ # forzar una reimportación limpia con el mock activo.
34
+ if "hola_claude" in sys.modules:
35
+ del sys.modules["hola_claude"]
36
+
37
+ module = importlib.import_module("hola_claude")
38
+ importlib.reload(module)
39
+ return module
40
+
41
+
42
+ def _build_fake_message(text="Respuesta de prueba", input_tokens=10, output_tokens=5):
43
+ """Construye un objeto mensaje falso con la forma que espera el código."""
44
+ fake_message = MagicMock()
45
+ fake_content_block = MagicMock()
46
+ fake_content_block.text = text
47
+ fake_message.content = [fake_content_block]
48
+ fake_message.usage.input_tokens = input_tokens
49
+ fake_message.usage.output_tokens = output_tokens
50
+ return fake_message
51
+
52
+
53
+ def test_saludar_llama_a_messages_create_con_modelo_correcto(
54
+ hola_claude_module, mock_anthropic_client
55
+ ):
56
+ """Protege contra regresiones que cambien el modelo o rompan la llamada a la API."""
57
+ _, mock_instance = mock_anthropic_client
58
+ mock_instance.messages.create.return_value = _build_fake_message()
59
+
60
+ hola_claude_module.client = mock_instance # aseguramos el mock en el módulo
61
+ hola_claude_module.saludar()
62
+
63
+ mock_instance.messages.create.assert_called_once()
64
+ _, kwargs = mock_instance.messages.create.call_args
65
+ assert kwargs["model"] == "claude-sonnet-5"
66
+ assert kwargs["max_tokens"] == 512
67
+
68
+
69
+ def test_saludar_envia_mensaje_con_rol_user_y_contenido_no_vacio(
70
+ hola_claude_module, mock_anthropic_client
71
+ ):
72
+ """Protege contra cambios accidentales en el payload enviado a Claude."""
73
+ _, mock_instance = mock_anthropic_client
74
+ mock_instance.messages.create.return_value = _build_fake_message()
75
+
76
+ hola_claude_module.client = mock_instance
77
+ hola_claude_module.saludar()
78
+
79
+ _, kwargs = mock_instance.messages.create.call_args
80
+ messages = kwargs["messages"]
81
+ assert len(messages) == 1
82
+ assert messages[0]["role"] == "user"
83
+ assert isinstance(messages[0]["content"], str)
84
+ assert len(messages[0]["content"]) > 0
85
+
86
+
87
+ def test_saludar_imprime_respuesta_y_tokens(
88
+ hola_claude_module, mock_anthropic_client, capsys
89
+ ):
90
+ """Protege contra regresiones en el formato de salida: texto y conteo de tokens."""
91
+ _, mock_instance = mock_anthropic_client
92
+ mock_instance.messages.create.return_value = _build_fake_message(
93
+ text="Sí, estoy listo.", input_tokens=42, output_tokens=7
94
+ )
95
+
96
+ hola_claude_module.client = mock_instance
97
+ hola_claude_module.saludar()
98
+
99
+ captured = capsys.readouterr()
100
+ assert "Sí, estoy listo." in captured.out
101
+ assert "42 input" in captured.out
102
+ assert "7 output" in captured.out
103
+
104
+
105
+ @pytest.mark.parametrize(
106
+ "input_tokens,output_tokens",
107
+ [
108
+ (0, 0), # caso límite: respuesta sin consumo de tokens reportado
109
+ (1, 1), # caso mínimo no nulo
110
+ (100000, 50000), # caso de valores grandes, detecta overflow o truncamiento
111
+ ],
112
+ )
113
+ def test_saludar_formatea_distintos_valores_de_tokens(
114
+ hola_claude_module, mock_anthropic_client, capsys, input_tokens, output_tokens
115
+ ):
116
+ """Protege contra errores de formato al construir el string de tokens usados."""
117
+ _, mock_instance = mock_anthropic_client
118
+ mock_instance.messages.create.return_value = _build_fake_message(
119
+ input_tokens=input_tokens, output_tokens=output_tokens
120
+ )
121
+
122
+ hola_claude_module.client = mock_instance
123
+ hola_claude_module.saludar()
124
+
125
+ captured = capsys.readouterr()
126
+ assert f"{input_tokens} input" in captured.out
127
+ assert f"{output_tokens} output" in captured.out
128
+
129
+
130
+ def test_saludar_propaga_excepcion_si_api_falla(
131
+ hola_claude_module, mock_anthropic_client
132
+ ):
133
+ """Verifica que un error de la API no se silencia, sino que se propaga al llamador."""
134
+ _, mock_instance = mock_anthropic_client
135
+ mock_instance.messages.create.side_effect = RuntimeError("fallo de red simulado")
136
+
137
+ hola_claude_module.client = mock_instance
138
+
139
+ with pytest.raises(RuntimeError, match="fallo de red simulado"):
140
+ hola_claude_module.saludar()
141
+
142
+
143
+ def test_client_se_inicializa_con_api_key_desde_entorno(
144
+ mock_anthropic_client, monkeypatch
145
+ ):
146
+ """Protege contra regresiones donde la API key deje de leerse de la variable de entorno."""
147
+ mock_class, _ = mock_anthropic_client
148
+ monkeypatch.setenv("ANTHROPIC_API_KEY", "clave-especifica-de-test")
149
+
150
+ import importlib
151
+ import sys
152
+
153
+ if "hola_claude" in sys.modules:
154
+ del sys.modules["hola_claude"]
155
+ importlib.import_module("hola_claude")
156
+
157
+ mock_class.assert_called_once_with(api_key="clave-especifica-de-test")
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "lanzakit-tester"
7
+ version = "0.1.0"
8
+ description = "MCP server that generates and validates pytest tests using Claude."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Raquel Martinez" }]
14
+ keywords = ["mcp", "pytest", "testing", "ai", "claude", "anthropic"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Topic :: Software Development :: Testing",
25
+ ]
26
+ dependencies = [
27
+ "anthropic>=2.0.0",
28
+ "python-dotenv>=1.0.0",
29
+ "mcp[cli]>=2.0.0",
30
+ "pytest>=8.0.0",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ dev = ["black", "isort"]
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/lanzakit/starter-kit"
38
+ Repository = "https://github.com/lanzakit/starter-kit"
39
+ Issues = "https://github.com/lanzakit/starter-kit/issues"
40
+
41
+ [project.scripts]
42
+ lanzakit-tester = "lanzakit_tester.cli:main"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/lanzakit_tester"]
@@ -0,0 +1,3 @@
1
+ black>=24.0.0
2
+ isort>=5.13.0
3
+ pytest>=8.0.0
@@ -0,0 +1,14 @@
1
+ """Lanzakit Tester: genera y valida tests pytest con IA (MCP server)."""
2
+ from lanzakit_tester.server import (
3
+ __version__,
4
+ analyze_python_file,
5
+ generate_and_validate_tests,
6
+ generate_tests_for_file,
7
+ )
8
+
9
+ __all__ = [
10
+ "__version__",
11
+ "analyze_python_file",
12
+ "generate_and_validate_tests",
13
+ "generate_tests_for_file",
14
+ ]
@@ -0,0 +1,25 @@
1
+ """Entrypoint de línea de comandos de Lanzakit Tester."""
2
+ import argparse
3
+
4
+ from lanzakit_tester import __version__
5
+
6
+
7
+ def main() -> None:
8
+ parser = argparse.ArgumentParser(
9
+ prog="lanzakit-tester",
10
+ description="Lanzakit Tester: MCP server que genera y valida tests pytest con IA. "
11
+ "Sin argumentos, arranca el servidor MCP por stdio.",
12
+ )
13
+ parser.add_argument(
14
+ "--version", action="version", version=f"%(prog)s {__version__}"
15
+ )
16
+ parser.parse_args()
17
+
18
+ # Import diferido: evita inicializar el servidor si solo se pide --help/--version
19
+ from lanzakit_tester.server import run
20
+
21
+ run()
22
+
23
+
24
+ if __name__ == "__main__":
25
+ main()
@@ -0,0 +1,422 @@
1
+ """
2
+ Lanzakit Tester - MCP Server
3
+
4
+ Agente de IA para generar y analizar tests de Python.
5
+ """
6
+ import ast
7
+ import os
8
+ import subprocess
9
+ import sys
10
+ from importlib.metadata import PackageNotFoundError, version
11
+ from pathlib import Path
12
+
13
+ from anthropic import Anthropic
14
+ from dotenv import load_dotenv
15
+ from mcp.server.mcpserver import MCPServer
16
+
17
+ load_dotenv()
18
+
19
+ try:
20
+ __version__ = version("lanzakit-tester")
21
+ except PackageNotFoundError: # ejecutando desde el código fuente sin instalar
22
+ __version__ = "0.0.0+unknown"
23
+
24
+ # Modelo de Claude usado para generar los tests (único punto de configuración)
25
+ MODEL = "claude-sonnet-5"
26
+
27
+ anthropic_client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
28
+
29
+
30
+ def _clean_code_output(text: str) -> str:
31
+ """Remueve markdown fences si Claude los agrega a pesar del prompt."""
32
+ text = text.strip()
33
+ if text.startswith("```python"):
34
+ text = text[9:].lstrip()
35
+ elif text.startswith("```"):
36
+ text = text[3:].lstrip()
37
+ if text.endswith("```"):
38
+ text = text[:-3].rstrip()
39
+ return text
40
+
41
+
42
+ # Crear el servidor MCP
43
+ mcp = MCPServer("lanzakit-tester", version=__version__)
44
+
45
+
46
+ @mcp.tool()
47
+ def analyze_python_file(file_path: str) -> dict:
48
+ """
49
+ Analiza un archivo Python y devuelve información estructural.
50
+
51
+ Lee el archivo, lo parsea con AST (Abstract Syntax Tree) y extrae:
52
+ - Nombre del archivo
53
+ - Número de líneas totales
54
+ - Lista de clases definidas
55
+ - Lista de funciones definidas (incluye métodos de clases)
56
+ - Imports utilizados
57
+
58
+ Args:
59
+ file_path: Ruta absoluta al archivo .py a analizar
60
+
61
+ Returns:
62
+ Diccionario con el análisis estructural del archivo
63
+ """
64
+ path = Path(file_path)
65
+
66
+ # Validaciones básicas
67
+ if not path.exists():
68
+ return {"error": f"El archivo {file_path} no existe"}
69
+
70
+ if path.suffix != ".py":
71
+ return {"error": f"El archivo debe ser .py, no {path.suffix}"}
72
+
73
+ # Leer el contenido
74
+ try:
75
+ source_code = path.read_text(encoding="utf-8")
76
+ except Exception as e:
77
+ return {"error": f"No se pudo leer el archivo: {e}"}
78
+
79
+ # Parsear con AST
80
+ try:
81
+ tree = ast.parse(source_code)
82
+ except SyntaxError as e:
83
+ return {"error": f"El archivo tiene errores de sintaxis: {e}"}
84
+
85
+ # Extraer información
86
+ classes = []
87
+ functions = []
88
+ imports = []
89
+
90
+ for node in ast.walk(tree):
91
+ if isinstance(node, ast.ClassDef):
92
+ classes.append({
93
+ "name": node.name,
94
+ "line": node.lineno,
95
+ "methods": [
96
+ n.name for n in node.body
97
+ if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
98
+ ]
99
+ })
100
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
101
+ functions.append({
102
+ "name": node.name,
103
+ "line": node.lineno,
104
+ "args": [arg.arg for arg in node.args.args]
105
+ })
106
+ elif isinstance(node, ast.Import):
107
+ for alias in node.names:
108
+ imports.append(alias.name)
109
+ elif isinstance(node, ast.ImportFrom):
110
+ module = node.module or ""
111
+ for alias in node.names:
112
+ imports.append(f"{module}.{alias.name}")
113
+
114
+ return {
115
+ "file": path.name,
116
+ "total_lines": len(source_code.splitlines()),
117
+ "classes": classes,
118
+ "functions": functions,
119
+ "imports": imports,
120
+ }
121
+
122
+
123
+ GENERATE_TESTS_PROMPT = """Eres Lanzakit Tester, un ingeniero senior de Python especializado en pytest.
124
+
125
+ Tu tarea: generar un archivo de tests pytest de alta calidad para el código proporcionado. Los tests deben detectar regresiones reales, ser deterministas y enseñar buenas prácticas con comentarios breves en español neutro LATAM.
126
+
127
+ <codigo_a_testear ruta="{file_path}">
128
+ {source_code}
129
+ </codigo_a_testear>
130
+
131
+ <reglas_fundamentales>
132
+
133
+ 1. FIDELIDAD AL CÓDIGO
134
+ - Testea el comportamiento OBSERVABLE del código, no lo que crees que debería hacer
135
+ - No inventes contratos ni validaciones que el código no implementa
136
+ - Usa docstrings, type hints y validaciones como fuente de verdad
137
+
138
+ 2. EDGE CASES JUSTIFICADOS
139
+ - Cada test debe responder: "¿qué regresión específica detectaría?"
140
+ - Deriva casos de las ramas, operaciones y límites reales del código
141
+ - No incluyas casos genéricos sin justificación
142
+
143
+ 3. IMPORTS Y DEPENDENCIAS
144
+ - Solo usa: pytest, unittest.mock, biblioteca estándar
145
+ - NO inventes: fixtures custom, pytest-plugins externos, módulos que no existen
146
+ - Para importar el código bajo prueba usa el nombre del archivo sin extensión
147
+ - Si no puedes determinar la ruta de import con confianza, NO inventes
148
+
149
+ 4. PYTEST MODERNO
150
+ - Usa pytest.mark.parametrize cuando varios casos compartan verificación
151
+ - Usa pytest.raises para excepciones
152
+ - Usa fixtures solo cuando reduzcan duplicación real
153
+ - Nombres: test_<funcion>_<escenario>_<resultado>
154
+
155
+ 5. COMENTARIOS EDUCATIVOS
156
+ - Comentarios BREVES en español explicando qué regresión protege cada test
157
+ - Un comentario por test o por grupo parametrizado, no por línea
158
+ - Nombres de funciones y variables en inglés (convención Python estándar)
159
+
160
+ 6. MOCKING RESPONSABLE
161
+ - No mockees la función bajo prueba
162
+ - Mockea solo fronteras externas (red, archivos, servicios)
163
+ - Prefiere lógica real sin mocks cuando sea posible
164
+
165
+ </reglas_fundamentales>
166
+
167
+ <prohibiciones>
168
+ - NO inventar imports, fixtures, módulos o dependencias
169
+ - NO usar assert True ni aserciones triviales
170
+ - NO capturar Exception genérica para hacer pasar un test
171
+ - NO incluir Markdown, ```python, preámbulos ni explicaciones externas al código
172
+ - NO modificar el código fuente del usuario
173
+ </prohibiciones>
174
+
175
+ <formato_salida>
176
+ Devuelve EXCLUSIVAMENTE código Python válido, listo para guardar como test_<nombre>.py.
177
+ Primera línea: un import o un comentario Python.
178
+ No incluyas nada antes ni después del código.
179
+ </formato_salida>"""
180
+
181
+
182
+ @mcp.tool()
183
+ def generate_tests_for_file(file_path: str) -> dict:
184
+ """
185
+ Genera tests pytest de alta calidad para un archivo Python usando Claude.
186
+
187
+ Lee el archivo, lo envía a Claude con un prompt optimizado para testing,
188
+ y devuelve el código de los tests generado.
189
+
190
+ Args:
191
+ file_path: Ruta absoluta al archivo .py a testear
192
+
193
+ Returns:
194
+ Diccionario con:
195
+ - file: nombre del archivo original
196
+ - test_code: código Python de los tests generados
197
+ - tokens_used: cuántos tokens consumió la llamada
198
+ """
199
+ path = Path(file_path)
200
+
201
+ if not path.exists():
202
+ return {"error": f"El archivo {file_path} no existe"}
203
+
204
+ if path.suffix != ".py":
205
+ return {"error": f"El archivo debe ser .py, no {path.suffix}"}
206
+
207
+ try:
208
+ source_code = path.read_text(encoding="utf-8")
209
+ except Exception as e:
210
+ return {"error": f"No se pudo leer el archivo: {e}"}
211
+
212
+ if not source_code.strip():
213
+ return {"error": "El archivo está vacío"}
214
+
215
+ # Llamar a Claude
216
+ try:
217
+ message = anthropic_client.messages.create(
218
+ model=MODEL,
219
+ max_tokens=16384,
220
+ thinking={"type": "disabled"},
221
+ messages=[
222
+ {
223
+ "role": "user",
224
+ "content": GENERATE_TESTS_PROMPT.format(
225
+ source_code=source_code,
226
+ file_path=str(path)
227
+ )
228
+ }
229
+ ]
230
+ )
231
+ except Exception as e:
232
+ return {"error": f"Error llamando a Claude API: {e}"}
233
+
234
+ # Buscar el primer bloque de texto (puede haber ThinkingBlocks antes)
235
+ test_code = next(
236
+ (block.text for block in message.content if block.type == "text"),
237
+ None
238
+ )
239
+
240
+ if test_code is None:
241
+ return {"error": "Claude no devolvió contenido de texto"}
242
+
243
+ # Limpiar markdown fences si los hay
244
+ test_code = _clean_code_output(test_code)
245
+
246
+ return {
247
+ "file": path.name,
248
+ "test_code": test_code,
249
+ "tokens_used": {
250
+ "input": message.usage.input_tokens,
251
+ "output": message.usage.output_tokens
252
+ }
253
+ }
254
+
255
+ @mcp.tool()
256
+ def generate_and_validate_tests(
257
+ file_path: str,
258
+ save_to: str = ""
259
+ ) -> dict:
260
+ """
261
+ Flujo completo: genera tests, valida sintaxis, ejecuta y reporta.
262
+
263
+ Esta tool cierra el loop de calidad: no solo genera tests sino que
264
+ verifica que compilen y que pasen antes de entregárselos al usuario.
265
+
266
+ Args:
267
+ file_path: Ruta absoluta al archivo .py a testear
268
+ save_to: Ruta donde guardar el archivo de tests (opcional).
269
+ Si está vacío, usa test_<nombre>.py al lado del archivo original.
270
+
271
+ Returns:
272
+ Dict con:
273
+ - test_code: código de los tests generados
274
+ - saved_to: dónde se guardó
275
+ - syntax_valid: bool
276
+ - execution: resultados de pytest (passed/failed/errors)
277
+ - summary: resumen legible
278
+ """
279
+ path = Path(file_path)
280
+
281
+ # Paso 1: generar los tests (reutilizamos la tool existente)
282
+ generation_result = generate_tests_for_file(file_path)
283
+
284
+ if "error" in generation_result:
285
+ return generation_result
286
+
287
+ test_code = generation_result["test_code"]
288
+
289
+ # Paso 2: validar sintaxis
290
+ syntax_check = _validate_syntax(test_code)
291
+
292
+ if not syntax_check["valid"]:
293
+ return {
294
+ "test_code": test_code,
295
+ "saved_to": None,
296
+ "syntax_valid": False,
297
+ "syntax_error": syntax_check["error"],
298
+ "execution": None,
299
+ "summary": f"❌ Tests generados pero con error de sintaxis: {syntax_check['error']}",
300
+ "tokens_used": generation_result["tokens_used"],
301
+ }
302
+
303
+ # Paso 3: determinar dónde guardar
304
+ if save_to:
305
+ save_path = Path(save_to)
306
+ else:
307
+ save_path = path.parent / f"test_{path.stem}.py"
308
+
309
+ # Paso 4: guardar el archivo
310
+ try:
311
+ save_path.write_text(test_code, encoding="utf-8")
312
+ except Exception as e:
313
+ return {
314
+ "test_code": test_code,
315
+ "saved_to": None,
316
+ "syntax_valid": True,
317
+ "execution": None,
318
+ "summary": f"⚠️ Tests válidos pero no se pudieron guardar: {e}",
319
+ "tokens_used": generation_result["tokens_used"],
320
+ }
321
+
322
+ # Paso 5: ejecutar pytest
323
+ execution = _run_pytest(save_path, source_dir=path.parent)
324
+
325
+ # Paso 6: armar resumen
326
+ if execution.get("all_passed"):
327
+ summary = (
328
+ f"✅ {execution['passed']} tests generados y pasaron. "
329
+ f"Guardado en: {save_path}"
330
+ )
331
+ elif "error" in execution:
332
+ summary = f"⚠️ Tests generados pero no se pudieron ejecutar: {execution['error']}"
333
+ else:
334
+ summary = (
335
+ f"⚠️ {execution.get('passed', 0)} pasaron, "
336
+ f"{execution.get('failed', 0)} fallaron, "
337
+ f"{execution.get('errors', 0)} errores. "
338
+ f"Guardado en: {save_path}"
339
+ )
340
+
341
+ return {
342
+ "test_code": test_code,
343
+ "saved_to": str(save_path),
344
+ "syntax_valid": True,
345
+ "execution": execution,
346
+ "summary": summary,
347
+ "tokens_used": generation_result["tokens_used"],
348
+ }
349
+
350
+ def _validate_syntax(code: str) -> dict:
351
+ """Valida que el código sea Python sintácticamente correcto."""
352
+ try:
353
+ ast.parse(code)
354
+ return {"valid": True}
355
+ except SyntaxError as e:
356
+ return {
357
+ "valid": False,
358
+ "error": f"Línea {e.lineno}: {e.msg}",
359
+ "offset": e.offset,
360
+ }
361
+
362
+
363
+ def _run_pytest(test_file_path: Path, source_dir: Path, timeout: int = 30) -> dict:
364
+ """
365
+ Ejecuta pytest sobre un archivo de tests en un subproceso aislado.
366
+
367
+ Args:
368
+ test_file_path: Ruta al archivo de tests
369
+ source_dir: Directorio desde donde ejecutar pytest (para resolución de imports)
370
+ timeout: Segundos máximos antes de matar el proceso
371
+
372
+ Returns:
373
+ Dict con: passed, failed, errors, output, stderr
374
+ """
375
+ try:
376
+ result = subprocess.run(
377
+ [sys.executable, "-m", "pytest", str(test_file_path), "-v", "--tb=short", "--no-header"],
378
+ cwd=str(source_dir),
379
+ capture_output=True,
380
+ text=True,
381
+ timeout=timeout,
382
+ )
383
+
384
+ output = result.stdout
385
+ stderr = result.stderr
386
+
387
+ # Parsear resultados básicos del output
388
+ passed = output.count(" PASSED")
389
+ failed = output.count(" FAILED")
390
+ errors = output.count(" ERROR")
391
+
392
+ return {
393
+ "passed": passed,
394
+ "failed": failed,
395
+ "errors": errors,
396
+ "all_passed": result.returncode == 0,
397
+ "output": output[-2000:],
398
+ "stderr": stderr[-500:] if stderr else "",
399
+ }
400
+ except subprocess.TimeoutExpired:
401
+ return {
402
+ "error": f"pytest excedió timeout de {timeout}s",
403
+ "all_passed": False,
404
+ }
405
+ except FileNotFoundError:
406
+ return {
407
+ "error": "pytest no está instalado en el entorno",
408
+ "all_passed": False,
409
+ }
410
+ except Exception as e:
411
+ return {
412
+ "error": f"Error ejecutando pytest: {e}",
413
+ "all_passed": False,
414
+ }
415
+
416
+ def run() -> None:
417
+ """Arranca el servidor MCP usando stdio (lo que Claude Desktop espera)."""
418
+ mcp.run(transport="stdio")
419
+
420
+
421
+ if __name__ == "__main__":
422
+ run()