@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.
Files changed (85) hide show
  1. package/CLAUDE.md +3 -3
  2. package/README.md +4 -4
  3. package/agentes/_intent-spec.md +73 -0
  4. package/agentes/auto-evolucion-swl.md +24 -0
  5. package/agentes/cloud-infra-swl.md +25 -0
  6. package/agentes/datos-swl.md +23 -0
  7. package/agentes/devops-ci-swl.md +24 -0
  8. package/agentes/gh-fix-ci-swl.md +275 -0
  9. package/agentes/migrador-swl.md +22 -0
  10. package/agentes/nemesis-auditor-swl.md +90 -1
  11. package/agentes/pagos-swl.md +25 -0
  12. package/agentes/release-manager-swl.md +24 -0
  13. package/agentes/sre-swl.md +24 -0
  14. package/comandos/swl/exportar-vault.md +106 -14
  15. package/comandos/swl/nemesis.md +70 -3
  16. package/comandos/swl/planear-fase.md +16 -0
  17. package/comandos/swl/release.md +62 -2
  18. package/comandos/swl/salud.md +32 -0
  19. package/comandos/swl/verificar.md +116 -2
  20. package/habilidades/agent-browser/SKILL.md +111 -4
  21. package/habilidades/agent-deep-links/SKILL.md +148 -0
  22. package/habilidades/aprender-de-git-diff/SKILL.md +288 -0
  23. package/habilidades/backend-async-postgres-testing/SKILL.md +215 -0
  24. package/habilidades/backend-error-design/SKILL.md +221 -0
  25. package/habilidades/browser-interaction-patterns/SKILL.md +514 -0
  26. package/habilidades/browser-research-domains/SKILL.md +635 -0
  27. package/habilidades/changelog-generator/SKILL.md +172 -0
  28. package/habilidades/changelog-generator/scripts/parse-commits.js +354 -0
  29. package/habilidades/devsecops-pipeline-security/SKILL.md +3 -0
  30. package/habilidades/diseno-herramientas-agente/SKILL.md +17 -1
  31. package/habilidades/fastapi-experto/SKILL.md +49 -4
  32. package/habilidades/harness-claude-code/SKILL.md +4 -1
  33. package/habilidades/meta-skills-estandar/SKILL.md +6 -0
  34. package/habilidades/meta-skills-estandar/recursos/skill-judge-rubrica.md +281 -0
  35. package/habilidades/postgresql-experto/SKILL.md +80 -4
  36. package/habilidades/proceso-autoverificacion-evidencias/SKILL.md +258 -0
  37. package/habilidades/proceso-confianza-pre-implementacion/SKILL.md +246 -0
  38. package/habilidades/proceso-ddia-fundamentos/SKILL.md +255 -0
  39. package/habilidades/proceso-ddia-streaming/SKILL.md +231 -0
  40. package/habilidades/proceso-discovery-machote/SKILL.md +157 -0
  41. package/habilidades/proceso-intent-engineering/SKILL.md +269 -0
  42. package/habilidades/proceso-modular-split/SKILL.md +256 -0
  43. package/habilidades/reducir-entropia/SKILL.md +219 -0
  44. package/habilidades/tdd-workflow/SKILL.md +12 -5
  45. package/hooks/extraccion-aprendizajes.js +8 -0
  46. package/hooks/lib/deep-links.js +185 -0
  47. package/hooks/lib/evolution-tracker.js +115 -18
  48. package/hooks/lib/gateway-notify.js +70 -7
  49. package/hooks/lib/task-budget.js +218 -0
  50. package/hooks/validar-intent-spec.js +222 -0
  51. package/manifiestos/hooks-config.json +9 -0
  52. package/manifiestos/modulos.json +22 -3
  53. package/manifiestos/skills-lock.json +1247 -1142
  54. package/package.json +3 -3
  55. package/plugin.json +18 -2
  56. package/reglas/arquitectura.md +38 -0
  57. package/reglas/arreglar-al-detectar.md +93 -0
  58. package/reglas/auditorias-documentales-estructurales.md +38 -0
  59. package/reglas/fragmentos-compartidos.md +26 -0
  60. package/reglas/intent-engineering.md +214 -0
  61. package/reglas/registro-componentes-nuevos.md +52 -0
  62. package/reglas/tests-cleanup.md +220 -0
  63. package/schemas/agent-frontmatter.schema.json +294 -167
  64. package/schemas/agent-message.schema.json +73 -53
  65. package/schemas/agent-output-implementacion.schema.json +114 -85
  66. package/schemas/agent-output-planificacion.schema.json +150 -113
  67. package/schemas/agent-output-review.schema.json +98 -78
  68. package/schemas/diary-entry.schema.json +42 -10
  69. package/schemas/hook-profiles.schema.json +54 -39
  70. package/schemas/hooks-config.schema.json +89 -74
  71. package/schemas/instinct.schema.json +152 -115
  72. package/schemas/modulos.schema.json +38 -29
  73. package/schemas/perfiles.schema.json +36 -28
  74. package/schemas/plugin.schema.json +77 -64
  75. package/schemas/skill-evals.schema.json +119 -95
  76. package/schemas/skill-frontmatter.schema.json +245 -170
  77. package/scripts/generar-inventario.js +3 -1
  78. package/scripts/lib/mcp_config.py +29 -14
  79. package/scripts/lib/schema-version.js +164 -0
  80. package/scripts/mcp-orchestrator.py +153 -131
  81. package/scripts/mcp-pool-manager.py +132 -107
  82. package/scripts/mcp-telemetry.py +139 -120
  83. package/scripts/validar-manifest.js +1 -1
  84. package/scripts/validar.js +3 -2
  85. 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 |