@saulwade/swl-ses 1.6.1 → 1.6.5
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.
- package/CLAUDE.md +3 -3
- package/README.md +4 -4
- package/agentes/_intent-spec.md +73 -0
- package/agentes/auto-evolucion-swl.md +24 -0
- package/agentes/cloud-infra-swl.md +25 -0
- package/agentes/datos-swl.md +23 -0
- package/agentes/devops-ci-swl.md +24 -0
- package/agentes/gh-fix-ci-swl.md +275 -0
- package/agentes/migrador-swl.md +22 -0
- package/agentes/nemesis-auditor-swl.md +90 -1
- package/agentes/pagos-swl.md +25 -0
- package/agentes/release-manager-swl.md +24 -0
- package/agentes/sre-swl.md +24 -0
- package/comandos/swl/exportar-vault.md +106 -14
- package/comandos/swl/nemesis.md +70 -3
- package/comandos/swl/planear-fase.md +16 -0
- package/comandos/swl/release.md +62 -2
- package/comandos/swl/salud.md +32 -0
- package/comandos/swl/verificar.md +116 -2
- package/habilidades/agent-browser/SKILL.md +111 -4
- package/habilidades/agent-deep-links/SKILL.md +148 -0
- package/habilidades/aprender-de-git-diff/SKILL.md +288 -0
- package/habilidades/backend-async-postgres-testing/SKILL.md +215 -0
- package/habilidades/backend-error-design/SKILL.md +221 -0
- package/habilidades/browser-interaction-patterns/SKILL.md +514 -0
- package/habilidades/browser-research-domains/SKILL.md +635 -0
- package/habilidades/changelog-generator/SKILL.md +172 -0
- package/habilidades/changelog-generator/scripts/parse-commits.js +354 -0
- package/habilidades/devsecops-pipeline-security/SKILL.md +3 -0
- package/habilidades/diseno-herramientas-agente/SKILL.md +17 -1
- package/habilidades/fastapi-experto/SKILL.md +49 -4
- package/habilidades/harness-claude-code/SKILL.md +4 -1
- package/habilidades/meta-skills-estandar/SKILL.md +6 -0
- package/habilidades/meta-skills-estandar/recursos/skill-judge-rubrica.md +281 -0
- package/habilidades/postgresql-experto/SKILL.md +80 -4
- package/habilidades/proceso-autoverificacion-evidencias/SKILL.md +258 -0
- package/habilidades/proceso-confianza-pre-implementacion/SKILL.md +246 -0
- package/habilidades/proceso-ddia-fundamentos/SKILL.md +255 -0
- package/habilidades/proceso-ddia-streaming/SKILL.md +231 -0
- package/habilidades/proceso-discovery-machote/SKILL.md +157 -0
- package/habilidades/proceso-intent-engineering/SKILL.md +269 -0
- package/habilidades/proceso-modular-split/SKILL.md +256 -0
- package/habilidades/reducir-entropia/SKILL.md +219 -0
- package/habilidades/tdd-workflow/SKILL.md +12 -5
- package/hooks/extraccion-aprendizajes.js +8 -0
- package/hooks/lib/deep-links.js +185 -0
- package/hooks/lib/evolution-tracker.js +115 -18
- package/hooks/lib/gateway-notify.js +70 -7
- package/hooks/lib/task-budget.js +218 -0
- package/hooks/validar-intent-spec.js +222 -0
- package/manifiestos/hooks-config.json +9 -0
- package/manifiestos/modulos.json +22 -3
- package/manifiestos/skills-lock.json +1247 -1142
- package/package.json +3 -3
- package/plugin.json +18 -2
- package/reglas/arquitectura.md +38 -0
- package/reglas/arreglar-al-detectar.md +93 -0
- package/reglas/auditorias-documentales-estructurales.md +38 -0
- package/reglas/fragmentos-compartidos.md +26 -0
- package/reglas/intent-engineering.md +214 -0
- package/reglas/registro-componentes-nuevos.md +52 -0
- package/reglas/tests-cleanup.md +220 -0
- package/schemas/agent-frontmatter.schema.json +294 -167
- package/schemas/agent-message.schema.json +73 -53
- package/schemas/agent-output-implementacion.schema.json +114 -85
- package/schemas/agent-output-planificacion.schema.json +150 -113
- package/schemas/agent-output-review.schema.json +98 -78
- package/schemas/diary-entry.schema.json +42 -10
- package/schemas/hook-profiles.schema.json +54 -39
- package/schemas/hooks-config.schema.json +89 -74
- package/schemas/instinct.schema.json +152 -115
- package/schemas/modulos.schema.json +38 -29
- package/schemas/perfiles.schema.json +36 -28
- package/schemas/plugin.schema.json +77 -64
- package/schemas/skill-evals.schema.json +119 -95
- package/schemas/skill-frontmatter.schema.json +245 -170
- package/scripts/generar-inventario.js +3 -1
- package/scripts/lib/mcp_config.py +29 -14
- package/scripts/lib/schema-version.js +164 -0
- package/scripts/mcp-orchestrator.py +153 -131
- package/scripts/mcp-pool-manager.py +132 -107
- package/scripts/mcp-telemetry.py +139 -120
- package/scripts/validar-manifest.js +1 -1
- package/scripts/validar.js +3 -2
- package/scripts/verificar-release.js +199 -1
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aprender-de-git-diff
|
|
3
|
+
description: >
|
|
4
|
+
Extrae una lección dominante (máximo 2-3) desde git history — branch diff
|
|
5
|
+
contra main, últimos N commits, o un commit específico. Mapea los cambios
|
|
6
|
+
observados a principios de ingeniería catalogados en `~/.claude/rules/` y
|
|
7
|
+
`reglas/`. Cargar cuando el usuario pide "qué aprendimos aquí", "reflexión
|
|
8
|
+
sobre el código", "engineering takeaway", o tras un cierre de sesión
|
|
9
|
+
productiva donde valga capturar el patrón validado. Adaptación de
|
|
10
|
+
`lesson-learned` de agent-toolkit. NO escribe APRENDIZAJES.md por sí solo
|
|
11
|
+
— propone la entrada para que el usuario apruebe vía `/swl:aprender`.
|
|
12
|
+
version: "1.0.0"
|
|
13
|
+
herramientasPermitidas: [Read, Grep, Glob, Bash]
|
|
14
|
+
exclusiones:
|
|
15
|
+
- "No cargar si el rango de análisis son <5 LOC o un solo commit de typo/rename/formato — no hay lección que extraer."
|
|
16
|
+
- "No cargar para extraer aprendizaje de feedback verbal del usuario; ese flujo es `/swl:aprender` (triggered por feedback, no por git)."
|
|
17
|
+
- "No cargar para análisis de codebase completo o auditoría — usar `mapear-codebase` o `revisor-codigo-swl`."
|
|
18
|
+
- "No cargar para sesiones sin commits realizados todavía; primero realizar el trabajo, luego analizarlo."
|
|
19
|
+
evolvable: true
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# Habilidad: Aprender desde git diff
|
|
23
|
+
|
|
24
|
+
## Propósito
|
|
25
|
+
|
|
26
|
+
Convertir el código que el usuario acaba de escribir en un espejo de los
|
|
27
|
+
principios de ingeniería que ya está aplicando (o que omite). No es una
|
|
28
|
+
lección abstracta — es la lectura del propio diff con vocabulario de
|
|
29
|
+
principios. El skill triggered por git complementa `/swl:aprender`
|
|
30
|
+
(triggered por feedback) cubriendo el vector "el código habla por sí
|
|
31
|
+
mismo".
|
|
32
|
+
|
|
33
|
+
## Cuándo cargar
|
|
34
|
+
|
|
35
|
+
- Usuario pregunta "¿qué lección hay aquí?", "¿qué aprendimos?",
|
|
36
|
+
"engineering takeaway".
|
|
37
|
+
- Tras un cierre de sesión con commits sustantivos donde valga reforzar el
|
|
38
|
+
patrón validado.
|
|
39
|
+
- Al final de una rama de feature, antes del merge, para capturar el
|
|
40
|
+
principio dominante.
|
|
41
|
+
- Para revisar el propio trabajo del agente al cierre de un PR.
|
|
42
|
+
|
|
43
|
+
## Cuándo NO cargar
|
|
44
|
+
|
|
45
|
+
Listado en `exclusiones` del frontmatter — incluye rangos triviales,
|
|
46
|
+
feedback verbal sin código (esa es `/swl:aprender`), auditoría de codebase
|
|
47
|
+
completo y sesiones sin commits.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Fases del análisis
|
|
52
|
+
|
|
53
|
+
### Fase 1 — Determinar scope
|
|
54
|
+
|
|
55
|
+
Preguntar al usuario o inferir del contexto:
|
|
56
|
+
|
|
57
|
+
| Scope | Comandos git | Cuándo aplicar |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| Branch feature | `git log main..HEAD --oneline` + `git diff main...HEAD` | Rama no-main (default) |
|
|
60
|
+
| Últimos N commits | `git log --oneline -N` + `git diff HEAD~N..HEAD` | Usuario da rango, o estamos en main (N=5 default) |
|
|
61
|
+
| Commit específico | `git show <sha>` | Usuario referencia commit |
|
|
62
|
+
| Working changes | `git diff` + `git diff --cached` | Usuario dice "estos cambios" antes de commit |
|
|
63
|
+
|
|
64
|
+
**Default**: si estamos en rama feature, analizar branch vs main. Si en
|
|
65
|
+
main, últimos 5 commits.
|
|
66
|
+
|
|
67
|
+
### Fase 2 — Recolectar cambios
|
|
68
|
+
|
|
69
|
+
- `git log` para mensajes de commit (los mensajes contienen intent que el
|
|
70
|
+
diff puro pierde).
|
|
71
|
+
- `git diff` para los cambios.
|
|
72
|
+
- Si el diff supera 500 líneas: `git diff --stat` primero, luego leer
|
|
73
|
+
selectivamente los 3-5 archivos con más cambios.
|
|
74
|
+
- **Solo leer archivos cambiados**. No expandir a todo el repo.
|
|
75
|
+
|
|
76
|
+
### Fase 3 — Analizar y mapear a principios
|
|
77
|
+
|
|
78
|
+
Identificar el **patrón dominante** — la cosa más instructiva sobre estos
|
|
79
|
+
cambios. Buscar:
|
|
80
|
+
|
|
81
|
+
- **Decisiones estructurales**: ¿cómo se organizó? ¿por qué esas
|
|
82
|
+
fronteras?
|
|
83
|
+
- **Trade-offs hechos**: ¿qué se ganó vs sacrificó? (legibilidad vs
|
|
84
|
+
rendimiento, DRY vs claridad, velocidad vs corrección).
|
|
85
|
+
- **Problemas resueltos**: ¿qué cambió del antes al después?
|
|
86
|
+
- **Oportunidades perdidas**: ¿dónde podría mejorar? (presentar como
|
|
87
|
+
"la próxima vez, considera...").
|
|
88
|
+
|
|
89
|
+
**Mapeo a catálogo de SWL** — usar como referencia:
|
|
90
|
+
|
|
91
|
+
1. `~/.claude/rules/estilo-codigo.md` — DRY, KISS, funciones cortas,
|
|
92
|
+
early return, sin código muerto.
|
|
93
|
+
2. `~/.claude/rules/arquitectura.md` — SOLID, módulos profundos, DI,
|
|
94
|
+
separación de concerns, patrones reconocidos.
|
|
95
|
+
3. `~/.claude/rules/pruebas.md` — AAA, factories sobre fixtures, tests
|
|
96
|
+
deterministas, edge cases.
|
|
97
|
+
4. `~/.claude/rules/seguridad.md`, `seguridad-agentes.md` — OWASP,
|
|
98
|
+
privilegio mínimo, anti-fallback.
|
|
99
|
+
5. `~/.claude/rules/arreglar-al-detectar.md` — detectar→informar→
|
|
100
|
+
arreglar, anti-deuda silenciosa.
|
|
101
|
+
6. `reglas/` del proyecto — convenciones locales.
|
|
102
|
+
7. `.planning/APRENDIZAJES.md` — patrones ya validados (no repetir).
|
|
103
|
+
|
|
104
|
+
Ser **específico**: citar archivo:línea concreto, no afirmaciones vagas.
|
|
105
|
+
|
|
106
|
+
### Fase 4 — Presentar la lección
|
|
107
|
+
|
|
108
|
+
Plantilla obligatoria:
|
|
109
|
+
|
|
110
|
+
```markdown
|
|
111
|
+
## Lección: [Nombre del principio]
|
|
112
|
+
|
|
113
|
+
**Qué pasó en el código:**
|
|
114
|
+
[2-3 oraciones describiendo el cambio específico, con archivos y commits]
|
|
115
|
+
|
|
116
|
+
**El principio en acción:**
|
|
117
|
+
[1-2 oraciones explicando el principio de ingeniería, citando la regla
|
|
118
|
+
SWL si aplica: `reglas/X.md § sección`]
|
|
119
|
+
|
|
120
|
+
**Por qué importa:**
|
|
121
|
+
[1-2 oraciones sobre la consecuencia práctica — qué saldría mal sin esto,
|
|
122
|
+
qué sale bien gracias a esto]
|
|
123
|
+
|
|
124
|
+
**Para próxima vez:**
|
|
125
|
+
[Una oración accionable y concreta]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Si hay una segunda lección que vale la pena (máximo 2 adicionales):
|
|
129
|
+
|
|
130
|
+
```markdown
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
### También digno de mención: [Principio]
|
|
134
|
+
|
|
135
|
+
**En el código:** [1 oración]
|
|
136
|
+
**El principio:** [1 oración]
|
|
137
|
+
**Para próxima vez:** [1 oración]
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Fase 5 — Propuesta de entrada en APRENDIZAJES.md
|
|
141
|
+
|
|
142
|
+
Tras presentar la lección al usuario, ofrecer la entrada que se podría
|
|
143
|
+
agregar a `.planning/APRENDIZAJES.md`:
|
|
144
|
+
|
|
145
|
+
```markdown
|
|
146
|
+
### [YYYY-MM-DD] <Título corto>
|
|
147
|
+
|
|
148
|
+
**rating**: HIGH / MEDIUM / LOW
|
|
149
|
+
**fuente**: git commits <sha-corto>..<sha-corto>
|
|
150
|
+
**principio**: <referencia a regla SWL>
|
|
151
|
+
|
|
152
|
+
**Contexto**: <qué hacíamos>
|
|
153
|
+
**Lección aplicada**: <lo extraído>
|
|
154
|
+
**Evidencia**: <archivo:línea principal>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
El usuario decide si la entrada se añade vía `/swl:aprender`. Este skill
|
|
158
|
+
**NO escribe directamente APRENDIZAJES.md** — propone, el usuario aprueba.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Qué NO hacer
|
|
163
|
+
|
|
164
|
+
| Evitar | Por qué | En su lugar |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| Listar cada principio que vagamente aplica | Abrumador y genérico | Elegir 1-2 más relevantes |
|
|
167
|
+
| Analizar archivos no cambiados | Scope creep | Stick al diff |
|
|
168
|
+
| Ignorar commit messages | Tienen intent que el diff pierde | Leerlos como contexto primario |
|
|
169
|
+
| Consejo abstracto desconectado del código | No accionable | Siempre referenciar archivo:línea |
|
|
170
|
+
| Feedback solo negativo | Desmoralizante | Liderar con qué funciona, luego sugerir |
|
|
171
|
+
| Más de 3 lecciones | Diluye el insight | Una bien fundada vence siete vagas |
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Reglas obligatorias
|
|
176
|
+
|
|
177
|
+
1. **Reflexivo, no prescriptivo**: usar el código del usuario como
|
|
178
|
+
evidencia primaria. **Por qué**: si el agente da consejo abstracto,
|
|
179
|
+
no usa el material que ya tiene a la vista — desperdicia la
|
|
180
|
+
especificidad del diff.
|
|
181
|
+
|
|
182
|
+
2. **Nunca decir "deberías haber..."**: usar "el enfoque aquí muestra..."
|
|
183
|
+
o "la próxima vez que enfrentes esto, considera...". **Por qué**:
|
|
184
|
+
tono de auditor erosiona la conversación; el skill es espejo, no juez.
|
|
185
|
+
|
|
186
|
+
3. **Si el código es bueno, decirlo**: no toda lección es sobre lo que
|
|
187
|
+
salió mal. Reconocer patrones buenos los refuerza. **Por qué**:
|
|
188
|
+
feedback solo negativo desmoraliza y oculta el valor del trabajo
|
|
189
|
+
completado.
|
|
190
|
+
|
|
191
|
+
4. **Si los cambios son triviales, decirlo**: "Estos cambios son
|
|
192
|
+
directos — no hay lección profunda aquí, solo buen mantenimiento".
|
|
193
|
+
**Por qué**: forzar una lección donde no la hay es ruido.
|
|
194
|
+
|
|
195
|
+
5. **Específico, no genérico**: cada claim apunta a un cambio concreto
|
|
196
|
+
de código con archivo:línea o sha. **Por qué**: consejo genérico ya
|
|
197
|
+
está en las reglas — el valor de este skill es la conexión con el
|
|
198
|
+
diff concreto.
|
|
199
|
+
|
|
200
|
+
6. **Una lección dominante, máximo 3**: priorizar. **Por qué**: 7
|
|
201
|
+
lecciones tibias no equivalen a 1 lección con peso.
|
|
202
|
+
|
|
203
|
+
7. **No escribir APRENDIZAJES.md directamente**: proponer la entrada,
|
|
204
|
+
el usuario aprueba vía `/swl:aprender`. **Por qué**: APRENDIZAJES.md
|
|
205
|
+
es memoria curada del usuario — escritura automática diluye su
|
|
206
|
+
calidad.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Relación con otras herramientas SWL
|
|
211
|
+
|
|
212
|
+
- **`/swl:aprender`**: triggered por feedback verbal del usuario.
|
|
213
|
+
Este skill es triggered por git history. Vectores complementarios.
|
|
214
|
+
- **`revisor-codigo-swl`**: emite veredicto de calidad con score y
|
|
215
|
+
hallazgos. Este skill extrae **principios**, no hallazgos. Pueden
|
|
216
|
+
ejecutarse en secuencia.
|
|
217
|
+
- **`/swl:compactar`** + **`/swl:checkpoint`**: cierran sesión preservando
|
|
218
|
+
estado. Este skill puede invocarse durante el cierre para capturar
|
|
219
|
+
el aprendizaje antes de compactar.
|
|
220
|
+
- **Skill `meta-skills-estandar`**: catálogo de principios para crear
|
|
221
|
+
skills. Este skill consume principios para reportar; meta-skills los
|
|
222
|
+
define para producir.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Ejemplo de uso
|
|
227
|
+
|
|
228
|
+
Usuario tras una sesión con 4 commits: *"¿qué aprendimos aquí?"*.
|
|
229
|
+
|
|
230
|
+
Agente carga este skill:
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
## Lección: Detección y prevención coordinadas
|
|
234
|
+
|
|
235
|
+
**Qué pasó en el código:**
|
|
236
|
+
Los 4 commits añadieron `tests/_helpers/sandbox.js` con auto-cleanup
|
|
237
|
+
vía `node:test after()` y refactorizaron 16 tests existentes para
|
|
238
|
+
usarlo (PR #35). El bug original — 5,800+ carpetas huérfanas en %TEMP%
|
|
239
|
+
— era acumulación de `fs.mkdtempSync` sin `rmSync`.
|
|
240
|
+
|
|
241
|
+
**El principio en acción:**
|
|
242
|
+
Fix sistemático en lugar de fix por archivo, alineado con
|
|
243
|
+
`reglas/arquitectura.md § Módulos profundos` (interfaz pequeña,
|
|
244
|
+
implementación rica). El helper expone 3 funciones (`create`, `list`,
|
|
245
|
+
`cleanup`) que ocultan la mecánica de `mkdtempSync`/`rmSync` y el
|
|
246
|
+
registro automático de `after()`.
|
|
247
|
+
|
|
248
|
+
**Por qué importa:**
|
|
249
|
+
Sin el helper, el patrón se hubiera "olvidado" en el siguiente test
|
|
250
|
+
nuevo — la misma causa raíz se hubiera reintroducido. Con el helper,
|
|
251
|
+
el nuevo test simplemente llama `sandboxes.create()`; el cleanup es
|
|
252
|
+
automático y no negociable.
|
|
253
|
+
|
|
254
|
+
**Para próxima vez:**
|
|
255
|
+
Al detectar un patrón roto en >3 archivos, pausar el fix por archivo
|
|
256
|
+
y diseñar el helper que lo previene estructuralmente.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
### También digno de mención: separación FIFO de teardowns
|
|
261
|
+
|
|
262
|
+
**En el código:** `singleton-guard.test.js` y `nudge-tracker.test.js`
|
|
263
|
+
añadieron `after(() => process.chdir(repoRoot))` ANTES de
|
|
264
|
+
`setupSandboxes()` para que el restore de CWD ocurra antes del
|
|
265
|
+
cleanup, evitando EBUSY en Windows.
|
|
266
|
+
**El principio:** ordering explícito de teardowns donde el cleanup
|
|
267
|
+
depende de estado externo. `node:test` ejecuta los `after()` en FIFO,
|
|
268
|
+
así que el orden de declaración importa.
|
|
269
|
+
**Para próxima vez:** cuando un test cambia CWD, restaurar CWD ANTES
|
|
270
|
+
de cualquier cleanup de filesystem que dependa de ese CWD.
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
**Entrada propuesta para APRENDIZAJES.md** (aprueba con `/swl:aprender` o ignora):
|
|
275
|
+
|
|
276
|
+
### [2026-05-17] Helper estructural para prevenir patrón roto
|
|
277
|
+
|
|
278
|
+
**rating**: HIGH
|
|
279
|
+
**fuente**: commits bff27bb..b431360 (PR #35-#36)
|
|
280
|
+
**principio**: `reglas/arquitectura.md § Módulos profundos`
|
|
281
|
+
|
|
282
|
+
**Contexto**: 16 tests usaban mkdtempSync sin cleanup, acumulando 5,800
|
|
283
|
+
carpetas en %TEMP% tras ~76 corridas.
|
|
284
|
+
**Lección aplicada**: cuando >3 archivos repiten un patrón roto,
|
|
285
|
+
diseñar helper que lo prevenga estructuralmente en lugar de fix por archivo.
|
|
286
|
+
**Evidencia**: `tests/_helpers/sandbox.js:14-30` (3 funciones públicas
|
|
287
|
+
encapsulando 1 cleanup automático).
|
|
288
|
+
```
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: backend-async-postgres-testing
|
|
3
|
+
description: >
|
|
4
|
+
Testing de código backend Python que usa asyncpg con context managers
|
|
5
|
+
asíncronos (`async with conn.transaction()`). Cubre el gotcha crítico de
|
|
6
|
+
AsyncMock que NO genera __aenter__/__aexit__ automáticamente, fixtures
|
|
7
|
+
reusables para mock_conn, testing de race conditions con SELECT FOR UPDATE,
|
|
8
|
+
y patrón de testing para servicios que envuelven INSERTs/UPDATEs en
|
|
9
|
+
transactions. Cargar cuando se escriban tests de services que usan
|
|
10
|
+
`async with conn.transaction():`, cuando un test "deba pasar" pero falla
|
|
11
|
+
con `TypeError: object MagicMock is not async iterable`, o cuando se
|
|
12
|
+
agregue transaction wrapping a un service que tenía tests previos rotos.
|
|
13
|
+
version: "1.0.0"
|
|
14
|
+
herramientasPermitidas: [Read, Grep]
|
|
15
|
+
exclusiones:
|
|
16
|
+
- "No cargar para testing de SQLAlchemy async (AsyncSession) — esos usan `async with session.begin()` con un patrón distinto; cargar `fastapi-experto` que cubre el patrón con sessionmaker."
|
|
17
|
+
- "No cargar para testing de endpoints HTTP — el cliente httpx no requiere mock de transaction; cargar `testing-python` o `fastapi-experto`."
|
|
18
|
+
- "No cargar para testing de capa externa (S3, HTTP client) — esos requieren mock distinto (respx, moto); este skill es específico para BD asyncpg."
|
|
19
|
+
- "No cargar para testing E2E con Postgres real — si la suite levanta BD con testcontainers o docker-compose, NO se mockea transaction; cargar `testing-python`."
|
|
20
|
+
evolvable: true
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# Backend Async Postgres Testing
|
|
24
|
+
|
|
25
|
+
Patrones de mock para código que usa `async with conn.transaction()` de asyncpg.
|
|
26
|
+
|
|
27
|
+
## Cuándo NO cargar
|
|
28
|
+
|
|
29
|
+
- El stack es SQLAlchemy async (`AsyncSession`), no asyncpg raw — cargar `fastapi-experto`.
|
|
30
|
+
- Los tests son E2E con BD real (testcontainers, docker-compose con Postgres) — NO se mockea, cargar `testing-python`.
|
|
31
|
+
- El código no envuelve queries en `async with conn.transaction():` — los mocks simples de `AsyncMock` bastan.
|
|
32
|
+
- Tests de capa externa (S3, HTTP) — esos usan respx/moto, no este patrón.
|
|
33
|
+
|
|
34
|
+
## El gotcha: AsyncMock NO genera context managers async automáticamente
|
|
35
|
+
|
|
36
|
+
SIGM L-157 (2026-05-20). Tras introducir `async with conn.transaction():` en `service.py` para envolver SELECT-then-INSERT y prevenir race conditions, 3 tests existentes rompieron:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
TypeError: 'AsyncMock' object does not support the asynchronous context manager protocol
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
El fixture original era:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
# MAL — AsyncMock NO implementa __aenter__/__aexit__
|
|
46
|
+
@pytest.fixture
|
|
47
|
+
def _mock_conn():
|
|
48
|
+
conn = AsyncMock()
|
|
49
|
+
conn.fetchrow = AsyncMock(return_value={"id": 1})
|
|
50
|
+
conn.execute = AsyncMock(return_value="UPDATE 1")
|
|
51
|
+
return conn
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Al ejecutar:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
async with conn.transaction(): # ← falla aquí
|
|
58
|
+
await conn.execute(...)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`AsyncMock()` retorna un MagicMock cuando se llama, pero ese MagicMock NO es un context manager async — falta `__aenter__` y `__aexit__`.
|
|
62
|
+
|
|
63
|
+
## Patrón correcto: configurar context manager explícitamente
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
67
|
+
|
|
68
|
+
@pytest.fixture
|
|
69
|
+
def _mock_conn():
|
|
70
|
+
"""Mock de asyncpg.Connection con transaction() configurado."""
|
|
71
|
+
conn = AsyncMock()
|
|
72
|
+
|
|
73
|
+
# Configurar transaction() para que retorne un context manager async
|
|
74
|
+
transaction_cm = AsyncMock()
|
|
75
|
+
transaction_cm.__aenter__ = AsyncMock(return_value=transaction_cm)
|
|
76
|
+
transaction_cm.__aexit__ = AsyncMock(return_value=None)
|
|
77
|
+
conn.transaction = MagicMock(return_value=transaction_cm)
|
|
78
|
+
# ↑ MagicMock — porque transaction() es sync (retorna el CM), no async
|
|
79
|
+
|
|
80
|
+
# Métodos comunes pre-configurados
|
|
81
|
+
conn.fetchrow = AsyncMock(return_value=None)
|
|
82
|
+
conn.fetch = AsyncMock(return_value=[])
|
|
83
|
+
conn.execute = AsyncMock(return_value="UPDATE 0")
|
|
84
|
+
conn.executemany = AsyncMock(return_value=None)
|
|
85
|
+
|
|
86
|
+
return conn
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Por qué `transaction` es `MagicMock` y no `AsyncMock`
|
|
90
|
+
|
|
91
|
+
`conn.transaction()` es una llamada sync que retorna un objeto context manager async. La firma real de asyncpg:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
def transaction(self, *, isolation: str = None, ...) -> Transaction:
|
|
95
|
+
"""Retorna un objeto Transaction (sync return)."""
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Si pones `conn.transaction = AsyncMock(...)`, entonces `conn.transaction(...)` retorna un coroutine — y `async with <coroutine>` falla con `TypeError: 'coroutine' object does not support the asynchronous context manager protocol`.
|
|
99
|
+
|
|
100
|
+
Regla:
|
|
101
|
+
- `transaction` (el método): `MagicMock` (sync return).
|
|
102
|
+
- El objeto retornado por `transaction()`: `AsyncMock` con `__aenter__` y `__aexit__` configurados.
|
|
103
|
+
|
|
104
|
+
### Helper reutilizable
|
|
105
|
+
|
|
106
|
+
Para evitar repetir el setup en cada test, extraer a helper:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
# tests/_helpers/asyncpg_mocks.py
|
|
110
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
111
|
+
|
|
112
|
+
def crear_mock_conn(*, fetchrow=None, fetch=None, execute_tag="UPDATE 0") -> AsyncMock:
|
|
113
|
+
"""Mock de asyncpg.Connection con transaction() configurado."""
|
|
114
|
+
conn = AsyncMock()
|
|
115
|
+
|
|
116
|
+
transaction_cm = AsyncMock()
|
|
117
|
+
transaction_cm.__aenter__ = AsyncMock(return_value=transaction_cm)
|
|
118
|
+
transaction_cm.__aexit__ = AsyncMock(return_value=None)
|
|
119
|
+
conn.transaction = MagicMock(return_value=transaction_cm)
|
|
120
|
+
|
|
121
|
+
conn.fetchrow = AsyncMock(return_value=fetchrow)
|
|
122
|
+
conn.fetch = AsyncMock(return_value=fetch or [])
|
|
123
|
+
conn.execute = AsyncMock(return_value=execute_tag)
|
|
124
|
+
return conn
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Uso:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
async def test_crear_valuacion_vigente(monkeypatch):
|
|
131
|
+
conn = crear_mock_conn(
|
|
132
|
+
fetchrow={"id": uuid.uuid4(), "es_vigente": False},
|
|
133
|
+
execute_tag="UPDATE 1",
|
|
134
|
+
)
|
|
135
|
+
service = ValuacionService(conn=conn)
|
|
136
|
+
await service.crear_valuacion_vigente(predio_id, ejercicio, valor)
|
|
137
|
+
|
|
138
|
+
# Verificar que se entró a la transaction
|
|
139
|
+
conn.transaction.assert_called_once()
|
|
140
|
+
# Verificar que se actualizó la fila vigente previa
|
|
141
|
+
conn.execute.assert_any_call(
|
|
142
|
+
"UPDATE valuacion SET es_vigente=false WHERE predio_id=$1 AND ejercicio=$2",
|
|
143
|
+
predio_id, ejercicio,
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Testing de race conditions con SELECT FOR UPDATE
|
|
148
|
+
|
|
149
|
+
Cuando el código real usa `SELECT FOR UPDATE`, el mock debe simular el lock acquired retornando el valor "lockeado" en el primer `fetchrow`:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
async def test_crear_valuacion_concurrente_serializada():
|
|
153
|
+
"""Simula 2 peticiones concurrentes: la 2da debe esperar al lock."""
|
|
154
|
+
conn = crear_mock_conn()
|
|
155
|
+
|
|
156
|
+
# 1ra petición ve la fila vacía
|
|
157
|
+
conn.fetchrow.side_effect = [
|
|
158
|
+
None, # 1er SELECT FOR UPDATE: no hay vigente
|
|
159
|
+
{"id": uuid.uuid4(), "version": 1}, # 2do SELECT FOR UPDATE: ya hay vigente (la 1ra creó)
|
|
160
|
+
]
|
|
161
|
+
conn.execute.return_value = "INSERT 0 1"
|
|
162
|
+
|
|
163
|
+
service = ValuacionService(conn=conn)
|
|
164
|
+
await service.crear_valuacion_vigente(predio_id=p, ejercicio=2026, valor=100)
|
|
165
|
+
await service.crear_valuacion_vigente(predio_id=p, ejercicio=2026, valor=200)
|
|
166
|
+
|
|
167
|
+
# Solo el primer INSERT debe haberse ejecutado
|
|
168
|
+
inserts = [call for call in conn.execute.call_args_list if "INSERT" in str(call)]
|
|
169
|
+
assert len(inserts) == 1
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Esta simulación NO valida que el lock realmente serialize en BD — eso requiere test E2E con Postgres real. Pero SÍ valida que el código de aplicación maneja correctamente el caso "la fila ya existe" cuando emerge del lock.
|
|
173
|
+
|
|
174
|
+
## Verificar la entrada a transaction
|
|
175
|
+
|
|
176
|
+
Cuando es importante verificar que el código envuelve correctamente las queries en `async with conn.transaction():`, agregar aserción:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
async def test_actualizar_estatus_dentro_de_transaction():
|
|
180
|
+
conn = crear_mock_conn(fetchrow={"id": 1, "estatus": "PENDIENTE"})
|
|
181
|
+
service = TramiteService(conn=conn)
|
|
182
|
+
await service.aprobar_tramite(tramite_id=1)
|
|
183
|
+
|
|
184
|
+
# Verificar que SELECT y UPDATE están dentro de la misma transaction
|
|
185
|
+
conn.transaction.assert_called_once()
|
|
186
|
+
# __aenter__ debe haberse llamado antes del primer fetchrow
|
|
187
|
+
transaction_cm = conn.transaction.return_value
|
|
188
|
+
assert transaction_cm.__aenter__.called
|
|
189
|
+
assert transaction_cm.__aexit__.called
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Si el código olvida envolver en transaction, `conn.transaction.assert_called_once()` falla — el test detecta el bug.
|
|
193
|
+
|
|
194
|
+
## Anti-patrones
|
|
195
|
+
|
|
196
|
+
- **Usar `AsyncMock` para `conn.transaction`** — retorna coroutine que no es context manager. Fix: `MagicMock(return_value=AsyncMock_con_aenter_aexit)`.
|
|
197
|
+
- **Compartir `_mock_conn` entre tests sin reset de side_effect** — un test configura `fetchrow.side_effect = [...]` y el siguiente recibe el iterador agotado. Fix: `@pytest.fixture` recrea conn por test (scope `function`, default).
|
|
198
|
+
- **Mockear `fetchrow.return_value = dict` cuando el código real esperaba un `Record`** — los `Record` de asyncpg permiten acceso por índice (`row[0]`) Y por nombre (`row["id"]`). El dict solo permite por nombre. Si el código real usa `row[0]`, el mock con dict falla con `KeyError: 0`. Fix: si el código usa acceso posicional, mockear con un objeto que soporte ambos accesos, o cambiar el código a acceso por nombre.
|
|
199
|
+
- **Olvidar mockear `__aexit__` con `return_value=None`** — si retorna otra cosa, la transaction parece haber fallado y el código puede invocar lógica de rollback que no esperabas. Fix: `__aexit__ = AsyncMock(return_value=None)` explícito.
|
|
200
|
+
|
|
201
|
+
## Gotchas / Errores comunes no obvios
|
|
202
|
+
|
|
203
|
+
- **`conn.transaction()` con `isolation=` o `readonly=` kwargs no se valida en mock**: el mock acepta cualquier kwarg. Si el código real espera `isolation="serializable"` y se cambia a `isolation="read committed"`, el test pasa pero el comportamiento cambia. Fix: aserción explícita `conn.transaction.assert_called_with(isolation="serializable")`.
|
|
204
|
+
- **`AsyncMock(side_effect=Exception)` dentro de transaction provoca llamada a `__aexit__` con exc_type/exc/tb**: para simular rollback, `__aexit__` recibe la excepción como args. Si tu mock tiene `__aexit__ = AsyncMock(return_value=None)`, la excepción se re-eleva. Si retorna `True`, la excepción se suprime (comportamiento normal de context manager). Fix: para tests de rollback, dejar `return_value=None` (deja que la excepción propague) y verificar con `pytest.raises(...)`.
|
|
205
|
+
- **`fetchrow.side_effect = [row, row]` se agota tras 2 llamadas**: en la 3ra llamada lanza `StopIteration`. Si el código real hace 3 fetchrows en distintas ramas, el test falla con error críptico. Fix: usar lista con suficientes elementos o `side_effect` callable que retorna según args (`lambda *args: row if args[0] == "SELECT..." else None`).
|
|
206
|
+
- **`MagicMock` (no `AsyncMock`) en `conn.transaction` permite `assert_called_with(*args)` natural** — `AsyncMock` también lo soporta, pero produce warnings de "coroutine never awaited" si el mock se invoca incorrectamente. La distinción importa para diagnóstico, no para corrección.
|
|
207
|
+
- **`pytest-asyncio` con `mode="auto"` puede no marcar el test como async si no detecta `async def`**: si el fixture es async pero el test es sync, el fixture nunca se await. Fix: explícito `@pytest.mark.asyncio` en el test, o usar `asyncio_mode = "auto"` en `pyproject.toml` y verificar que el test es `async def`.
|
|
208
|
+
|
|
209
|
+
## Referencias
|
|
210
|
+
|
|
211
|
+
| Tema | Recurso |
|
|
212
|
+
|------|---------|
|
|
213
|
+
| Patrón general de async testing en Python | `Skill("testing-python")` |
|
|
214
|
+
| Endpoints FastAPI testing con httpx | `Skill("fastapi-experto")` § Testing |
|
|
215
|
+
| Race conditions en PostgreSQL (lado código) | `Skill("postgresql-experto")` § SELECT-then-INSERT |
|