@saulwade/swl-ses 1.6.3 → 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 +2 -2
- package/agentes/gh-fix-ci-swl.md +275 -0
- package/agentes/nemesis-auditor-swl.md +90 -1
- package/comandos/swl/exportar-vault.md +106 -14
- package/comandos/swl/nemesis.md +70 -3
- 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/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/fastapi-experto/SKILL.md +49 -4
- package/habilidades/harness-claude-code/SKILL.md +4 -1
- package/habilidades/postgresql-experto/SKILL.md +80 -4
- package/habilidades/proceso-discovery-machote/SKILL.md +157 -0
- package/habilidades/proceso-modular-split/SKILL.md +256 -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/manifiestos/modulos.json +13 -3
- package/manifiestos/skills-lock.json +1247 -1191
- package/package.json +3 -3
- package/plugin.json +11 -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/registro-componentes-nuevos.md +14 -0
- package/reglas/tests-cleanup.md +220 -0
- package/scripts/lib/mcp_config.py +29 -14
- package/scripts/mcp-orchestrator.py +153 -131
- package/scripts/mcp-pool-manager.py +132 -107
- package/scripts/mcp-telemetry.py +139 -120
- package/scripts/verificar-release.js +199 -1
|
@@ -8,10 +8,15 @@ description: >
|
|
|
8
8
|
cuando WebFetch devuelve contenido incompleto. También alimenta directamente
|
|
9
9
|
el wiki de proyecto (raw/) con fuentes verificadas.
|
|
10
10
|
user-invocable: false
|
|
11
|
-
version: "1.
|
|
12
|
-
herramientasPermitidas: [Read, Bash, WebFetch]
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
version: "1.2.0"
|
|
12
|
+
herramientasPermitidas: [Read, Bash, WebFetch, Skill]
|
|
13
|
+
skillsInvocables: [browser-interaction-patterns, browser-research-domains]
|
|
14
|
+
evolved: true
|
|
15
|
+
evolved-from: "1.6.3"
|
|
16
|
+
evolved-at: "2026-05-18"
|
|
17
|
+
evolved-by: "aprender"
|
|
18
|
+
evolved-note: "Adoptar patrones CDP de browser-harness (MIT) — coordinate clicks, wait_for_network_idle, dialogs auto-detect; añadir skillsInvocables a 2 skills nuevos consolidados"
|
|
19
|
+
fuente: "Vercel Labs agent-browser — 26K+ GitHub stars (2026-04); patrones CDP avanzados adaptados de browser-use/browser-harness (MIT, 2026)"
|
|
15
20
|
evolvable: true # default para skill estandar
|
|
16
21
|
exclusiones:
|
|
17
22
|
- "No cargar para scraping de páginas estáticas simples — usar WebFetch directamente, es más rápido y sin overhead."
|
|
@@ -216,3 +221,105 @@ puede consumir 50K tokens solo en overhead de scraping. Con agent-browser: ~9K t
|
|
|
216
221
|
**Usar `agent-browser` en un loop con 10+ URLs causa que el proceso Chrome se quede sin memoria y falle silenciosamente**: el Chrome controlado no libera memoria entre páginas del mismo proceso. Causa: cada `open` reutiliza el mismo proceso Chrome sin reiniciarlo. Fix: para loops de investigación intensiva (>10 URLs), dividir en sub-lotes y reiniciar `agent-browser` entre grupos, o procesar en secuencia con pausa de 500ms entre páginas.
|
|
217
222
|
|
|
218
223
|
**`agent-browser navigate` a una página de login redirecciona a la URL original sin error**: si el sitio detecta el browser headless y redirecciona al login, el agente no recibe señal de fallo — simplemente extrae el contenido del formulario de login en lugar de la página deseada. Causa: sites como LinkedIn, Cloudflare-protected pages bloquean headless browsers. Fix: verificar que el output extraído contiene el contenido esperado (>200 palabras reales), no un formulario. Usar `--headed` para hacer el login visible y manual cuando el sitio requiere autenticación humana.
|
|
224
|
+
|
|
225
|
+
## Patrones avanzados de CDP (adoptados de browser-harness MIT)
|
|
226
|
+
|
|
227
|
+
Cuatro patrones que mejoran robustez y reducen tokens, adaptados del runtime
|
|
228
|
+
`browser-use/browser-harness`. Aplican aunque agent-browser sea otra
|
|
229
|
+
implementación (npm CLI vs Python CDP) — son patrones agnósticos al runtime.
|
|
230
|
+
|
|
231
|
+
### 1. Coordinate clicks > selector clicks
|
|
232
|
+
|
|
233
|
+
Cuando la página tiene iframes cross-origin, shadow DOM, o componentes
|
|
234
|
+
complejos, los selectors CSS son frágiles. La alternativa robusta es operar
|
|
235
|
+
a nivel **compositor** de Chrome con eventos de mouse por coordenadas.
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
# MAL — selector frágil cuando hay iframe o shadow DOM
|
|
239
|
+
agent-browser click "button.submit-btn"
|
|
240
|
+
|
|
241
|
+
# BIEN — leer coordenadas del screenshot y click por XY
|
|
242
|
+
agent-browser screenshot /tmp/page.png
|
|
243
|
+
# (visual: el botón está en ~ x=420, y=380)
|
|
244
|
+
agent-browser click-xy 420 380
|
|
245
|
+
agent-browser screenshot /tmp/after.png # verificar
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
El click coordinate atraviesa iframes cross-origin y shadow DOM sin trabajo
|
|
249
|
+
extra porque sucede en el compositor antes de llegar al árbol DOM. Solo bajar
|
|
250
|
+
a DOM-level cuando el elemento NO tiene geometría visible.
|
|
251
|
+
|
|
252
|
+
### 2. `wait_for_network_idle` > `wait` fijo
|
|
253
|
+
|
|
254
|
+
Esperar timeouts fijos (`wait 2000`) es ciego — desperdicia tiempo en páginas
|
|
255
|
+
rápidas y falla en páginas lentas. El patrón correcto es esperar evento-driven
|
|
256
|
+
hasta que NO haya requests pendientes por N ms.
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
# MAL — timeout ciego
|
|
260
|
+
agent-browser open https://app-spa.com
|
|
261
|
+
agent-browser wait 2000
|
|
262
|
+
agent-browser get text "main"
|
|
263
|
+
|
|
264
|
+
# BIEN — esperar idle de red (event-driven)
|
|
265
|
+
agent-browser open https://app-spa.com
|
|
266
|
+
agent-browser wait-network-idle --timeout 10000 --idle-ms 500
|
|
267
|
+
agent-browser get text "main"
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Si tu agent-browser CLI no expone este helper, fallback razonable: `wait` con
|
|
271
|
+
verificación posterior (screenshot + assert que el contenido renderizó).
|
|
272
|
+
|
|
273
|
+
### 3. Screenshots primero para verificación
|
|
274
|
+
|
|
275
|
+
Tras CUALQUIER acción que modifica estado (click, navigate, form submit), la
|
|
276
|
+
única señal confiable de éxito es re-capturar screenshot. El DOM puede
|
|
277
|
+
mentir durante transiciones; `page_info()` puede tener race conditions.
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
# MAL — asumir que el click funcionó
|
|
281
|
+
agent-browser click-xy 420 380
|
|
282
|
+
agent-browser get text ".confirmation-msg" # puede estar vacío
|
|
283
|
+
|
|
284
|
+
# BIEN — screenshot intermedio verifica visualmente
|
|
285
|
+
agent-browser click-xy 420 380
|
|
286
|
+
agent-browser screenshot /tmp/after-click.png
|
|
287
|
+
# Inspeccionar visualmente o cargar a vision LLM para verificar
|
|
288
|
+
agent-browser get text ".confirmation-msg"
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### 4. Auto-detección de dialogs antes de actuar
|
|
292
|
+
|
|
293
|
+
Los dialogs nativos (`alert`, `confirm`, `prompt`, `beforeunload`) **congelan
|
|
294
|
+
el thread de JS** del page. Cualquier acción siguiente cuelga hasta que el
|
|
295
|
+
dialog se resuelva. Patrón seguro: verificar `page_info()` antes de cada
|
|
296
|
+
acción tras navegación o form submit.
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
agent-browser open https://site.com/form
|
|
300
|
+
agent-browser submit-form
|
|
301
|
+
# Antes del siguiente click, verificar
|
|
302
|
+
agent-browser page-info
|
|
303
|
+
# Si retorna {"dialog": {"type": "beforeunload", "message": "..."}},
|
|
304
|
+
# dismissar con accept o cancel antes de continuar
|
|
305
|
+
agent-browser dismiss-dialog --accept
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Si el CLI de agent-browser no expone helper de dialog, la opción CDP raw es
|
|
309
|
+
`Page.handleJavaScriptDialog`. Inyectar stubs JS (`window.alert = ...`) es
|
|
310
|
+
alternativa solo cuando NO hay `beforeunload` involucrado.
|
|
311
|
+
|
|
312
|
+
## Cuándo cargar skills complementarios
|
|
313
|
+
|
|
314
|
+
Cuando este skill base no resuelve el caso:
|
|
315
|
+
|
|
316
|
+
- **`browser-interaction-patterns`** — escalas a un tropiezo específico:
|
|
317
|
+
iframes cross-origin, shadow DOM, dropdown que no responde, upload bloqueado,
|
|
318
|
+
cookies HttpOnly, network requests que no producen DOM change. 14 patrones
|
|
319
|
+
con código MAL/BIEN.
|
|
320
|
+
- **`browser-research-domains`** — el research toca github, arxiv, hackernews,
|
|
321
|
+
stackoverflow, pubmed, openalex o sec edgar. Para esos dominios, **NUNCA
|
|
322
|
+
abras un browser** — todo está en API. Reduce tokens 20-50× y latencia 5-20×.
|
|
323
|
+
|
|
324
|
+
Cargar con `Skill("browser-interaction-patterns")` o
|
|
325
|
+
`Skill("browser-research-domains")` según necesidad.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-deep-links
|
|
3
|
+
description: >
|
|
4
|
+
Constructor y validador de deep links para abrir archivos, líneas, carpetas y
|
|
5
|
+
ajustes directamente en IDEs y editores desde notificaciones, mensajes del
|
|
6
|
+
gateway o output de agentes. Cubre VS Code (vscode://), VS Code Insiders,
|
|
7
|
+
Cursor (cursor://), JetBrains (jetbrains://), Codex Desktop (codex://) y
|
|
8
|
+
fallbacks para apps sin esquema oficial. Cargar cuando un agente o hook deba
|
|
9
|
+
emitir referencias a archivo:línea que el usuario pueda abrir con un clic
|
|
10
|
+
desde Telegram, desktop notifications, Discord, Slack o e-mail.
|
|
11
|
+
version: 1.0.0
|
|
12
|
+
nivelRiesgo: BAJO
|
|
13
|
+
herramientasPermitidas: [Read]
|
|
14
|
+
exclusiones:
|
|
15
|
+
- "No invocar para generar URLs HTTP/HTTPS de navegación web — esto es solo para deep links a apps de escritorio."
|
|
16
|
+
- "No invocar para integraciones con servicios externos (Linear, Notion, Jira); este skill solo cubre IDEs y editores locales del desarrollador."
|
|
17
|
+
- "No invocar para enlaces a sesiones de Claude Code en la nube — esos son URLs claude.ai/code/..., no deep links."
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# /habilidades/agent-deep-links — Deep links a IDEs
|
|
21
|
+
|
|
22
|
+
## Cuándo cargar
|
|
23
|
+
|
|
24
|
+
- Antes de emitir una notificación que incluya referencias a archivos del proyecto
|
|
25
|
+
desde `notificador-swl`, `hooks/lib/gateway-notify.js`, `inbox-aviso.js`,
|
|
26
|
+
`notificacion-sesion-stop.js` o cualquier hook que mencione `archivo:línea`.
|
|
27
|
+
- Al diseñar nuevos hooks o agentes que produzcan output con referencias a código
|
|
28
|
+
que el usuario pueda querer abrir directamente.
|
|
29
|
+
- Antes de generar mensajes desde el `documentador-swl` cuyo destino sea un canal
|
|
30
|
+
externo (Telegram, Discord, e-mail) y el receptor probablemente esté en su
|
|
31
|
+
IDE.
|
|
32
|
+
|
|
33
|
+
## Cuándo NO cargar
|
|
34
|
+
|
|
35
|
+
- Cuando la notificación es solo texto plano sin referencias a archivos
|
|
36
|
+
(mensajes de status, métricas agregadas).
|
|
37
|
+
- Cuando el receptor es un agente, no un humano (los agentes no abren IDEs).
|
|
38
|
+
- Cuando estás generando URLs de navegación web (HTTP/HTTPS) — usa generación
|
|
39
|
+
estándar.
|
|
40
|
+
- Cuando el proyecto no expone su path absoluto al gateway (los deep links de
|
|
41
|
+
IDE requieren absolute paths).
|
|
42
|
+
|
|
43
|
+
## Helper programático
|
|
44
|
+
|
|
45
|
+
La lógica del skill está implementada en `hooks/lib/deep-links.js`. El skill
|
|
46
|
+
documenta **qué** generar; el helper genera **cómo**. Para uso desde código:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
const { construirDeepLink, soportaDeepLinks } = require('./lib/deep-links');
|
|
50
|
+
|
|
51
|
+
const url = construirDeepLink({
|
|
52
|
+
ide: 'cursor', // 'vscode' | 'vscode-insiders' | 'cursor' | 'codex' | 'jetbrains' | 'visualstudio'
|
|
53
|
+
rutaAbsoluta: '/abs/path/a/archivo.js',
|
|
54
|
+
linea: 42,
|
|
55
|
+
columna: 8,
|
|
56
|
+
});
|
|
57
|
+
// → 'cursor://file//abs/path/a/archivo.js:42:8'
|
|
58
|
+
|
|
59
|
+
if (!url) {
|
|
60
|
+
// El IDE solicitado no soporta deep links — usar fallback en texto plano
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Matriz de soporte por IDE
|
|
65
|
+
|
|
66
|
+
| IDE / App | Esquema | Soporte | Patrón canónico | Notas |
|
|
67
|
+
|---|---|---|---|---|
|
|
68
|
+
| **VS Code** | `vscode://` | Total | `vscode://file/<absoluteFile>:<line>:<column>` | Requiere VS Code instalado en máquina del receptor. |
|
|
69
|
+
| **VS Code Insiders** | `vscode-insiders://` | Total | `vscode-insiders://file/<absoluteFile>:<line>:<column>` | Esquema específico Insiders. |
|
|
70
|
+
| **Cursor** | `cursor://` | Total | `cursor://file/<absoluteFile>:<line>:<column>` | Fork de VS Code; misma forma. |
|
|
71
|
+
| **Codex Desktop** | `codex://` | Total | `codex://threads/<thread-uuid>`, `codex://settings` | Para hilos y settings; no archivos. |
|
|
72
|
+
| **JetBrains (IntelliJ/PyCharm/WebStorm/...)** | `jetbrains://` | Total | `jetbrains://<ide-id>/navigate/reference?project=<name>&path=<relPath>` | Requiere nombre del proyecto. Ver fila JetBrains abajo. |
|
|
73
|
+
| **JetBrains Toolbox URL** | `idea://` (deprecado), `jetbrains://idea/...` | Parcial | `jetbrains://idea/navigate/reference?project=<name>&path=<rel>:<line>` | Cada IDE tiene su `<ide-id>`: idea, pycharm, webstorm, goland, rubymine, etc. |
|
|
74
|
+
| **Visual Studio (Windows)** | (no estándar) | Sin esquema oficial | Usar CLI fallback: `devenv /edit <rutaAbsoluta>` | Solo Windows. |
|
|
75
|
+
| **Xcode** | `xcode://` | Parcial | `xcode://...` | Esquema existe; rutas de archivo no bien documentadas. NO confiable. |
|
|
76
|
+
| **Claude Desktop** | `claude://` | Desconocido | `claude://...` | Esquema registrado, rutas no documentadas. |
|
|
77
|
+
| **Codex CLI** (terminal) | n/a | No aplica | n/a | Es CLI; no abre archivos en IDE. Usar fallback texto plano. |
|
|
78
|
+
| **Cualquier app web** | n/a | No aplica | n/a | Si el destino es navegador, usar URL HTTP estándar. |
|
|
79
|
+
|
|
80
|
+
## Reglas de construcción
|
|
81
|
+
|
|
82
|
+
1. **Path absoluto obligatorio**. Deep links con rutas relativas no funcionan.
|
|
83
|
+
El helper rechaza `path.relative` y retorna `null`.
|
|
84
|
+
2. **Encoding de paths con espacios**: aplicar `encodeURI` al path completo (no
|
|
85
|
+
solo al filename). El helper lo hace automáticamente.
|
|
86
|
+
3. **Línea y columna son opcionales**: si no se especifican, el IDE abre al
|
|
87
|
+
inicio del archivo. Línea 1, columna 1 es válido pero redundante.
|
|
88
|
+
4. **JetBrains requiere `proyecto`**: sin el nombre del proyecto, el deep link
|
|
89
|
+
no resuelve. Si no se conoce, usar `vscode://` como fallback (la mayoría de
|
|
90
|
+
los desarrolladores tienen VS Code instalado).
|
|
91
|
+
5. **NUNCA inventar esquemas**: si la matriz dice "No aplica" o "Desconocido",
|
|
92
|
+
el helper retorna `null` y el caller debe usar texto plano.
|
|
93
|
+
|
|
94
|
+
## Formato según receptor
|
|
95
|
+
|
|
96
|
+
| Receptor | Forma del enlace |
|
|
97
|
+
|---|---|
|
|
98
|
+
| **Telegram** (Markdown V2) | `[abrir en IDE](cursor://file//abs/path:42:8)` |
|
|
99
|
+
| **Discord** | `[abrir en IDE](cursor://file//abs/path:42:8)` |
|
|
100
|
+
| **Slack** | `<cursor://file//abs/path:42:8\|abrir en Cursor>` (pipe-separado) |
|
|
101
|
+
| **Desktop notification** (Linux/macOS) | URL plana sin label; el notificador maneja el clic |
|
|
102
|
+
| **E-mail HTML** | `<a href="cursor://file//abs/path:42:8">abrir en Cursor</a>` |
|
|
103
|
+
| **Texto plano / fallback** | `archivo.js:42 (abrir manualmente)` |
|
|
104
|
+
|
|
105
|
+
## Gotchas observables
|
|
106
|
+
|
|
107
|
+
- **El receptor debe tener el IDE instalado**: un enlace `cursor://` no funciona
|
|
108
|
+
si el receptor no tiene Cursor instalado. NO hay forma de detectarlo desde
|
|
109
|
+
el emisor; documentar al usuario que el deep link es opt-in.
|
|
110
|
+
- **Slack tiene su propia sintaxis** `<url|label>`, no Markdown estándar. Mezclar
|
|
111
|
+
los formatos rompe el clic.
|
|
112
|
+
- **Windows usa backslashes**: el path absoluto en Windows es
|
|
113
|
+
`C:\Users\...\archivo.js`. El esquema `vscode://file/C:\\Users\\...` requiere
|
|
114
|
+
doble-backslash en algunos casos; el helper normaliza con `path.resolve` +
|
|
115
|
+
`encodeURI` para producir `vscode://file/C:/Users/...` (slashes forward) que
|
|
116
|
+
VS Code/Cursor sí aceptan correctamente.
|
|
117
|
+
- **JetBrains URL Handler debe estar habilitado**: en JetBrains está bajo
|
|
118
|
+
Settings → Tools → Web Browsers and Preview → "Use JetBrains as a
|
|
119
|
+
redirect..." NO está activado por defecto en todas las distribuciones.
|
|
120
|
+
|
|
121
|
+
## Integración con SWL
|
|
122
|
+
|
|
123
|
+
Hoy se integra en `hooks/lib/gateway-notify.js` (opt-in): cuando un caller pasa
|
|
124
|
+
`payload.fileRef = { archivo, linea, columna }` Y existe `payload.idePreferido`,
|
|
125
|
+
el helper enriquece el mensaje con el deep link en el formato del adaptador
|
|
126
|
+
destino (Telegram / Discord / etc.).
|
|
127
|
+
|
|
128
|
+
Extensiones futuras (no implementadas hoy):
|
|
129
|
+
|
|
130
|
+
- `notificador-swl` (Telegram nativo): pasar `idePreferido` desde
|
|
131
|
+
`instintos/perfil-usuario.yaml § ide_preferido`.
|
|
132
|
+
- `documentador-swl`: enlaces a archivos citados en docs generados.
|
|
133
|
+
- `revisor-codigo-swl`: enlaces a archivo:línea en reportes de hallazgos.
|
|
134
|
+
|
|
135
|
+
## Origen
|
|
136
|
+
|
|
137
|
+
Adaptado del skill `agent-deep-links` de
|
|
138
|
+
`temp/awesome-codex-skills-master/agent-deep-links/` (ComposioHQ, MIT) con:
|
|
139
|
+
|
|
140
|
+
- Frontmatter al estándar SWL (es-MX, `nivelRiesgo`, `exclusiones`,
|
|
141
|
+
`herramientasPermitidas`).
|
|
142
|
+
- Matriz extendida con JetBrains (no estaba) y Visual Studio fallback.
|
|
143
|
+
- Sección "Formato según receptor" agregada para Telegram/Discord además del
|
|
144
|
+
Slack original.
|
|
145
|
+
- Helper Node.js (`hooks/lib/deep-links.js`) que el repo origen no tenía.
|
|
146
|
+
- Sección "Integración con SWL" agregada.
|
|
147
|
+
|
|
148
|
+
Documentado en ADR-0029 (integración parcial awesome-codex-skills, Opción B).
|
|
@@ -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 |
|