@saulwade/swl-ses 2.2.4 → 2.3.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.
- package/CLAUDE.md +48 -7
- package/README.md +3 -3
- package/agentes/accesibilidad-wcag-swl.md +690 -690
- package/agentes/arquitecto-swl.md +267 -267
- package/agentes/auto-evolucion-swl.md +908 -908
- package/agentes/backend-api-swl.md +472 -472
- package/agentes/backend-csharp-swl.md +420 -420
- package/agentes/backend-go-swl.md +390 -390
- package/agentes/backend-java-swl.md +281 -281
- package/agentes/backend-node-swl.md +479 -479
- package/agentes/backend-python-swl.md +605 -605
- package/agentes/backend-rust-swl.md +364 -364
- package/agentes/backend-workers-swl.md +482 -482
- package/agentes/cloud-infra-swl.md +509 -509
- package/agentes/consolidador-swl.md +541 -541
- package/agentes/datos-swl.md +605 -605
- package/agentes/depurador-swl.md +352 -352
- package/agentes/devops-ci-swl.md +400 -400
- package/agentes/disenador-ui-swl.md +569 -569
- package/agentes/documentador-swl.md +345 -345
- package/agentes/frontend-angular-swl.md +621 -621
- package/agentes/frontend-css-swl.md +716 -716
- package/agentes/frontend-react-swl.md +692 -692
- package/agentes/frontend-swl.md +496 -496
- package/agentes/frontend-tailwind-swl.md +826 -826
- package/agentes/gh-fix-ci-swl.md +2 -2
- package/agentes/implementador-swl.md +328 -328
- package/agentes/investigador-swl.md +432 -432
- package/agentes/investigador-ux-swl.md +505 -505
- package/agentes/llm-apps-swl.md +278 -278
- package/agentes/migrador-swl.md +442 -442
- package/agentes/mobile-android-swl.md +511 -511
- package/agentes/mobile-cross-swl.md +541 -541
- package/agentes/mobile-ios-swl.md +502 -502
- package/agentes/mobile-testing-swl.md +302 -302
- package/agentes/nemesis-auditor-swl.md +285 -285
- package/agentes/notificador-swl.md +918 -918
- package/agentes/observabilidad-swl.md +438 -438
- package/agentes/orquestador-swl.md +1026 -1026
- package/agentes/pagos-swl.md +310 -310
- package/agentes/perfilador-usuario-swl.md +321 -321
- package/agentes/planificador-swl.md +399 -399
- package/agentes/producto-prd-swl.md +589 -589
- package/agentes/red-team-swl.md +1 -1
- package/agentes/release-manager-swl.md +590 -590
- package/agentes/rendimiento-swl.md +713 -713
- package/agentes/resolutor-build-swl.md +245 -245
- package/agentes/revisor-angular-swl.md +278 -278
- package/agentes/revisor-codigo-swl.md +505 -505
- package/agentes/revisor-csharp-swl.md +264 -264
- package/agentes/revisor-go-swl.md +259 -259
- package/agentes/revisor-java-swl.md +257 -257
- package/agentes/revisor-kotlin-swl.md +273 -273
- package/agentes/revisor-nextjs-swl.md +281 -281
- package/agentes/revisor-php-swl.md +271 -271
- package/agentes/revisor-react-swl.md +278 -278
- package/agentes/revisor-rust-swl.md +346 -346
- package/agentes/revisor-seguridad-swl.md +399 -399
- package/agentes/revisor-swift-swl.md +268 -268
- package/agentes/revisor-typescript-swl.md +346 -346
- package/agentes/sre-swl.md +291 -291
- package/agentes/tdd-qa-swl.md +393 -393
- package/comandos/swl/aprobar-plan.md +146 -141
- package/comandos/swl/briefing.md +119 -119
- package/comandos/swl/contribuir.md +233 -233
- package/comandos/swl/predecir.md +139 -139
- package/comandos/swl/release.md +30 -8
- package/comandos/swl/sesiones.md +1 -1
- package/comandos/swl/status.md +343 -333
- package/gateway/agent-executor.js +1 -1
- package/gateway/lib/event-channel.js +191 -191
- package/habilidades/agent-deep-links/SKILL.md +148 -148
- package/habilidades/agentes-como-servicio/SKILL.md +2 -2
- package/habilidades/angular-moderno/SKILL.md +20 -1
- package/habilidades/auto-evolucion-protocolo/SKILL.md +276 -276
- package/habilidades/backend-async-postgres-testing/SKILL.md +215 -215
- package/habilidades/backend-error-design/SKILL.md +221 -221
- package/habilidades/backend-production-resilience/SKILL.md +288 -288
- package/habilidades/benchmark-memoria/SKILL.md +186 -186
- package/habilidades/calidad-anti-patrones-universales/SKILL.md +368 -368
- package/habilidades/calidad-contract-testing/SKILL.md +165 -165
- package/habilidades/calidad-mutation-testing/SKILL.md +170 -170
- package/habilidades/changelog-generator/scripts/parse-commits.js +367 -367
- package/habilidades/checklist-seguridad/recursos/stride-cobertura.md +60 -60
- package/habilidades/contenedores-docker/SKILL.md +3 -1
- package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
- package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
- package/habilidades/drift-detection/SKILL.md +179 -179
- package/habilidades/ejecutar-fase/SKILL.md +564 -541
- package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
- package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +1 -1
- package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -57
- package/habilidades/eval-framework/SKILL.md +212 -212
- package/habilidades/extractor-de-aprendizajes/SKILL.md +3 -3
- package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
- package/habilidades/harness-claude-code/recursos/disciplina-harness-regla.md +252 -252
- package/habilidades/legacy-code-rescue/SKILL.md +267 -267
- package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
- package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
- package/habilidades/perfil-usuario/SKILL.md +200 -200
- package/habilidades/planear-fase/SKILL.md +15 -1
- package/habilidades/prevencion-sobreingenieria/recursos/EXAMPLES.md +580 -580
- package/habilidades/proceso-ddia-fundamentos/SKILL.md +255 -255
- package/habilidades/proceso-ddia-streaming/SKILL.md +231 -231
- package/habilidades/proceso-debate-adversarial/SKILL.md +191 -164
- package/habilidades/proceso-debate-adversarial/recursos/personas.md +105 -105
- package/habilidades/proceso-discovery-machote/SKILL.md +157 -157
- package/habilidades/proceso-dynamic-workflows/SKILL.md +138 -138
- package/habilidades/proceso-dynamic-workflows/recursos/template-adversarial-verify.js +65 -65
- package/habilidades/proceso-dynamic-workflows/recursos/template-triage.js +65 -65
- package/habilidades/proceso-intent-engineering/SKILL.md +269 -269
- package/habilidades/proceso-modular-split/SKILL.md +256 -256
- package/habilidades/prompt-engineering/recursos/patrones-avanzados.md +2 -2
- package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
- package/habilidades/structured-outputs/SKILL.md +2 -2
- package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -83
- package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -85
- package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -94
- package/habilidades/swl-dashboard/SKILL.md +2 -2
- package/habilidades/tdd-workflow/SKILL.md +33 -1
- package/habilidades/tdd-workflow/recursos/gherkin-bdd.md +111 -111
- package/habilidades/validacion-ci-sistema/recursos/validadores-completos.md +1 -1
- package/habilidades/validacion-ci-sistema/scripts/validar-sistema.sh +1 -1
- package/hooks/captura-acciones-post.js +151 -0
- package/hooks/captura-acciones-session.js +155 -0
- package/hooks/ciclo-evolucion-subagente.js +26 -26
- package/hooks/ciclo-evolucion.js +26 -26
- package/hooks/claudemd-bloat-detector.js +161 -161
- package/hooks/claudemd-duplicacion-detector.js +170 -170
- package/hooks/contexto-iteracion.js +144 -144
- package/hooks/guardrail-modelo.js +1 -1
- package/hooks/lib/agent-routing.js +107 -107
- package/hooks/lib/auto-consolidator.js +335 -335
- package/hooks/lib/autonomia.js +208 -208
- package/hooks/lib/briefing.js +512 -474
- package/hooks/lib/captura-acciones.js +392 -0
- package/hooks/lib/ciclo-evolucion.js +47 -47
- package/hooks/lib/deep-links.js +185 -185
- package/hooks/lib/error-classifier.js +308 -308
- package/hooks/lib/etapa-auto-evolucion.js +701 -701
- package/hooks/lib/etapa-metricas.js +388 -388
- package/hooks/lib/etapa-perfil-usuario.js +376 -376
- package/hooks/lib/loop-telemetry.js +321 -321
- package/hooks/lib/merkle-audit.js +96 -96
- package/hooks/lib/model-router.js +2 -2
- package/hooks/lib/propose-step.js +358 -358
- package/hooks/lib/provenance-tracker.js +191 -191
- package/hooks/lib/rate-limit-tracker.js +253 -253
- package/hooks/lib/resource-quota.js +122 -122
- package/hooks/lib/retry-jitter.js +165 -165
- package/hooks/lib/security-net.js +201 -201
- package/hooks/lib/skill-auditor.js +588 -588
- package/hooks/lib/sync-status.js +228 -228
- package/hooks/lib/taint-tracker.js +107 -107
- package/hooks/lib/text-similarity.js +241 -241
- package/hooks/lib/token-estimator.js +1 -0
- package/hooks/lib/toon-compressor.js +245 -245
- package/hooks/lib/usage-model.js +1 -1
- package/hooks/linea-estado.js +2 -0
- package/hooks/registro-turnos.js +209 -209
- package/hooks/session-briefing.js +98 -98
- package/hooks/spec-gate.js +211 -211
- package/hooks/sugerir-regenerar-inventario.js +170 -170
- package/hooks/tdd-gate.js +241 -241
- package/hooks/telemetria-skill-routing.js +100 -100
- package/hooks/validar-formato-post-subagente.js +140 -140
- package/hooks/validar-intent-spec.js +242 -242
- package/hooks/validar-memoria-hook.js +218 -218
- package/hooks/validar-planning-paths.js +134 -134
- package/instintos/autonomia.yaml +27 -27
- package/instintos/prompt-appendices.yaml +57 -57
- package/llms.txt +2 -2
- package/manifiestos/agent-output-schemas.json +57 -57
- package/manifiestos/canonical-hashes.json +2291 -1637
- package/manifiestos/hooks-config.json +18 -0
- package/manifiestos/modulos.json +4 -0
- package/manifiestos/planning-paths.json +44 -44
- package/manifiestos/skills-lock.json +1282 -1282
- package/package.json +2 -2
- package/plantillas/auditor-veto-template.md +105 -105
- package/plantillas/github-workflows/README.md +47 -47
- package/plantillas/github-workflows/release-please.yml +44 -44
- package/plantillas/github-workflows/swl-ci.yml +107 -107
- package/plantillas/github-workflows/swl-security.yml +51 -51
- package/plugin.json +2 -2
- package/reglas/accesibilidad.md +10 -10
- package/reglas/analisis-previo-tareas-grandes.md +172 -172
- package/reglas/api-diseno.md +9 -9
- package/reglas/arreglar-al-detectar.md +240 -240
- package/reglas/auditorias-documentales-estructurales.md +7 -7
- package/reglas/cloud-infra.md +8 -8
- package/reglas/consultar-vault-primero.md +195 -195
- package/reglas/debatir-antes-de-aceptar.md +158 -158
- package/reglas/fragmentos-compartidos.md +183 -183
- package/reglas/hooks.md +6 -6
- package/reglas/intent-engineering.md +218 -218
- package/reglas/markitdown.md +8 -8
- package/reglas/memoria-consolidada.md +41 -0
- package/reglas/monitor-ci.md +309 -309
- package/reglas/patrones.md +6 -6
- package/reglas/seguridad-agentes.md +66 -0
- package/reglas/sesiones-paralelas.md +180 -180
- package/reglas/sin-duplicacion-reglas-globales.md +182 -182
- package/reglas/skills-estandar.md +6 -6
- package/reglas/testing.md +7 -7
- package/reglas/tests-cleanup.md +224 -224
- package/reglas/usar-code-review-graph.md +155 -155
- package/reglas/usar-context7.md +226 -226
- package/reglas/verificar-citas-normativas.md +548 -548
- package/schemas/agent-frontmatter.schema.json +3 -3
- package/schemas/agent-message.schema.json +73 -73
- package/schemas/agent-output-implementacion.schema.json +114 -114
- package/schemas/agent-output-planificacion.schema.json +150 -150
- package/schemas/agent-output-review.schema.json +98 -98
- package/schemas/diary-entry.schema.json +112 -112
- package/schemas/hook-profiles.schema.json +54 -54
- package/schemas/hooks-config.schema.json +89 -89
- package/schemas/instinct.schema.json +161 -152
- package/schemas/modulos.schema.json +38 -38
- package/schemas/perfiles.schema.json +36 -36
- package/schemas/plugin.schema.json +77 -77
- package/schemas/skill-evals.schema.json +119 -119
- package/schemas/skill-frontmatter.schema.json +245 -245
- package/scripts/audit-tools/audit-history.js +330 -330
- package/scripts/audit-tools/bundle-tracker.js +290 -290
- package/scripts/audit-tools/canary-monitor.js +352 -352
- package/scripts/audit-tools/code-profiler.js +605 -605
- package/scripts/audit-tools/dep-doctor.js +320 -320
- package/scripts/audit-tools/env-validator.js +206 -206
- package/scripts/audit-tools/lib/fs-walk.js +48 -48
- package/scripts/audit-tools/lib/output.js +23 -23
- package/scripts/audit-tools/migration-checker.js +392 -392
- package/scripts/audit-tools/pentest-scanner.js +1436 -1436
- package/scripts/benchmark-memoria.js +167 -167
- package/scripts/bootstrap-instintos.js +25 -7
- package/scripts/cli/aprobar-plan.js +73 -73
- package/scripts/cli/briefing.js +23 -23
- package/scripts/cli/ciclo-evolucion.js +26 -26
- package/scripts/cli/configurar-ci.js +40 -40
- package/scripts/cli/derivar-feature-list.js +25 -25
- package/scripts/cli/detectar-host.js +27 -27
- package/scripts/cli/diary-entry.js +69 -69
- package/scripts/cli/execution-state.js +18 -18
- package/scripts/cli/gateway-notify.js +41 -41
- package/scripts/cli/liberar-fase.js +42 -42
- package/scripts/cli/loop-telemetry.js +125 -125
- package/scripts/cli/mark-evolved.js +56 -56
- package/scripts/cli/metricas-dora.js +26 -26
- package/scripts/cli/near-duplicate.js +55 -55
- package/scripts/cli/notificaciones.js +123 -123
- package/scripts/cli/propose-step.js +29 -29
- package/scripts/cli/schedule-parse.js +19 -19
- package/scripts/cli/sugerir-modelo.js +20 -20
- package/scripts/cli/verificar-plan.js +36 -36
- package/scripts/cli/verificar-trazabilidad.js +35 -35
- package/scripts/configurar-branch-protection.js +418 -418
- package/scripts/detectar-aprendizajes-duplicados.js +151 -151
- package/scripts/field-report.js +199 -199
- package/scripts/generar-checklists-consolidados.js +273 -273
- package/scripts/generar-matriz-lenguajes.js +271 -271
- package/scripts/lib/artefactos-python.js +43 -43
- package/scripts/lib/auditar-invocaciones-comandos.js +104 -104
- package/scripts/lib/benchmark-metrics.js +160 -160
- package/scripts/lib/budget-enforcer.js +252 -252
- package/scripts/lib/ci-reader.js +193 -193
- package/scripts/lib/claude-sessions.js +1 -0
- package/scripts/lib/configurar-ci.js +380 -380
- package/scripts/lib/contadores-inventario.js +217 -217
- package/scripts/lib/detectar-host-swl.js +175 -175
- package/scripts/lib/detectar-stack-detallado.js +307 -307
- package/scripts/lib/detector-autoduplicacion-intra-archivo.js +234 -234
- package/scripts/lib/detector-reglas-duplicadas.js +220 -220
- package/scripts/lib/diary-entry.js +234 -234
- package/scripts/lib/eval-metrics-store.js +218 -218
- package/scripts/lib/eval-quality.js +171 -171
- package/scripts/lib/eval-schemas.js +144 -144
- package/scripts/lib/eval-self-correct.js +106 -106
- package/scripts/lib/eval-validator.js +185 -185
- package/scripts/lib/evidencia-release.js +322 -322
- package/scripts/lib/expandir-targets.js +71 -71
- package/scripts/lib/gate-hooks-requires.js +249 -249
- package/scripts/lib/gate-licencias.js +212 -212
- package/scripts/lib/git-metricas.js +257 -257
- package/scripts/lib/gitignore-manifest.js +44 -11
- package/scripts/lib/jaccard-similarity.js +98 -98
- package/scripts/lib/longmemeval-runner.js +125 -125
- package/scripts/lib/metricas-dora.js +204 -204
- package/scripts/lib/npm-version.js +261 -261
- package/scripts/lib/paquetes-conocidos.js +50 -50
- package/scripts/lib/plan-lock.js +275 -275
- package/scripts/lib/pr-analyzer.js +399 -399
- package/scripts/lib/prompt-builder.js +264 -264
- package/scripts/lib/reglas-globales-conocidas.json +180 -180
- package/scripts/lib/resolver-plan-fase.js +37 -37
- package/scripts/lib/rrf-fusion.js +175 -175
- package/scripts/lib/schema-version.js +164 -164
- package/scripts/lib/scoring-instintos.js +277 -277
- package/scripts/lib/semantic-search.js +252 -252
- package/scripts/lib/skill-metrics.js +41 -1
- package/scripts/lib/toml-merge.js +204 -204
- package/scripts/lib/tool-cost-analyzer.js +1 -0
- package/scripts/limpiar-artefactos-python.js +131 -131
- package/scripts/mcp-server/auth.js +105 -105
- package/scripts/mcp-server/cache.js +106 -106
- package/scripts/migrar-csv-a-array.js +168 -168
- package/scripts/migrar-fase-dominio.js +200 -200
- package/scripts/publicar.js +497 -497
- package/scripts/run-eval.js +141 -141
- package/scripts/tui/componentes/selector-multi.js +189 -189
- package/scripts/tui/componentes/selector-unico.js +158 -158
- package/scripts/tui/ejecutores.js +375 -375
- package/scripts/tui/index.js +162 -162
- package/scripts/tui/lib/colores.js +129 -129
- package/scripts/tui/lib/render.js +264 -264
- package/scripts/tui/lib/teclas.js +113 -113
- package/scripts/tui/pantallas/inspect.js +173 -173
- package/scripts/tui/pantallas/install-wizard.js +334 -334
- package/scripts/tui/pantallas/menu-principal.js +52 -52
- package/scripts/tui/pantallas/progreso.js +274 -274
- package/scripts/tui/pantallas/resumen.js +132 -132
- package/scripts/tui/pantallas/uninstall-wizard.js +208 -208
- package/scripts/tui/pantallas/update-wizard.js +232 -232
- package/scripts/tui/pantallas/welcome.js +187 -187
- package/scripts/validar-userland-vacio.js +110 -110
- package/scripts/verificar-trazabilidad.js +329 -298
|
@@ -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 |
|