@saulwade/swl-ses 2.6.1 → 2.8.0

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 (268) hide show
  1. package/CLAUDE.md +14 -2
  2. package/README.md +65 -18
  3. package/agentes/_intent-spec.md +73 -73
  4. package/agentes/_propose-step.md +90 -90
  5. package/bin/swl-ses.js +10 -0
  6. package/comandos/swl/brainstorm.md +1 -0
  7. package/comandos/swl/briefing.md +119 -119
  8. package/comandos/swl/contribuir.md +233 -233
  9. package/comandos/swl/deuda-codigo.md +97 -97
  10. package/comandos/swl/mcp-status.md +1 -0
  11. package/gateway/lib/event-channel.js +191 -191
  12. package/habilidades/agent-deep-links/SKILL.md +148 -148
  13. package/habilidades/backend-async-postgres-testing/SKILL.md +216 -216
  14. package/habilidades/backend-error-design/SKILL.md +221 -221
  15. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  16. package/habilidades/calidad-anti-patrones-universales/SKILL.md +105 -1
  17. package/habilidades/calidad-contract-testing/SKILL.md +165 -165
  18. package/habilidades/calidad-mutation-testing/SKILL.md +25 -1
  19. package/habilidades/checklist-seguridad/recursos/stride-cobertura.md +60 -60
  20. package/habilidades/ci-cd-pipelines/SKILL.md +5 -1
  21. package/habilidades/css-moderno/SKILL.md +7 -1
  22. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  23. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  24. package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -57
  25. package/habilidades/extractor-de-aprendizajes/SKILL.md +5 -1
  26. package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
  27. package/habilidades/harness-claude-code/SKILL.md +3 -2
  28. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  29. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  30. package/habilidades/perfil-usuario/SKILL.md +200 -200
  31. package/habilidades/prevencion-sobreingenieria/recursos/EXAMPLES.md +580 -580
  32. package/habilidades/prevencion-sobreingenieria/recursos/soluciones-nativas.md +166 -166
  33. package/habilidades/prevencion-sobreingenieria/recursos/variables-residuales-post-refactor.md +85 -85
  34. package/habilidades/proceso-ddia-streaming/SKILL.md +231 -231
  35. package/habilidades/proceso-discovery-machote/SKILL.md +157 -157
  36. package/habilidades/proceso-dynamic-workflows/SKILL.md +60 -0
  37. package/habilidades/proceso-dynamic-workflows/recursos/template-adversarial-verify.js +65 -65
  38. package/habilidades/proceso-dynamic-workflows/recursos/template-triage.js +65 -65
  39. package/habilidades/proceso-intent-engineering/SKILL.md +269 -269
  40. package/habilidades/proceso-modular-split/SKILL.md +256 -256
  41. package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
  42. package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -83
  43. package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -85
  44. package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -94
  45. package/habilidades/tdd-workflow/recursos/gherkin-bdd.md +111 -111
  46. package/hooks/calidad-pre-commit.js +159 -10
  47. package/hooks/ciclo-evolucion-subagente.js +26 -26
  48. package/hooks/ciclo-evolucion.js +26 -26
  49. package/hooks/contexto-subagente.js +68 -68
  50. package/hooks/lib/auto-consolidator.js +335 -335
  51. package/hooks/lib/ciclo-evolucion.js +47 -47
  52. package/hooks/lib/deep-links.js +185 -185
  53. package/hooks/lib/error-classifier.js +308 -308
  54. package/hooks/lib/notificacion-formato.js +45 -11
  55. package/hooks/lib/provenance-tracker.js +191 -191
  56. package/hooks/lib/raiz-proyecto.js +35 -4
  57. package/hooks/lib/resource-quota.js +122 -122
  58. package/hooks/lib/retry-jitter.js +165 -165
  59. package/hooks/lib/security-net.js +201 -201
  60. package/hooks/lib/skill-auditor.js +588 -588
  61. package/hooks/lib/sync-status.js +228 -228
  62. package/hooks/lib/taint-tracker.js +107 -107
  63. package/hooks/lib/text-similarity.js +241 -241
  64. package/hooks/lib/toon-compressor.js +245 -245
  65. package/hooks/notificacion-telegram.js +5 -11
  66. package/hooks/session-briefing.js +12 -4
  67. package/instintos/autonomia.yaml +27 -27
  68. package/instintos/prompt-appendices.yaml +57 -57
  69. package/llms.txt +1 -1
  70. package/manifiestos/agent-output-schemas.json +57 -57
  71. package/manifiestos/canonical-hashes.json +662 -0
  72. package/manifiestos/harness-ir.json +47536 -0
  73. package/manifiestos/hooks-config.json +469 -469
  74. package/manifiestos/invariantes-criticos.json +30 -30
  75. package/manifiestos/policy-bundle.json +2065 -0
  76. package/manifiestos/policy-corpus-w2.json +3926 -0
  77. package/manifiestos/runtime-adapters-core3.json +208 -0
  78. package/manifiestos/runtime-conformance.json +139 -0
  79. package/manifiestos/skills-lock.json +43 -43
  80. package/package.json +2 -2
  81. package/plantillas/auditor-veto-template.md +105 -105
  82. package/plantillas/github-workflows/release-please.yml +44 -44
  83. package/plantillas/github-workflows/swl-ci.yml +107 -107
  84. package/plantillas/github-workflows/swl-security.yml +51 -51
  85. package/plugin.json +2 -2
  86. package/reglas/accesibilidad.md +10 -10
  87. package/reglas/auditorias-documentales-estructurales.md +7 -7
  88. package/reglas/cloud-infra.md +8 -8
  89. package/reglas/consultar-vault-primero.md +195 -195
  90. package/reglas/git-workflow.md +1 -0
  91. package/reglas/hooks.md +6 -6
  92. package/reglas/intent-engineering.md +218 -218
  93. package/reglas/markitdown.md +8 -8
  94. package/reglas/monitor-ci.md +12 -0
  95. package/reglas/patrones.md +6 -6
  96. package/reglas/testing.md +7 -7
  97. package/reglas/tests-cleanup.md +224 -224
  98. package/schemas/agent-message.schema.json +73 -73
  99. package/schemas/agent-output-implementacion.schema.json +114 -114
  100. package/schemas/agent-output-planificacion.schema.json +150 -150
  101. package/schemas/agent-output-review.schema.json +98 -98
  102. package/schemas/diary-entry.schema.json +112 -112
  103. package/schemas/gate-state.schema.json +76 -0
  104. package/schemas/harness-ir.schema.json +369 -0
  105. package/schemas/hook-profiles.schema.json +54 -54
  106. package/schemas/hooks-config.schema.json +89 -89
  107. package/schemas/legacy-gates.schema.json +45 -0
  108. package/schemas/modulos.schema.json +38 -38
  109. package/schemas/perfiles.schema.json +36 -36
  110. package/schemas/plugin.schema.json +77 -77
  111. package/schemas/policy-bundle.schema.json +140 -0
  112. package/schemas/policy-enforcement.schema.json +117 -0
  113. package/schemas/policy-operation.schema.json +261 -0
  114. package/schemas/runtime-adapter.schema.json +176 -0
  115. package/schemas/runtime-build-attestation.schema.json +100 -0
  116. package/schemas/runtime-conformance.schema.json +239 -0
  117. package/schemas/runtime-diagnostic.schema.json +395 -0
  118. package/schemas/skill-evals.schema.json +119 -119
  119. package/schemas/skill-frontmatter.schema.json +245 -245
  120. package/schemas/w4-certification-request.schema.json +72 -0
  121. package/schemas/w4-certification-verdict.schema.json +224 -0
  122. package/schemas/w4-corpus.schema.json +172 -0
  123. package/schemas/w4-mutation-report.schema.json +116 -0
  124. package/schemas/w4-replay-result.schema.json +164 -0
  125. package/schemas/w4-scoring-report.schema.json +89 -0
  126. package/scripts/audit-tools/audit-history.js +330 -330
  127. package/scripts/audit-tools/bundle-tracker.js +290 -290
  128. package/scripts/audit-tools/canary-monitor.js +352 -352
  129. package/scripts/audit-tools/code-profiler.js +605 -605
  130. package/scripts/audit-tools/dep-doctor.js +320 -320
  131. package/scripts/audit-tools/env-validator.js +206 -206
  132. package/scripts/audit-tools/lib/fs-walk.js +48 -48
  133. package/scripts/audit-tools/lib/output.js +23 -23
  134. package/scripts/audit-tools/migration-checker.js +392 -392
  135. package/scripts/audit-tools/pentest-scanner.js +1436 -1436
  136. package/scripts/bootstrap-instintos.js +3 -0
  137. package/scripts/cli/aprobar-plan.js +73 -73
  138. package/scripts/cli/briefing.js +23 -23
  139. package/scripts/cli/ciclo-evolucion.js +26 -26
  140. package/scripts/cli/derivar-feature-list.js +25 -25
  141. package/scripts/cli/detectar-host.js +27 -27
  142. package/scripts/cli/diary-entry.js +69 -69
  143. package/scripts/cli/execution-state.js +18 -18
  144. package/scripts/cli/gateway-notify.js +41 -41
  145. package/scripts/cli/liberar-fase.js +42 -42
  146. package/scripts/cli/mark-evolved.js +56 -56
  147. package/scripts/cli/metricas-dora.js +26 -26
  148. package/scripts/cli/near-duplicate.js +55 -55
  149. package/scripts/cli/notificaciones.js +123 -123
  150. package/scripts/cli/propose-step.js +29 -29
  151. package/scripts/cli/schedule-parse.js +19 -19
  152. package/scripts/cli/sugerir-modelo.js +20 -20
  153. package/scripts/cli/verificar-plan.js +36 -36
  154. package/scripts/cli/verificar-trazabilidad.js +35 -35
  155. package/scripts/comandos/install-asistido.js +8 -7
  156. package/scripts/configurar-branch-protection.js +418 -418
  157. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  158. package/scripts/doctor.js +61 -36
  159. package/scripts/generar-checklists-consolidados.js +273 -273
  160. package/scripts/generar-claims-runtime.js +1342 -0
  161. package/scripts/generar-harness-ir.js +257 -0
  162. package/scripts/generar-inventario.js +52 -54
  163. package/scripts/generar-policy-bundle.js +202 -0
  164. package/scripts/instalador.js +26 -7
  165. package/scripts/lib/approval-receipts.js +190 -0
  166. package/scripts/lib/artefactos-python.js +43 -43
  167. package/scripts/lib/benchmark-metrics.js +160 -160
  168. package/scripts/lib/budget-enforcer.js +252 -252
  169. package/scripts/lib/certificacion-loop-state.js +421 -0
  170. package/scripts/lib/ci-reader.js +193 -193
  171. package/scripts/lib/ciclo-autonomo/yaml-instintos.js +56 -0
  172. package/scripts/lib/clasificar-directorio.js +92 -0
  173. package/scripts/lib/contadores-inventario.js +217 -217
  174. package/scripts/lib/detectar-host-swl.js +175 -175
  175. package/scripts/lib/detectar-runtime.js +29 -20
  176. package/scripts/lib/detectar-stack-detallado.js +307 -307
  177. package/scripts/lib/detector-autoduplicacion-intra-archivo.js +234 -234
  178. package/scripts/lib/detector-reglas-duplicadas.js +220 -220
  179. package/scripts/lib/eval-metrics-store.js +218 -218
  180. package/scripts/lib/eval-quality.js +171 -171
  181. package/scripts/lib/eval-schemas.js +144 -144
  182. package/scripts/lib/eval-self-correct.js +106 -106
  183. package/scripts/lib/eval-validator.js +185 -185
  184. package/scripts/lib/evidence-verifier.js +192 -0
  185. package/scripts/lib/evidencia-release.js +322 -322
  186. package/scripts/lib/frontmatter-canonico.js +509 -0
  187. package/scripts/lib/gate-engine.js +871 -0
  188. package/scripts/lib/gate-hooks-requires.js +249 -249
  189. package/scripts/lib/gate-licencias.js +212 -212
  190. package/scripts/lib/git-config-preflight.js +48 -0
  191. package/scripts/lib/git-metricas.js +257 -257
  192. package/scripts/lib/harness-ir.js +778 -0
  193. package/scripts/lib/harness-source-snapshot.js +309 -0
  194. package/scripts/lib/integrity-ledger.js +1147 -0
  195. package/scripts/lib/jaccard-similarity.js +98 -98
  196. package/scripts/lib/legacy-gate-migration.js +324 -0
  197. package/scripts/lib/limpiar-basura-global.js +45 -2
  198. package/scripts/lib/longmemeval-runner.js +125 -125
  199. package/scripts/lib/metricas-dora.js +204 -204
  200. package/scripts/lib/notificaciones-telegram.js +1 -0
  201. package/scripts/lib/npm-version.js +1 -0
  202. package/scripts/lib/paquetes-conocidos.js +50 -50
  203. package/scripts/lib/plan-lock.js +61 -13
  204. package/scripts/lib/policy-broker.js +338 -0
  205. package/scripts/lib/policy-bundle.js +342 -0
  206. package/scripts/lib/policy-context-provider.js +310 -0
  207. package/scripts/lib/policy-contract.js +479 -0
  208. package/scripts/lib/policy-verifier-utils.js +65 -0
  209. package/scripts/lib/pr-analyzer.js +399 -399
  210. package/scripts/lib/principal-verifier.js +178 -0
  211. package/scripts/lib/prompt-builder.js +264 -264
  212. package/scripts/lib/resolver-plan-fase.js +37 -37
  213. package/scripts/lib/rrf-fusion.js +175 -175
  214. package/scripts/lib/runtime-adapter-contract.js +267 -0
  215. package/scripts/lib/runtime-artifact-verifier.js +426 -0
  216. package/scripts/lib/runtime-build-attestation.js +127 -0
  217. package/scripts/lib/runtime-bundle-installer.js +586 -0
  218. package/scripts/lib/runtime-compiler.js +327 -0
  219. package/scripts/lib/runtime-conformance.js +202 -0
  220. package/scripts/lib/runtime-doctor-core3.js +567 -0
  221. package/scripts/lib/runtime-doctor-input.js +59 -0
  222. package/scripts/lib/runtime-operation-adapter.js +267 -0
  223. package/scripts/lib/schema-version.js +164 -164
  224. package/scripts/lib/semantic-search.js +252 -252
  225. package/scripts/lib/signed-envelope.js +545 -0
  226. package/scripts/lib/single-use-store.js +359 -0
  227. package/scripts/lib/skills-externas.js +31 -0
  228. package/scripts/lib/transformadores/codex.js +15 -8
  229. package/scripts/lib/transformadores/gemini.js +79 -5
  230. package/scripts/lib/w4-attestation-adapter.js +158 -0
  231. package/scripts/lib/w4-canario.js +337 -0
  232. package/scripts/lib/w4-claims.js +182 -0
  233. package/scripts/lib/w4-corpus-generador.js +542 -0
  234. package/scripts/lib/w4-gate-c5.js +115 -0
  235. package/scripts/lib/w4-harness-bajo-prueba.js +155 -0
  236. package/scripts/lib/w4-matriz-combos.js +55 -0
  237. package/scripts/lib/w4-motor-mutacion.js +1348 -0
  238. package/scripts/lib/w4-motor-replay.js +735 -0
  239. package/scripts/lib/w4-pin-origen.js +54 -0
  240. package/scripts/lib/w4-publicar-request.js +132 -0
  241. package/scripts/lib/w4-revocacion.js +62 -0
  242. package/scripts/lib/w4-runtimes-core3.js +38 -0
  243. package/scripts/lib/w4-scorer-certificacion.js +692 -0
  244. package/scripts/lib/w4-superficie-candidato.js +49 -0
  245. package/scripts/lib/w4-veredicto.js +452 -0
  246. package/scripts/lib/w4-verificar-veredicto.js +302 -0
  247. package/scripts/limpiar-artefactos-python.js +131 -131
  248. package/scripts/migrar-csv-a-array.js +168 -168
  249. package/scripts/migrar-fase-dominio.js +200 -200
  250. package/scripts/migrar-gates-legacy.js +108 -0
  251. package/scripts/publicar-certification-request.js +115 -0
  252. package/scripts/runtime-doctor.js +107 -0
  253. package/scripts/tui/componentes/selector-multi.js +189 -189
  254. package/scripts/tui/componentes/selector-unico.js +158 -158
  255. package/scripts/tui/ejecutores.js +375 -375
  256. package/scripts/tui/lib/colores.js +129 -129
  257. package/scripts/tui/lib/render.js +264 -264
  258. package/scripts/tui/lib/teclas.js +113 -113
  259. package/scripts/tui/pantallas/install-wizard.js +12 -7
  260. package/scripts/tui/pantallas/menu-principal.js +52 -52
  261. package/scripts/tui/pantallas/progreso.js +274 -274
  262. package/scripts/tui/pantallas/resumen.js +132 -132
  263. package/scripts/validar-userland-vacio.js +110 -110
  264. package/scripts/verificar-aislamiento-swl-eval.js +87 -0
  265. package/scripts/verificar-empaquetado-downstream.js +375 -0
  266. package/scripts/verificar-loop-constructor.js +215 -0
  267. package/scripts/verificar-trazabilidad.js +13 -6
  268. package/scripts/verificar-veredicto-real.js +84 -0
@@ -1,216 +1,216 @@
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.1"
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
- - **Un test que simula transacciones concurrentes abriendo un `asyncpg.connect()` NUEVO (no el mock ni la conexión de la fixture) contra una BD con RLS DENY-by-default (`FORCE ROW LEVEL SECURITY`) ve CERO filas silenciosamente si esa conexión no setea `app.tenant_id`** [CONFIRMADO]: el GUC de tenant NUNCA se hereda entre conexiones distintas — ni siquiera si la conexión que sembró los datos ya seteó `app.tenant_id` y sigue abierta en otra variable. Este patrón aparece en tests de integración E2E (no en los mocks de este skill) que abren 2+ `asyncpg.connect()` para simular procesos concurrentes reales (ej. probar un `SELECT ... FOR SHARE`/TOCTOU). El síntoma es engañoso: no hay excepción de permisos, la query simplemente devuelve `None`/lista vacía, y el código que asume una fila (`row["estatus"]`) revienta con `TypeError: 'NoneType' object is not subscriptable` — fácil de confundir con un bug de lógica. Solo se manifiesta contra un rol que SÍ enforcea RLS (ej. el rol de aplicación en CI); un rol superusuario (usado en desarrollo local) bypasea RLS y el test pasa igual, ocultando el bug hasta CI. Fix: en CADA conexión nueva que abra el test (no solo la primera), `await conn.execute("SELECT set_config('app.tenant_id', $1, false)", str(tenant_id))` antes de cualquier query sobre una tabla con RLS.
209
-
210
- ## Referencias
211
-
212
- | Tema | Recurso |
213
- |------|---------|
214
- | Patrón general de async testing en Python | `Skill("testing-python")` |
215
- | Endpoints FastAPI testing con httpx | `Skill("fastapi-experto")` § Testing |
216
- | 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.1"
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
+ - **Un test que simula transacciones concurrentes abriendo un `asyncpg.connect()` NUEVO (no el mock ni la conexión de la fixture) contra una BD con RLS DENY-by-default (`FORCE ROW LEVEL SECURITY`) ve CERO filas silenciosamente si esa conexión no setea `app.tenant_id`** [CONFIRMADO]: el GUC de tenant NUNCA se hereda entre conexiones distintas — ni siquiera si la conexión que sembró los datos ya seteó `app.tenant_id` y sigue abierta en otra variable. Este patrón aparece en tests de integración E2E (no en los mocks de este skill) que abren 2+ `asyncpg.connect()` para simular procesos concurrentes reales (ej. probar un `SELECT ... FOR SHARE`/TOCTOU). El síntoma es engañoso: no hay excepción de permisos, la query simplemente devuelve `None`/lista vacía, y el código que asume una fila (`row["estatus"]`) revienta con `TypeError: 'NoneType' object is not subscriptable` — fácil de confundir con un bug de lógica. Solo se manifiesta contra un rol que SÍ enforcea RLS (ej. el rol de aplicación en CI); un rol superusuario (usado en desarrollo local) bypasea RLS y el test pasa igual, ocultando el bug hasta CI. Fix: en CADA conexión nueva que abra el test (no solo la primera), `await conn.execute("SELECT set_config('app.tenant_id', $1, false)", str(tenant_id))` antes de cualquier query sobre una tabla con RLS.
209
+
210
+ ## Referencias
211
+
212
+ | Tema | Recurso |
213
+ |------|---------|
214
+ | Patrón general de async testing en Python | `Skill("testing-python")` |
215
+ | Endpoints FastAPI testing con httpx | `Skill("fastapi-experto")` § Testing |
216
+ | Race conditions en PostgreSQL (lado código) | `Skill("postgresql-experto")` § SELECT-then-INSERT |