@saulwade/swl-ses 2.3.0 → 2.4.1

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 (327) hide show
  1. package/CLAUDE.md +241 -199
  2. package/README.md +597 -597
  3. package/agentes/_intent-spec.md +73 -73
  4. package/agentes/_propose-step.md +90 -90
  5. package/agentes/accesibilidad-wcag-swl.md +2 -2
  6. package/agentes/arquitecto-swl.md +2 -2
  7. package/agentes/auto-evolucion-swl.md +2 -2
  8. package/agentes/backend-api-swl.md +2 -2
  9. package/agentes/backend-csharp-swl.md +2 -2
  10. package/agentes/backend-go-swl.md +2 -2
  11. package/agentes/backend-java-swl.md +2 -2
  12. package/agentes/backend-node-swl.md +2 -2
  13. package/agentes/backend-python-swl.md +2 -2
  14. package/agentes/backend-rust-swl.md +2 -2
  15. package/agentes/backend-workers-swl.md +2 -2
  16. package/agentes/cloud-infra-swl.md +2 -2
  17. package/agentes/consolidador-swl.md +2 -2
  18. package/agentes/datos-swl.md +2 -2
  19. package/agentes/depurador-swl.md +2 -2
  20. package/agentes/devops-ci-swl.md +2 -2
  21. package/agentes/disenador-ui-swl.md +2 -2
  22. package/agentes/documentador-swl.md +2 -2
  23. package/agentes/frontend-angular-swl.md +2 -2
  24. package/agentes/frontend-css-swl.md +2 -2
  25. package/agentes/frontend-react-swl.md +2 -2
  26. package/agentes/frontend-swl.md +2 -2
  27. package/agentes/frontend-tailwind-swl.md +2 -2
  28. package/agentes/gh-fix-ci-swl.md +275 -275
  29. package/agentes/implementador-swl.md +2 -2
  30. package/agentes/investigador-swl.md +2 -2
  31. package/agentes/investigador-ux-swl.md +2 -2
  32. package/agentes/llm-apps-swl.md +2 -2
  33. package/agentes/migrador-swl.md +2 -2
  34. package/agentes/mobile-android-swl.md +2 -2
  35. package/agentes/mobile-cross-swl.md +2 -2
  36. package/agentes/mobile-ios-swl.md +2 -2
  37. package/agentes/mobile-testing-swl.md +2 -2
  38. package/agentes/nemesis-auditor-swl.md +1 -1
  39. package/agentes/notificador-swl.md +2 -2
  40. package/agentes/observabilidad-swl.md +2 -2
  41. package/agentes/orquestador-swl.md +41 -14
  42. package/agentes/pagos-swl.md +2 -2
  43. package/agentes/perfilador-usuario-swl.md +2 -2
  44. package/agentes/planificador-swl.md +2 -2
  45. package/agentes/producto-prd-swl.md +2 -2
  46. package/agentes/red-team-swl.md +218 -218
  47. package/agentes/release-manager-swl.md +2 -2
  48. package/agentes/rendimiento-swl.md +2 -2
  49. package/agentes/resolutor-build-swl.md +2 -2
  50. package/agentes/revisor-angular-swl.md +2 -2
  51. package/agentes/revisor-codigo-swl.md +36 -3
  52. package/agentes/revisor-csharp-swl.md +2 -2
  53. package/agentes/revisor-go-swl.md +2 -2
  54. package/agentes/revisor-java-swl.md +2 -2
  55. package/agentes/revisor-kotlin-swl.md +2 -2
  56. package/agentes/revisor-nextjs-swl.md +2 -2
  57. package/agentes/revisor-php-swl.md +2 -2
  58. package/agentes/revisor-react-swl.md +2 -2
  59. package/agentes/revisor-rust-swl.md +2 -2
  60. package/agentes/revisor-seguridad-swl.md +2 -2
  61. package/agentes/revisor-swift-swl.md +2 -2
  62. package/agentes/revisor-typescript-swl.md +2 -2
  63. package/agentes/sre-swl.md +2 -2
  64. package/agentes/tdd-qa-swl.md +2 -2
  65. package/comandos/swl/briefing.md +119 -119
  66. package/comandos/swl/contexto.md +0 -2
  67. package/comandos/swl/contribuir.md +233 -233
  68. package/comandos/swl/deuda-codigo.md +97 -0
  69. package/comandos/swl/predecir.md +139 -139
  70. package/comandos/swl/sesiones.md +1 -1
  71. package/gateway/agent-executor.js +1 -1
  72. package/gateway/lib/event-channel.js +191 -191
  73. package/habilidades/agent-deep-links/SKILL.md +148 -148
  74. package/habilidades/agentes-como-servicio/SKILL.md +2 -2
  75. package/habilidades/angular-moderno/SKILL.md +24 -1
  76. package/habilidades/auto-evolucion-protocolo/SKILL.md +276 -276
  77. package/habilidades/backend-async-postgres-testing/SKILL.md +215 -215
  78. package/habilidades/backend-error-design/SKILL.md +221 -221
  79. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  80. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  81. package/habilidades/calidad-anti-patrones-universales/SKILL.md +368 -368
  82. package/habilidades/calidad-contract-testing/SKILL.md +165 -165
  83. package/habilidades/calidad-mutation-testing/SKILL.md +170 -170
  84. package/habilidades/changelog-generator/scripts/parse-commits.js +367 -367
  85. package/habilidades/checklist-seguridad/recursos/stride-cobertura.md +60 -60
  86. package/habilidades/contenedores-docker/SKILL.md +3 -1
  87. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  88. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  89. package/habilidades/drift-detection/SKILL.md +179 -179
  90. package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +1 -1
  91. package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -57
  92. package/habilidades/eval-framework/SKILL.md +212 -212
  93. package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
  94. package/habilidades/harness-claude-code/SKILL.md +4 -4
  95. package/habilidades/harness-claude-code/recursos/disciplina-harness-regla.md +253 -252
  96. package/habilidades/legacy-code-rescue/SKILL.md +268 -267
  97. package/habilidades/meta-skills-estandar/SKILL.md +1 -1
  98. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  99. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +2 -2
  100. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  101. package/habilidades/perfil-usuario/SKILL.md +200 -200
  102. package/habilidades/postgresql-experto/SKILL.md +3 -1
  103. package/habilidades/prevencion-sobreingenieria/SKILL.md +70 -92
  104. package/habilidades/prevencion-sobreingenieria/recursos/EXAMPLES.md +580 -580
  105. package/habilidades/prevencion-sobreingenieria/recursos/soluciones-nativas.md +166 -0
  106. package/habilidades/prevencion-sobreingenieria/recursos/variables-residuales-post-refactor.md +85 -0
  107. package/habilidades/proceso-ddia-fundamentos/SKILL.md +255 -255
  108. package/habilidades/proceso-ddia-streaming/SKILL.md +231 -231
  109. package/habilidades/proceso-debate-adversarial/recursos/personas.md +105 -105
  110. package/habilidades/proceso-discovery-machote/SKILL.md +157 -157
  111. package/habilidades/proceso-dynamic-workflows/SKILL.md +138 -138
  112. package/habilidades/proceso-dynamic-workflows/recursos/template-adversarial-verify.js +65 -65
  113. package/habilidades/proceso-dynamic-workflows/recursos/template-triage.js +65 -65
  114. package/habilidades/proceso-intent-engineering/SKILL.md +269 -269
  115. package/habilidades/proceso-modular-split/SKILL.md +256 -256
  116. package/habilidades/prompt-engineering/recursos/patrones-avanzados.md +2 -2
  117. package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
  118. package/habilidades/structured-outputs/SKILL.md +2 -2
  119. package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -83
  120. package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -85
  121. package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -94
  122. package/habilidades/swl-dashboard/SKILL.md +2 -2
  123. package/habilidades/tdd-workflow/recursos/gherkin-bdd.md +111 -111
  124. package/habilidades/validacion-ci-sistema/recursos/validadores-completos.md +1 -1
  125. package/habilidades/validacion-ci-sistema/scripts/validar-sistema.sh +1 -1
  126. package/hooks/audit-trail.js +4 -4
  127. package/hooks/calidad-pre-commit.js +15 -11
  128. package/hooks/captura-acciones-post.js +3 -2
  129. package/hooks/captura-acciones-session.js +3 -2
  130. package/hooks/ciclo-evolucion-subagente.js +26 -26
  131. package/hooks/ciclo-evolucion.js +26 -26
  132. package/hooks/claudemd-bloat-detector.js +161 -161
  133. package/hooks/claudemd-duplicacion-detector.js +170 -170
  134. package/hooks/contexto-iteracion.js +145 -144
  135. package/hooks/contexto-subagente.js +68 -0
  136. package/hooks/guardrail-modelo.js +14 -4
  137. package/hooks/inyeccion-contexto.js +12 -12
  138. package/hooks/lib/agent-routing.js +107 -107
  139. package/hooks/lib/auto-consolidator.js +335 -335
  140. package/hooks/lib/autonomia.js +208 -208
  141. package/hooks/lib/ciclo-evolucion.js +47 -47
  142. package/hooks/lib/deep-links.js +185 -185
  143. package/hooks/lib/error-classifier.js +308 -308
  144. package/hooks/lib/etapa-auto-evolucion.js +701 -701
  145. package/hooks/lib/etapa-metricas.js +388 -388
  146. package/hooks/lib/etapa-perfil-usuario.js +376 -376
  147. package/hooks/lib/loop-telemetry.js +321 -321
  148. package/hooks/lib/merkle-audit.js +96 -96
  149. package/hooks/lib/model-router.js +2 -2
  150. package/hooks/lib/propose-step.js +358 -358
  151. package/hooks/lib/provenance-tracker.js +191 -191
  152. package/hooks/lib/rate-limit-tracker.js +253 -253
  153. package/hooks/lib/resource-quota.js +122 -122
  154. package/hooks/lib/retry-jitter.js +165 -165
  155. package/hooks/lib/security-net.js +201 -201
  156. package/hooks/lib/skill-auditor.js +588 -588
  157. package/hooks/lib/sync-status.js +228 -228
  158. package/hooks/lib/taint-tracker.js +107 -107
  159. package/hooks/lib/text-similarity.js +241 -241
  160. package/hooks/lib/token-estimator.js +1 -0
  161. package/hooks/lib/toon-compressor.js +245 -245
  162. package/hooks/lib/usage-model.js +1 -1
  163. package/hooks/registro-turnos.js +210 -209
  164. package/hooks/session-briefing.js +98 -98
  165. package/hooks/spec-gate.js +212 -211
  166. package/hooks/sugerir-contribuir.js +2 -1
  167. package/hooks/sugerir-regenerar-inventario.js +171 -170
  168. package/hooks/tdd-gate.js +242 -241
  169. package/hooks/telemetria-skill-routing.js +100 -100
  170. package/hooks/validar-formato-post-subagente.js +140 -140
  171. package/hooks/validar-intent-spec.js +242 -242
  172. package/hooks/validar-memoria-hook.js +218 -218
  173. package/hooks/validar-planning-paths.js +134 -134
  174. package/instintos/autonomia.yaml +27 -27
  175. package/instintos/prompt-appendices.yaml +57 -57
  176. package/llms.txt +29 -29
  177. package/manifiestos/agent-output-schemas.json +57 -57
  178. package/manifiestos/canonical-hashes.json +2948 -1964
  179. package/manifiestos/hook-profiles.json +0 -2
  180. package/manifiestos/hooks-config.json +469 -477
  181. package/manifiestos/invariantes-criticos.json +30 -0
  182. package/manifiestos/modulos.json +1390 -1390
  183. package/manifiestos/planning-paths.json +44 -44
  184. package/manifiestos/skills-lock.json +1282 -1282
  185. package/package.json +93 -93
  186. package/plantillas/auditor-veto-template.md +105 -105
  187. package/plantillas/github-workflows/README.md +47 -47
  188. package/plantillas/github-workflows/release-please.yml +44 -44
  189. package/plantillas/github-workflows/swl-ci.yml +107 -107
  190. package/plantillas/github-workflows/swl-security.yml +51 -51
  191. package/plugin.json +369 -371
  192. package/reglas/accesibilidad.md +10 -10
  193. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  194. package/reglas/api-diseno.md +9 -9
  195. package/reglas/arreglar-al-detectar.md +240 -240
  196. package/reglas/auditorias-documentales-estructurales.md +7 -7
  197. package/reglas/cloud-infra.md +8 -8
  198. package/reglas/consultar-vault-primero.md +195 -195
  199. package/reglas/debatir-antes-de-aceptar.md +158 -158
  200. package/reglas/fragmentos-compartidos.md +183 -183
  201. package/reglas/hooks.md +6 -6
  202. package/reglas/intent-engineering.md +218 -218
  203. package/reglas/markitdown.md +8 -8
  204. package/reglas/monitor-ci.md +309 -309
  205. package/reglas/patrones.md +6 -6
  206. package/reglas/sesiones-paralelas.md +180 -180
  207. package/reglas/sin-duplicacion-reglas-globales.md +182 -182
  208. package/reglas/skills-estandar.md +6 -6
  209. package/reglas/testing.md +7 -7
  210. package/reglas/tests-cleanup.md +224 -224
  211. package/reglas/usar-code-review-graph.md +155 -155
  212. package/reglas/usar-context7.md +226 -226
  213. package/reglas/verificar-citas-normativas.md +548 -548
  214. package/schemas/agent-frontmatter.schema.json +5 -3
  215. package/schemas/agent-message.schema.json +73 -73
  216. package/schemas/agent-output-implementacion.schema.json +114 -114
  217. package/schemas/agent-output-planificacion.schema.json +150 -150
  218. package/schemas/agent-output-review.schema.json +98 -98
  219. package/schemas/diary-entry.schema.json +112 -112
  220. package/schemas/hook-profiles.schema.json +54 -54
  221. package/schemas/hooks-config.schema.json +89 -89
  222. package/schemas/modulos.schema.json +38 -38
  223. package/schemas/perfiles.schema.json +36 -36
  224. package/schemas/plugin.schema.json +77 -77
  225. package/schemas/skill-evals.schema.json +119 -119
  226. package/schemas/skill-frontmatter.schema.json +245 -245
  227. package/scripts/audit-tools/audit-history.js +330 -330
  228. package/scripts/audit-tools/bundle-tracker.js +290 -290
  229. package/scripts/audit-tools/canary-monitor.js +352 -352
  230. package/scripts/audit-tools/code-profiler.js +605 -605
  231. package/scripts/audit-tools/dep-doctor.js +320 -320
  232. package/scripts/audit-tools/env-validator.js +206 -206
  233. package/scripts/audit-tools/lib/fs-walk.js +48 -48
  234. package/scripts/audit-tools/lib/output.js +23 -23
  235. package/scripts/audit-tools/migration-checker.js +392 -392
  236. package/scripts/audit-tools/pentest-scanner.js +1436 -1436
  237. package/scripts/benchmark-memoria.js +167 -167
  238. package/scripts/cli/aprobar-plan.js +73 -73
  239. package/scripts/cli/briefing.js +23 -23
  240. package/scripts/cli/ciclo-evolucion.js +26 -26
  241. package/scripts/cli/configurar-ci.js +40 -40
  242. package/scripts/cli/derivar-feature-list.js +25 -25
  243. package/scripts/cli/detectar-host.js +27 -27
  244. package/scripts/cli/diary-entry.js +69 -69
  245. package/scripts/cli/execution-state.js +18 -18
  246. package/scripts/cli/gateway-notify.js +41 -41
  247. package/scripts/cli/liberar-fase.js +42 -42
  248. package/scripts/cli/loop-telemetry.js +125 -125
  249. package/scripts/cli/mark-evolved.js +56 -56
  250. package/scripts/cli/metricas-dora.js +26 -26
  251. package/scripts/cli/near-duplicate.js +55 -55
  252. package/scripts/cli/notificaciones.js +123 -123
  253. package/scripts/cli/propose-step.js +29 -29
  254. package/scripts/cli/schedule-parse.js +19 -19
  255. package/scripts/cli/sugerir-modelo.js +20 -20
  256. package/scripts/cli/verificar-plan.js +36 -36
  257. package/scripts/cli/verificar-trazabilidad.js +35 -35
  258. package/scripts/configurar-branch-protection.js +418 -418
  259. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  260. package/scripts/field-report.js +199 -199
  261. package/scripts/generar-checklists-consolidados.js +273 -273
  262. package/scripts/generar-matriz-lenguajes.js +271 -271
  263. package/scripts/instalador.js +4 -1
  264. package/scripts/lib/artefactos-python.js +43 -43
  265. package/scripts/lib/auditar-invocaciones-comandos.js +104 -104
  266. package/scripts/lib/benchmark-metrics.js +160 -160
  267. package/scripts/lib/budget-enforcer.js +252 -252
  268. package/scripts/lib/ci-reader.js +193 -193
  269. package/scripts/lib/claude-sessions.js +1 -0
  270. package/scripts/lib/configurar-ci.js +380 -380
  271. package/scripts/lib/contadores-inventario.js +217 -217
  272. package/scripts/lib/detectar-host-swl.js +175 -175
  273. package/scripts/lib/detectar-stack-detallado.js +307 -307
  274. package/scripts/lib/detector-autoduplicacion-intra-archivo.js +234 -234
  275. package/scripts/lib/detector-reglas-duplicadas.js +220 -220
  276. package/scripts/lib/diary-entry.js +234 -234
  277. package/scripts/lib/eval-metrics-store.js +218 -218
  278. package/scripts/lib/eval-quality.js +171 -171
  279. package/scripts/lib/eval-schemas.js +144 -144
  280. package/scripts/lib/eval-self-correct.js +106 -106
  281. package/scripts/lib/eval-validator.js +185 -185
  282. package/scripts/lib/evidencia-release.js +322 -322
  283. package/scripts/lib/gate-hooks-requires.js +249 -249
  284. package/scripts/lib/gate-licencias.js +212 -212
  285. package/scripts/lib/git-metricas.js +257 -257
  286. package/scripts/lib/jaccard-similarity.js +98 -98
  287. package/scripts/lib/longmemeval-runner.js +125 -125
  288. package/scripts/lib/metricas-dora.js +204 -204
  289. package/scripts/lib/npm-version.js +261 -261
  290. package/scripts/lib/paquetes-conocidos.js +50 -50
  291. package/scripts/lib/plan-lock.js +275 -275
  292. package/scripts/lib/pr-analyzer.js +399 -399
  293. package/scripts/lib/preservar-usuario.js +39 -5
  294. package/scripts/lib/prompt-builder.js +264 -264
  295. package/scripts/lib/reglas-globales-conocidas.json +180 -180
  296. package/scripts/lib/resolver-plan-fase.js +37 -37
  297. package/scripts/lib/rrf-fusion.js +175 -175
  298. package/scripts/lib/schema-version.js +164 -164
  299. package/scripts/lib/scoring-instintos.js +277 -277
  300. package/scripts/lib/semantic-search.js +252 -252
  301. package/scripts/lib/tool-cost-analyzer.js +1 -0
  302. package/scripts/limpiar-artefactos-python.js +131 -131
  303. package/scripts/migrar-csv-a-array.js +168 -168
  304. package/scripts/migrar-fase-dominio.js +200 -200
  305. package/scripts/publicar.js +497 -497
  306. package/scripts/run-eval.js +141 -141
  307. package/scripts/tui/componentes/selector-multi.js +189 -189
  308. package/scripts/tui/componentes/selector-unico.js +158 -158
  309. package/scripts/tui/ejecutores.js +375 -375
  310. package/scripts/tui/index.js +162 -162
  311. package/scripts/tui/lib/colores.js +129 -129
  312. package/scripts/tui/lib/render.js +264 -264
  313. package/scripts/tui/lib/teclas.js +113 -113
  314. package/scripts/tui/pantallas/inspect.js +173 -173
  315. package/scripts/tui/pantallas/install-wizard.js +334 -334
  316. package/scripts/tui/pantallas/menu-principal.js +52 -52
  317. package/scripts/tui/pantallas/progreso.js +274 -274
  318. package/scripts/tui/pantallas/resumen.js +132 -132
  319. package/scripts/tui/pantallas/uninstall-wizard.js +208 -208
  320. package/scripts/tui/pantallas/update-wizard.js +232 -232
  321. package/scripts/tui/pantallas/welcome.js +187 -187
  322. package/scripts/validar-userland-vacio.js +110 -110
  323. package/scripts/validar.js +1 -1
  324. package/scripts/verificar-release.js +51 -0
  325. package/hooks/lib/context-compressor.js +0 -657
  326. package/hooks/linea-estado.js +0 -326
  327. package/hooks/monitor-contexto.js +0 -373
@@ -1,215 +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 |
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 |