javi-forge 0.1.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 (500) hide show
  1. package/.gitignore.template +105 -0
  2. package/.releaserc +44 -0
  3. package/README.md +45 -0
  4. package/ai-config/.skillignore +15 -0
  5. package/ai-config/AUTO_INVOKE.md +300 -0
  6. package/ai-config/agents/_TEMPLATE.md +93 -0
  7. package/ai-config/agents/business/api-designer.md +1657 -0
  8. package/ai-config/agents/business/business-analyst.md +1331 -0
  9. package/ai-config/agents/business/product-strategist.md +206 -0
  10. package/ai-config/agents/business/project-manager.md +178 -0
  11. package/ai-config/agents/business/requirements-analyst.md +1277 -0
  12. package/ai-config/agents/business/technical-writer.md +1679 -0
  13. package/ai-config/agents/creative/ux-designer.md +205 -0
  14. package/ai-config/agents/data-ai/ai-engineer.md +487 -0
  15. package/ai-config/agents/data-ai/analytics-engineer.md +953 -0
  16. package/ai-config/agents/data-ai/data-engineer.md +173 -0
  17. package/ai-config/agents/data-ai/data-scientist.md +672 -0
  18. package/ai-config/agents/data-ai/mlops-engineer.md +814 -0
  19. package/ai-config/agents/data-ai/prompt-engineer.md +772 -0
  20. package/ai-config/agents/development/angular-expert.md +620 -0
  21. package/ai-config/agents/development/backend-architect.md +795 -0
  22. package/ai-config/agents/development/database-specialist.md +212 -0
  23. package/ai-config/agents/development/frontend-specialist.md +686 -0
  24. package/ai-config/agents/development/fullstack-engineer.md +668 -0
  25. package/ai-config/agents/development/golang-pro.md +338 -0
  26. package/ai-config/agents/development/java-enterprise.md +400 -0
  27. package/ai-config/agents/development/javascript-pro.md +422 -0
  28. package/ai-config/agents/development/nextjs-pro.md +474 -0
  29. package/ai-config/agents/development/python-pro.md +570 -0
  30. package/ai-config/agents/development/react-pro.md +487 -0
  31. package/ai-config/agents/development/rust-pro.md +246 -0
  32. package/ai-config/agents/development/spring-boot-4-expert.md +326 -0
  33. package/ai-config/agents/development/typescript-pro.md +336 -0
  34. package/ai-config/agents/development/vue-specialist.md +605 -0
  35. package/ai-config/agents/infrastructure/cloud-architect.md +472 -0
  36. package/ai-config/agents/infrastructure/deployment-manager.md +358 -0
  37. package/ai-config/agents/infrastructure/devops-engineer.md +455 -0
  38. package/ai-config/agents/infrastructure/incident-responder.md +519 -0
  39. package/ai-config/agents/infrastructure/kubernetes-expert.md +705 -0
  40. package/ai-config/agents/infrastructure/monitoring-specialist.md +674 -0
  41. package/ai-config/agents/infrastructure/performance-engineer.md +658 -0
  42. package/ai-config/agents/orchestrator.md +241 -0
  43. package/ai-config/agents/quality/accessibility-auditor.md +1204 -0
  44. package/ai-config/agents/quality/code-reviewer-compact.md +123 -0
  45. package/ai-config/agents/quality/code-reviewer.md +363 -0
  46. package/ai-config/agents/quality/dependency-manager.md +743 -0
  47. package/ai-config/agents/quality/e2e-test-specialist.md +1005 -0
  48. package/ai-config/agents/quality/performance-tester.md +1086 -0
  49. package/ai-config/agents/quality/security-auditor.md +133 -0
  50. package/ai-config/agents/quality/test-engineer.md +453 -0
  51. package/ai-config/agents/specialists/api-designer.md +87 -0
  52. package/ai-config/agents/specialists/backend-architect.md +73 -0
  53. package/ai-config/agents/specialists/code-reviewer.md +77 -0
  54. package/ai-config/agents/specialists/db-optimizer.md +75 -0
  55. package/ai-config/agents/specialists/devops-engineer.md +83 -0
  56. package/ai-config/agents/specialists/documentation-writer.md +78 -0
  57. package/ai-config/agents/specialists/frontend-developer.md +75 -0
  58. package/ai-config/agents/specialists/performance-analyst.md +82 -0
  59. package/ai-config/agents/specialists/refactor-specialist.md +74 -0
  60. package/ai-config/agents/specialists/security-auditor.md +74 -0
  61. package/ai-config/agents/specialists/test-engineer.md +81 -0
  62. package/ai-config/agents/specialists/ux-consultant.md +76 -0
  63. package/ai-config/agents/specialized/agent-generator.md +1190 -0
  64. package/ai-config/agents/specialized/blockchain-developer.md +149 -0
  65. package/ai-config/agents/specialized/code-migrator.md +892 -0
  66. package/ai-config/agents/specialized/context-manager.md +978 -0
  67. package/ai-config/agents/specialized/documentation-writer.md +1078 -0
  68. package/ai-config/agents/specialized/ecommerce-expert.md +1756 -0
  69. package/ai-config/agents/specialized/embedded-engineer.md +1714 -0
  70. package/ai-config/agents/specialized/error-detective.md +1034 -0
  71. package/ai-config/agents/specialized/fintech-specialist.md +1659 -0
  72. package/ai-config/agents/specialized/freelance-project-planner-v2.md +1988 -0
  73. package/ai-config/agents/specialized/freelance-project-planner-v3.md +2136 -0
  74. package/ai-config/agents/specialized/freelance-project-planner-v4.md +4503 -0
  75. package/ai-config/agents/specialized/freelance-project-planner.md +722 -0
  76. package/ai-config/agents/specialized/game-developer.md +1963 -0
  77. package/ai-config/agents/specialized/healthcare-dev.md +1620 -0
  78. package/ai-config/agents/specialized/mobile-developer.md +188 -0
  79. package/ai-config/agents/specialized/parallel-plan-executor.md +506 -0
  80. package/ai-config/agents/specialized/plan-executor.md +485 -0
  81. package/ai-config/agents/specialized/solo-dev-planner-modular/00-INDEX.md +485 -0
  82. package/ai-config/agents/specialized/solo-dev-planner-modular/01-CORE.md +3493 -0
  83. package/ai-config/agents/specialized/solo-dev-planner-modular/02-SELF-CORRECTION.md +778 -0
  84. package/ai-config/agents/specialized/solo-dev-planner-modular/03-PROGRESSIVE-SETUP.md +918 -0
  85. package/ai-config/agents/specialized/solo-dev-planner-modular/04-DEPLOYMENT.md +1537 -0
  86. package/ai-config/agents/specialized/solo-dev-planner-modular/05-TESTING.md +2633 -0
  87. package/ai-config/agents/specialized/solo-dev-planner-modular/06-OPERATIONS.md +5610 -0
  88. package/ai-config/agents/specialized/solo-dev-planner-modular/INSTALL.md +335 -0
  89. package/ai-config/agents/specialized/solo-dev-planner-modular/QUICK-REFERENCE.txt +215 -0
  90. package/ai-config/agents/specialized/solo-dev-planner-modular/README.md +260 -0
  91. package/ai-config/agents/specialized/solo-dev-planner-modular/START-HERE.md +379 -0
  92. package/ai-config/agents/specialized/solo-dev-planner-modular/WORKFLOW-DIAGRAM.md +355 -0
  93. package/ai-config/agents/specialized/solo-dev-planner-modular/solo-dev-planner.md +279 -0
  94. package/ai-config/agents/specialized/template-writer.md +347 -0
  95. package/ai-config/agents/specialized/test-runner.md +99 -0
  96. package/ai-config/agents/specialized/vibekanban-smart-worker.md +244 -0
  97. package/ai-config/agents/specialized/wave-executor.md +138 -0
  98. package/ai-config/agents/specialized/workflow-optimizer.md +1114 -0
  99. package/ai-config/commands/git/changelog.md +32 -0
  100. package/ai-config/commands/git/ci-local.md +70 -0
  101. package/ai-config/commands/git/commit.md +35 -0
  102. package/ai-config/commands/git/fix-issue.md +23 -0
  103. package/ai-config/commands/git/pr-create.md +42 -0
  104. package/ai-config/commands/git/pr-review.md +50 -0
  105. package/ai-config/commands/git/worktree.md +39 -0
  106. package/ai-config/commands/refactoring/cleanup.md +24 -0
  107. package/ai-config/commands/refactoring/dead-code.md +40 -0
  108. package/ai-config/commands/refactoring/extract.md +31 -0
  109. package/ai-config/commands/testing/e2e.md +30 -0
  110. package/ai-config/commands/testing/tdd.md +36 -0
  111. package/ai-config/commands/testing/test-coverage.md +30 -0
  112. package/ai-config/commands/testing/test-fix.md +24 -0
  113. package/ai-config/commands/workflow/generate-agents-md.md +85 -0
  114. package/ai-config/commands/workflow/planning.md +47 -0
  115. package/ai-config/commands/workflows/compound.md +89 -0
  116. package/ai-config/commands/workflows/plan.md +77 -0
  117. package/ai-config/commands/workflows/review.md +78 -0
  118. package/ai-config/commands/workflows/work.md +75 -0
  119. package/ai-config/config.yaml +18 -0
  120. package/ai-config/hooks/_TEMPLATE.md +96 -0
  121. package/ai-config/hooks/block-dangerous-commands.md +75 -0
  122. package/ai-config/hooks/commit-guard.md +90 -0
  123. package/ai-config/hooks/context-loader.md +73 -0
  124. package/ai-config/hooks/improve-prompt.md +91 -0
  125. package/ai-config/hooks/learning-log.md +72 -0
  126. package/ai-config/hooks/model-router.md +86 -0
  127. package/ai-config/hooks/secret-scanner.md +64 -0
  128. package/ai-config/hooks/skill-validator.md +102 -0
  129. package/ai-config/hooks/task-artifact.md +114 -0
  130. package/ai-config/hooks/validate-workflow.md +100 -0
  131. package/ai-config/prompts/base.md +71 -0
  132. package/ai-config/prompts/modes/debug.md +34 -0
  133. package/ai-config/prompts/modes/deploy.md +40 -0
  134. package/ai-config/prompts/modes/research.md +32 -0
  135. package/ai-config/prompts/modes/review.md +33 -0
  136. package/ai-config/prompts/review-policy.md +79 -0
  137. package/ai-config/skills/_TEMPLATE.md +157 -0
  138. package/ai-config/skills/backend/api-gateway/SKILL.md +254 -0
  139. package/ai-config/skills/backend/bff-concepts/SKILL.md +239 -0
  140. package/ai-config/skills/backend/bff-spring/SKILL.md +364 -0
  141. package/ai-config/skills/backend/chi-router/SKILL.md +396 -0
  142. package/ai-config/skills/backend/error-handling/SKILL.md +255 -0
  143. package/ai-config/skills/backend/exceptions-spring/SKILL.md +323 -0
  144. package/ai-config/skills/backend/fastapi/SKILL.md +302 -0
  145. package/ai-config/skills/backend/gateway-spring/SKILL.md +390 -0
  146. package/ai-config/skills/backend/go-backend/SKILL.md +457 -0
  147. package/ai-config/skills/backend/gradle-multimodule/SKILL.md +274 -0
  148. package/ai-config/skills/backend/graphql-concepts/SKILL.md +352 -0
  149. package/ai-config/skills/backend/graphql-spring/SKILL.md +398 -0
  150. package/ai-config/skills/backend/grpc-concepts/SKILL.md +283 -0
  151. package/ai-config/skills/backend/grpc-spring/SKILL.md +445 -0
  152. package/ai-config/skills/backend/jwt-auth/SKILL.md +412 -0
  153. package/ai-config/skills/backend/notifications-concepts/SKILL.md +259 -0
  154. package/ai-config/skills/backend/recommendations-concepts/SKILL.md +261 -0
  155. package/ai-config/skills/backend/search-concepts/SKILL.md +263 -0
  156. package/ai-config/skills/backend/search-spring/SKILL.md +375 -0
  157. package/ai-config/skills/backend/spring-boot-4/SKILL.md +172 -0
  158. package/ai-config/skills/backend/websockets/SKILL.md +532 -0
  159. package/ai-config/skills/data-ai/ai-ml/SKILL.md +423 -0
  160. package/ai-config/skills/data-ai/analytics-concepts/SKILL.md +195 -0
  161. package/ai-config/skills/data-ai/analytics-spring/SKILL.md +340 -0
  162. package/ai-config/skills/data-ai/duckdb-analytics/SKILL.md +440 -0
  163. package/ai-config/skills/data-ai/langchain/SKILL.md +238 -0
  164. package/ai-config/skills/data-ai/mlflow/SKILL.md +302 -0
  165. package/ai-config/skills/data-ai/onnx-inference/SKILL.md +290 -0
  166. package/ai-config/skills/data-ai/powerbi/SKILL.md +352 -0
  167. package/ai-config/skills/data-ai/pytorch/SKILL.md +274 -0
  168. package/ai-config/skills/data-ai/scikit-learn/SKILL.md +321 -0
  169. package/ai-config/skills/data-ai/vector-db/SKILL.md +301 -0
  170. package/ai-config/skills/database/graph-databases/SKILL.md +218 -0
  171. package/ai-config/skills/database/graph-spring/SKILL.md +361 -0
  172. package/ai-config/skills/database/pgx-postgres/SKILL.md +512 -0
  173. package/ai-config/skills/database/redis-cache/SKILL.md +343 -0
  174. package/ai-config/skills/database/sqlite-embedded/SKILL.md +388 -0
  175. package/ai-config/skills/database/timescaledb/SKILL.md +320 -0
  176. package/ai-config/skills/docs/api-documentation/SKILL.md +293 -0
  177. package/ai-config/skills/docs/docs-spring/SKILL.md +377 -0
  178. package/ai-config/skills/docs/mustache-templates/SKILL.md +190 -0
  179. package/ai-config/skills/docs/technical-docs/SKILL.md +447 -0
  180. package/ai-config/skills/frontend/astro-ssr/SKILL.md +441 -0
  181. package/ai-config/skills/frontend/frontend-design/SKILL.md +54 -0
  182. package/ai-config/skills/frontend/frontend-web/SKILL.md +368 -0
  183. package/ai-config/skills/frontend/mantine-ui/SKILL.md +396 -0
  184. package/ai-config/skills/frontend/tanstack-query/SKILL.md +439 -0
  185. package/ai-config/skills/frontend/zod-validation/SKILL.md +417 -0
  186. package/ai-config/skills/frontend/zustand-state/SKILL.md +350 -0
  187. package/ai-config/skills/infrastructure/chaos-engineering/SKILL.md +244 -0
  188. package/ai-config/skills/infrastructure/chaos-spring/SKILL.md +378 -0
  189. package/ai-config/skills/infrastructure/devops-infra/SKILL.md +435 -0
  190. package/ai-config/skills/infrastructure/docker-containers/SKILL.md +420 -0
  191. package/ai-config/skills/infrastructure/kubernetes/SKILL.md +456 -0
  192. package/ai-config/skills/infrastructure/opentelemetry/SKILL.md +546 -0
  193. package/ai-config/skills/infrastructure/traefik-proxy/SKILL.md +474 -0
  194. package/ai-config/skills/infrastructure/woodpecker-ci/SKILL.md +315 -0
  195. package/ai-config/skills/mobile/ionic-capacitor/SKILL.md +504 -0
  196. package/ai-config/skills/mobile/mobile-ionic/SKILL.md +448 -0
  197. package/ai-config/skills/prompt-improver/SKILL.md +125 -0
  198. package/ai-config/skills/quality/ghagga-review/SKILL.md +216 -0
  199. package/ai-config/skills/references/hooks-patterns/SKILL.md +238 -0
  200. package/ai-config/skills/references/mcp-servers/SKILL.md +275 -0
  201. package/ai-config/skills/references/plugins-reference/SKILL.md +110 -0
  202. package/ai-config/skills/references/skills-reference/SKILL.md +420 -0
  203. package/ai-config/skills/references/subagent-templates/SKILL.md +193 -0
  204. package/ai-config/skills/systems-iot/modbus-protocol/SKILL.md +410 -0
  205. package/ai-config/skills/systems-iot/mqtt-rumqttc/SKILL.md +408 -0
  206. package/ai-config/skills/systems-iot/rust-systems/SKILL.md +386 -0
  207. package/ai-config/skills/systems-iot/tokio-async/SKILL.md +324 -0
  208. package/ai-config/skills/testing/playwright-e2e/SKILL.md +289 -0
  209. package/ai-config/skills/testing/testcontainers/SKILL.md +299 -0
  210. package/ai-config/skills/testing/vitest-testing/SKILL.md +381 -0
  211. package/ai-config/skills/workflow/ci-local-guide/SKILL.md +118 -0
  212. package/ai-config/skills/workflow/claude-automation-recommender/SKILL.md +299 -0
  213. package/ai-config/skills/workflow/claude-md-improver/SKILL.md +158 -0
  214. package/ai-config/skills/workflow/finishing-a-development-branch/SKILL.md +117 -0
  215. package/ai-config/skills/workflow/git-github/SKILL.md +334 -0
  216. package/ai-config/skills/workflow/git-github/references/examples.md +160 -0
  217. package/ai-config/skills/workflow/git-workflow/SKILL.md +214 -0
  218. package/ai-config/skills/workflow/ide-plugins/SKILL.md +277 -0
  219. package/ai-config/skills/workflow/ide-plugins-intellij/SKILL.md +401 -0
  220. package/ai-config/skills/workflow/obsidian-brain-workflow/SKILL.md +199 -0
  221. package/ai-config/skills/workflow/using-git-worktrees/SKILL.md +100 -0
  222. package/ai-config/skills/workflow/verification-before-completion/SKILL.md +73 -0
  223. package/ai-config/skills/workflow/wave-workflow/SKILL.md +178 -0
  224. package/ci-local/README.md +170 -0
  225. package/ci-local/ci-local.sh +297 -0
  226. package/ci-local/hooks/commit-msg +74 -0
  227. package/ci-local/hooks/pre-commit +162 -0
  228. package/ci-local/hooks/pre-push +41 -0
  229. package/ci-local/install.sh +49 -0
  230. package/ci-local/semgrep.yml +214 -0
  231. package/dist/commands/analyze.d.ts +9 -0
  232. package/dist/commands/analyze.d.ts.map +1 -0
  233. package/dist/commands/analyze.js +55 -0
  234. package/dist/commands/analyze.js.map +1 -0
  235. package/dist/commands/analyze.test.d.ts +2 -0
  236. package/dist/commands/analyze.test.d.ts.map +1 -0
  237. package/dist/commands/analyze.test.js +145 -0
  238. package/dist/commands/analyze.test.js.map +1 -0
  239. package/dist/commands/doctor.d.ts +7 -0
  240. package/dist/commands/doctor.d.ts.map +1 -0
  241. package/dist/commands/doctor.js +158 -0
  242. package/dist/commands/doctor.js.map +1 -0
  243. package/dist/commands/doctor.test.d.ts +2 -0
  244. package/dist/commands/doctor.test.d.ts.map +1 -0
  245. package/dist/commands/doctor.test.js +200 -0
  246. package/dist/commands/doctor.test.js.map +1 -0
  247. package/dist/commands/init.d.ts +9 -0
  248. package/dist/commands/init.d.ts.map +1 -0
  249. package/dist/commands/init.js +283 -0
  250. package/dist/commands/init.js.map +1 -0
  251. package/dist/commands/init.test.d.ts +2 -0
  252. package/dist/commands/init.test.d.ts.map +1 -0
  253. package/dist/commands/init.test.js +271 -0
  254. package/dist/commands/init.test.js.map +1 -0
  255. package/dist/commands/sync.d.ts +8 -0
  256. package/dist/commands/sync.d.ts.map +1 -0
  257. package/dist/commands/sync.js +201 -0
  258. package/dist/commands/sync.js.map +1 -0
  259. package/dist/constants.d.ts +21 -0
  260. package/dist/constants.d.ts.map +1 -0
  261. package/dist/constants.js +57 -0
  262. package/dist/constants.js.map +1 -0
  263. package/dist/e2e/aggressive.e2e.test.d.ts +2 -0
  264. package/dist/e2e/aggressive.e2e.test.d.ts.map +1 -0
  265. package/dist/e2e/aggressive.e2e.test.js +350 -0
  266. package/dist/e2e/aggressive.e2e.test.js.map +1 -0
  267. package/dist/e2e/commands.e2e.test.d.ts +2 -0
  268. package/dist/e2e/commands.e2e.test.d.ts.map +1 -0
  269. package/dist/e2e/commands.e2e.test.js +213 -0
  270. package/dist/e2e/commands.e2e.test.js.map +1 -0
  271. package/dist/index.d.ts +3 -0
  272. package/dist/index.d.ts.map +1 -0
  273. package/dist/index.js +82 -0
  274. package/dist/index.js.map +1 -0
  275. package/dist/lib/common.d.ts +17 -0
  276. package/dist/lib/common.d.ts.map +1 -0
  277. package/dist/lib/common.js +111 -0
  278. package/dist/lib/common.js.map +1 -0
  279. package/dist/lib/common.test.d.ts +2 -0
  280. package/dist/lib/common.test.d.ts.map +1 -0
  281. package/dist/lib/common.test.js +316 -0
  282. package/dist/lib/common.test.js.map +1 -0
  283. package/dist/lib/frontmatter.d.ts +18 -0
  284. package/dist/lib/frontmatter.d.ts.map +1 -0
  285. package/dist/lib/frontmatter.js +61 -0
  286. package/dist/lib/frontmatter.js.map +1 -0
  287. package/dist/lib/frontmatter.test.d.ts +2 -0
  288. package/dist/lib/frontmatter.test.d.ts.map +1 -0
  289. package/dist/lib/frontmatter.test.js +257 -0
  290. package/dist/lib/frontmatter.test.js.map +1 -0
  291. package/dist/lib/template.d.ts +24 -0
  292. package/dist/lib/template.d.ts.map +1 -0
  293. package/dist/lib/template.js +78 -0
  294. package/dist/lib/template.js.map +1 -0
  295. package/dist/lib/template.test.d.ts +2 -0
  296. package/dist/lib/template.test.d.ts.map +1 -0
  297. package/dist/lib/template.test.js +201 -0
  298. package/dist/lib/template.test.js.map +1 -0
  299. package/dist/types/index.d.ts +48 -0
  300. package/dist/types/index.d.ts.map +1 -0
  301. package/dist/types/index.js +2 -0
  302. package/dist/types/index.js.map +1 -0
  303. package/dist/ui/AnalyzeUI.d.ts +7 -0
  304. package/dist/ui/AnalyzeUI.d.ts.map +1 -0
  305. package/dist/ui/AnalyzeUI.js +100 -0
  306. package/dist/ui/AnalyzeUI.js.map +1 -0
  307. package/dist/ui/App.d.ts +13 -0
  308. package/dist/ui/App.d.ts.map +1 -0
  309. package/dist/ui/App.js +100 -0
  310. package/dist/ui/App.js.map +1 -0
  311. package/dist/ui/CIContext.d.ts +9 -0
  312. package/dist/ui/CIContext.d.ts.map +1 -0
  313. package/dist/ui/CIContext.js +9 -0
  314. package/dist/ui/CIContext.js.map +1 -0
  315. package/dist/ui/CISelector.d.ts +8 -0
  316. package/dist/ui/CISelector.d.ts.map +1 -0
  317. package/dist/ui/CISelector.js +45 -0
  318. package/dist/ui/CISelector.js.map +1 -0
  319. package/dist/ui/Doctor.d.ts +3 -0
  320. package/dist/ui/Doctor.d.ts.map +1 -0
  321. package/dist/ui/Doctor.js +89 -0
  322. package/dist/ui/Doctor.js.map +1 -0
  323. package/dist/ui/Header.d.ts +8 -0
  324. package/dist/ui/Header.d.ts.map +1 -0
  325. package/dist/ui/Header.js +30 -0
  326. package/dist/ui/Header.js.map +1 -0
  327. package/dist/ui/MemorySelector.d.ts +8 -0
  328. package/dist/ui/MemorySelector.d.ts.map +1 -0
  329. package/dist/ui/MemorySelector.js +46 -0
  330. package/dist/ui/MemorySelector.js.map +1 -0
  331. package/dist/ui/NameInput.d.ts +8 -0
  332. package/dist/ui/NameInput.d.ts.map +1 -0
  333. package/dist/ui/NameInput.js +69 -0
  334. package/dist/ui/NameInput.js.map +1 -0
  335. package/dist/ui/OptionSelector.d.ts +12 -0
  336. package/dist/ui/OptionSelector.d.ts.map +1 -0
  337. package/dist/ui/OptionSelector.js +69 -0
  338. package/dist/ui/OptionSelector.js.map +1 -0
  339. package/dist/ui/Progress.d.ts +11 -0
  340. package/dist/ui/Progress.d.ts.map +1 -0
  341. package/dist/ui/Progress.js +58 -0
  342. package/dist/ui/Progress.js.map +1 -0
  343. package/dist/ui/StackSelector.d.ts +9 -0
  344. package/dist/ui/StackSelector.d.ts.map +1 -0
  345. package/dist/ui/StackSelector.js +65 -0
  346. package/dist/ui/StackSelector.js.map +1 -0
  347. package/dist/ui/Summary.d.ts +12 -0
  348. package/dist/ui/Summary.d.ts.map +1 -0
  349. package/dist/ui/Summary.js +114 -0
  350. package/dist/ui/Summary.js.map +1 -0
  351. package/dist/ui/SyncUI.d.ts +10 -0
  352. package/dist/ui/SyncUI.d.ts.map +1 -0
  353. package/dist/ui/SyncUI.js +64 -0
  354. package/dist/ui/SyncUI.js.map +1 -0
  355. package/dist/ui/Welcome.d.ts +7 -0
  356. package/dist/ui/Welcome.d.ts.map +1 -0
  357. package/dist/ui/Welcome.js +45 -0
  358. package/dist/ui/Welcome.js.map +1 -0
  359. package/dist/ui/theme.d.ts +10 -0
  360. package/dist/ui/theme.d.ts.map +1 -0
  361. package/dist/ui/theme.js +9 -0
  362. package/dist/ui/theme.js.map +1 -0
  363. package/modules/engram/.gitignore-snippet.txt +6 -0
  364. package/modules/engram/.mcp-config-snippet.json +11 -0
  365. package/modules/engram/README.md +146 -0
  366. package/modules/engram/install-engram.sh +216 -0
  367. package/modules/ghagga/.env.example +43 -0
  368. package/modules/ghagga/README.md +153 -0
  369. package/modules/ghagga/docker-compose.yml +80 -0
  370. package/modules/ghagga/setup-ghagga.sh +139 -0
  371. package/modules/memory-simple/.project/NOTES.md +22 -0
  372. package/modules/memory-simple/README.md +23 -0
  373. package/modules/obsidian-brain/.obsidian/app.json +23 -0
  374. package/modules/obsidian-brain/.obsidian/appearance.json +5 -0
  375. package/modules/obsidian-brain/.obsidian/bookmarks.json +34 -0
  376. package/modules/obsidian-brain/.obsidian/community-plugins.json +1 -0
  377. package/modules/obsidian-brain/.obsidian/core-plugins-migration.json +21 -0
  378. package/modules/obsidian-brain/.obsidian/core-plugins.json +18 -0
  379. package/modules/obsidian-brain/.obsidian/daily-notes.json +5 -0
  380. package/modules/obsidian-brain/.obsidian/graph.json +37 -0
  381. package/modules/obsidian-brain/.obsidian/hotkeys.json +14 -0
  382. package/modules/obsidian-brain/.obsidian/plugins/dataview/data.json +25 -0
  383. package/modules/obsidian-brain/.obsidian/plugins/obsidian-kanban/data.json +29 -0
  384. package/modules/obsidian-brain/.obsidian/plugins/templater-obsidian/data.json +18 -0
  385. package/modules/obsidian-brain/.obsidian/snippets/project-memory.css +71 -0
  386. package/modules/obsidian-brain/.obsidian-gitignore-snippet.txt +8 -0
  387. package/modules/obsidian-brain/.project/Attachments/.gitkeep +0 -0
  388. package/modules/obsidian-brain/.project/Memory/BLOCKERS.md +78 -0
  389. package/modules/obsidian-brain/.project/Memory/CONTEXT.md +102 -0
  390. package/modules/obsidian-brain/.project/Memory/DASHBOARD.md +73 -0
  391. package/modules/obsidian-brain/.project/Memory/DECISIONS.md +87 -0
  392. package/modules/obsidian-brain/.project/Memory/KANBAN.md +15 -0
  393. package/modules/obsidian-brain/.project/Memory/README.md +61 -0
  394. package/modules/obsidian-brain/.project/Memory/WAVES.md +78 -0
  395. package/modules/obsidian-brain/.project/Sessions/TEMPLATE.md +99 -0
  396. package/modules/obsidian-brain/.project/Templates/ADR.md +33 -0
  397. package/modules/obsidian-brain/.project/Templates/Blocker.md +21 -0
  398. package/modules/obsidian-brain/.project/Templates/Session.md +88 -0
  399. package/modules/obsidian-brain/README.md +268 -0
  400. package/modules/obsidian-brain/new-wave.sh +182 -0
  401. package/package.json +51 -0
  402. package/schemas/agent.schema.json +34 -0
  403. package/schemas/ai-config.schema.json +28 -0
  404. package/schemas/skill.schema.json +44 -0
  405. package/src/commands/analyze.test.ts +145 -0
  406. package/src/commands/analyze.ts +69 -0
  407. package/src/commands/doctor.test.ts +208 -0
  408. package/src/commands/doctor.ts +163 -0
  409. package/src/commands/init.test.ts +298 -0
  410. package/src/commands/init.ts +285 -0
  411. package/src/constants.ts +69 -0
  412. package/src/e2e/aggressive.e2e.test.ts +557 -0
  413. package/src/e2e/commands.e2e.test.ts +298 -0
  414. package/src/index.tsx +106 -0
  415. package/src/lib/common.test.ts +318 -0
  416. package/src/lib/common.ts +127 -0
  417. package/src/lib/frontmatter.test.ts +291 -0
  418. package/src/lib/frontmatter.ts +77 -0
  419. package/src/lib/template.test.ts +226 -0
  420. package/src/lib/template.ts +99 -0
  421. package/src/types/index.ts +53 -0
  422. package/src/ui/AnalyzeUI.tsx +133 -0
  423. package/src/ui/App.tsx +175 -0
  424. package/src/ui/CIContext.tsx +25 -0
  425. package/src/ui/CISelector.tsx +72 -0
  426. package/src/ui/Doctor.tsx +122 -0
  427. package/src/ui/Header.tsx +48 -0
  428. package/src/ui/MemorySelector.tsx +73 -0
  429. package/src/ui/NameInput.tsx +82 -0
  430. package/src/ui/OptionSelector.tsx +100 -0
  431. package/src/ui/Progress.tsx +88 -0
  432. package/src/ui/StackSelector.tsx +101 -0
  433. package/src/ui/Summary.tsx +134 -0
  434. package/src/ui/Welcome.tsx +54 -0
  435. package/src/ui/theme.ts +10 -0
  436. package/stryker.config.json +19 -0
  437. package/tasks/_TEMPLATE/files-edited.md +3 -0
  438. package/tasks/_TEMPLATE/plan.md +3 -0
  439. package/tasks/_TEMPLATE/research.md +3 -0
  440. package/tasks/_TEMPLATE/verification.md +5 -0
  441. package/templates/common/dependabot/cargo.yml +11 -0
  442. package/templates/common/dependabot/github-actions.yml +16 -0
  443. package/templates/common/dependabot/gomod.yml +15 -0
  444. package/templates/common/dependabot/gradle.yml +15 -0
  445. package/templates/common/dependabot/header.yml +3 -0
  446. package/templates/common/dependabot/maven.yml +15 -0
  447. package/templates/common/dependabot/npm.yml +20 -0
  448. package/templates/common/dependabot/pip.yml +11 -0
  449. package/templates/dependabot.yml +162 -0
  450. package/templates/github/ci-go.yml +41 -0
  451. package/templates/github/ci-java.yml +45 -0
  452. package/templates/github/ci-monorepo.yml +150 -0
  453. package/templates/github/ci-node.yml +42 -0
  454. package/templates/github/ci-python.yml +42 -0
  455. package/templates/github/ci-rust.yml +42 -0
  456. package/templates/github/dependabot-automerge.yml +40 -0
  457. package/templates/gitlab/gitlab-ci-go.yml +88 -0
  458. package/templates/gitlab/gitlab-ci-java.yml +79 -0
  459. package/templates/gitlab/gitlab-ci-monorepo.yml +126 -0
  460. package/templates/gitlab/gitlab-ci-node.yml +63 -0
  461. package/templates/gitlab/gitlab-ci-python.yml +147 -0
  462. package/templates/gitlab/gitlab-ci-rust.yml +67 -0
  463. package/templates/global/claude-settings.json +98 -0
  464. package/templates/global/codex-config.toml +8 -0
  465. package/templates/global/copilot-instructions/base-rules.instructions.md +13 -0
  466. package/templates/global/copilot-instructions/sdd-orchestrator.instructions.md +37 -0
  467. package/templates/global/gemini-commands/cleanup.toml +20 -0
  468. package/templates/global/gemini-commands/commit.toml +15 -0
  469. package/templates/global/gemini-commands/dead-code.toml +22 -0
  470. package/templates/global/gemini-commands/plan.toml +30 -0
  471. package/templates/global/gemini-commands/review.toml +17 -0
  472. package/templates/global/gemini-commands/sdd-apply.toml +22 -0
  473. package/templates/global/gemini-commands/sdd-ff.toml +14 -0
  474. package/templates/global/gemini-commands/sdd-new.toml +21 -0
  475. package/templates/global/gemini-commands/sdd-verify.toml +21 -0
  476. package/templates/global/gemini-commands/tdd.toml +26 -0
  477. package/templates/global/gemini-settings.json +8 -0
  478. package/templates/global/opencode-config.json +44 -0
  479. package/templates/global/sdd-instructions.md +47 -0
  480. package/templates/global/sdd-orchestrator-claude.md +46 -0
  481. package/templates/global/sdd-orchestrator-copilot.md +34 -0
  482. package/templates/renovate.json +69 -0
  483. package/templates/woodpecker/monorepo/backend.yml +34 -0
  484. package/templates/woodpecker/monorepo/frontend.yml +34 -0
  485. package/templates/woodpecker/monorepo/summary.yml +25 -0
  486. package/templates/woodpecker/woodpecker-go.yml +51 -0
  487. package/templates/woodpecker/woodpecker-java.yml +67 -0
  488. package/templates/woodpecker/woodpecker-node.yml +47 -0
  489. package/templates/woodpecker/woodpecker-python.yml +108 -0
  490. package/templates/woodpecker/woodpecker-rust.yml +57 -0
  491. package/tsconfig.json +19 -0
  492. package/vitest.config.ts +16 -0
  493. package/workflows/reusable-build-go.yml +111 -0
  494. package/workflows/reusable-build-java.yml +120 -0
  495. package/workflows/reusable-build-node.yml +145 -0
  496. package/workflows/reusable-build-python.yml +159 -0
  497. package/workflows/reusable-build-rust.yml +135 -0
  498. package/workflows/reusable-docker.yml +120 -0
  499. package/workflows/reusable-ghagga-review.yml +165 -0
  500. package/workflows/reusable-release.yml +91 -0
@@ -0,0 +1,2633 @@
1
+ ---
2
+ name: solo-dev-planner-testing
3
+ description: "Mรณdulo 5: Testing robusto con Testcontainers"
4
+ ---
5
+
6
+ # ๐Ÿงช Solo Dev Planner - Testing Strategy
7
+
8
+ > Mรณdulo 5 de 6: Testing robusto con Testcontainers
9
+
10
+ ## ๐Ÿ“š Relacionado con:
11
+ - 01-CORE.md (CI/CD integration)
12
+ - 02-SELF-CORRECTION.md (Auto-fix tests)
13
+ - 06-OPERATIONS.md (Mise tasks, DB para tests)
14
+
15
+ ---
16
+
17
+ ## ๐Ÿงช Testing Strategy Completa
18
+
19
+ ### Pirรกmide de Testing
20
+
21
+ ```
22
+ /\
23
+ /E2E\ โ† 10% (costosos, lentos, frรกgiles)
24
+ /------\
25
+ /Integr.\ โ† 20% (medios, con DB/API)
26
+ /----------\
27
+ / Unit \ โ† 70% (rรกpidos, baratos, aislados)
28
+ /--------------\
29
+
30
+ Regla de oro: Mientras mรกs bajo en la pirรกmide, mejor ROI
31
+ ```
32
+
33
+ ---
34
+
35
+ ## ๐Ÿ“Š Unit Tests (70% de cobertura)
36
+
37
+ ### TypeScript con Bun
38
+
39
+ ```typescript
40
+ // tests/unit/user.test.ts
41
+ import { test, expect, describe, beforeEach } from 'bun:test';
42
+ import { User } from '@/models/User';
43
+ import { hash } from '@/utils/crypto';
44
+
45
+ describe('User Model', () => {
46
+ describe('validation', () => {
47
+ test('rejects invalid email', () => {
48
+ expect(() => User.create({
49
+ email: 'invalid',
50
+ password: 'pass123'
51
+ })).toThrow('Invalid email format');
52
+ });
53
+
54
+ test('requires password min length', () => {
55
+ expect(() => User.create({
56
+ email: 'test@example.com',
57
+ password: '123'
58
+ })).toThrow('Password must be at least 8 characters');
59
+ });
60
+
61
+ test('accepts valid user data', () => {
62
+ const user = User.create({
63
+ email: 'test@example.com',
64
+ password: 'validpass123'
65
+ });
66
+
67
+ expect(user.email).toBe('test@example.com');
68
+ expect(user.password).not.toBe('validpass123'); // Should be hashed
69
+ });
70
+ });
71
+
72
+ describe('authentication', () => {
73
+ test('verifies correct password', async () => {
74
+ const user = await User.create({
75
+ email: 'test@example.com',
76
+ password: 'secret123'
77
+ });
78
+
79
+ const isValid = await user.verifyPassword('secret123');
80
+ expect(isValid).toBe(true);
81
+ });
82
+
83
+ test('rejects incorrect password', async () => {
84
+ const user = await User.create({
85
+ email: 'test@example.com',
86
+ password: 'secret123'
87
+ });
88
+
89
+ const isValid = await user.verifyPassword('wrong');
90
+ expect(isValid).toBe(false);
91
+ });
92
+ });
93
+ });
94
+ ```
95
+
96
+ ### Python con pytest
97
+
98
+ ```python
99
+ # tests/unit/test_user.py
100
+ import pytest
101
+ from app.models.user import User
102
+ from app.exceptions import ValidationError
103
+
104
+ class TestUserModel:
105
+ """Test suite for User model"""
106
+
107
+ def test_rejects_invalid_email(self):
108
+ with pytest.raises(ValidationError, match="Invalid email"):
109
+ User.create(email="invalid", password="pass123")
110
+
111
+ def test_requires_password_min_length(self):
112
+ with pytest.raises(ValidationError, match="at least 8 characters"):
113
+ User.create(email="test@example.com", password="123")
114
+
115
+ def test_accepts_valid_user_data(self):
116
+ user = User.create(
117
+ email="test@example.com",
118
+ password="validpass123"
119
+ )
120
+
121
+ assert user.email == "test@example.com"
122
+ assert user.password != "validpass123" # Should be hashed
123
+
124
+ @pytest.mark.asyncio
125
+ async def test_verifies_correct_password(self):
126
+ user = await User.create(
127
+ email="test@example.com",
128
+ password="secret123"
129
+ )
130
+
131
+ is_valid = await user.verify_password("secret123")
132
+ assert is_valid is True
133
+
134
+ @pytest.mark.asyncio
135
+ async def test_rejects_incorrect_password(self):
136
+ user = await User.create(
137
+ email="test@example.com",
138
+ password="secret123"
139
+ )
140
+
141
+ is_valid = await user.verify_password("wrong")
142
+ assert is_valid is False
143
+
144
+
145
+ # Fixtures en conftest.py
146
+ # tests/conftest.py
147
+ import pytest
148
+ from app.database import engine, SessionLocal
149
+ from app.models import Base
150
+
151
+ @pytest.fixture(scope="function")
152
+ async def db_session():
153
+ """Create a fresh database for each test"""
154
+ async with engine.begin() as conn:
155
+ await conn.run_sync(Base.metadata.create_all)
156
+
157
+ session = SessionLocal()
158
+ try:
159
+ yield session
160
+ finally:
161
+ session.close()
162
+ async with engine.begin() as conn:
163
+ await conn.run_sync(Base.metadata.drop_all)
164
+ ```
165
+
166
+ ### Go con testify
167
+
168
+ ```go
169
+ // internal/models/user_test.go
170
+ package models
171
+
172
+ import (
173
+ "testing"
174
+ "github.com/stretchr/testify/assert"
175
+ "github.com/stretchr/testify/require"
176
+ )
177
+
178
+ func TestUser_Create_ValidatesEmail(t *testing.T) {
179
+ tests := []struct {
180
+ name string
181
+ email string
182
+ wantErr bool
183
+ }{
184
+ {
185
+ name: "valid email",
186
+ email: "test@example.com",
187
+ wantErr: false,
188
+ },
189
+ {
190
+ name: "invalid email - no @",
191
+ email: "invalid",
192
+ wantErr: true,
193
+ },
194
+ {
195
+ name: "invalid email - no domain",
196
+ email: "test@",
197
+ wantErr: true,
198
+ },
199
+ }
200
+
201
+ for _, tt := range tests {
202
+ t.Run(tt.name, func(t *testing.T) {
203
+ user, err := NewUser(tt.email, "password123")
204
+
205
+ if tt.wantErr {
206
+ assert.Error(t, err)
207
+ assert.Nil(t, user)
208
+ } else {
209
+ assert.NoError(t, err)
210
+ assert.NotNil(t, user)
211
+ assert.Equal(t, tt.email, user.Email)
212
+ }
213
+ })
214
+ }
215
+ }
216
+
217
+ func TestUser_VerifyPassword(t *testing.T) {
218
+ user, err := NewUser("test@example.com", "secret123")
219
+ require.NoError(t, err)
220
+
221
+ t.Run("correct password", func(t *testing.T) {
222
+ valid := user.VerifyPassword("secret123")
223
+ assert.True(t, valid)
224
+ })
225
+
226
+ t.Run("incorrect password", func(t *testing.T) {
227
+ valid := user.VerifyPassword("wrong")
228
+ assert.False(t, valid)
229
+ })
230
+ }
231
+ ```
232
+
233
+ ### Test Data Factories (TypeScript)
234
+
235
+ ```typescript
236
+ // tests/factories/user.factory.ts
237
+ import { faker } from '@faker-js/faker';
238
+ import { db } from '@/db';
239
+
240
+ export const UserFactory = {
241
+ /**
242
+ * Build user data without saving to DB
243
+ */
244
+ build: (overrides: Partial<User> = {}) => ({
245
+ email: faker.internet.email(),
246
+ name: faker.person.fullName(),
247
+ password: faker.internet.password({ length: 12 }),
248
+ role: 'user',
249
+ createdAt: new Date(),
250
+ ...overrides,
251
+ }),
252
+
253
+ /**
254
+ * Create user in database
255
+ */
256
+ create: async (overrides: Partial<User> = {}) => {
257
+ const data = UserFactory.build(overrides);
258
+ return await db.user.create({ data });
259
+ },
260
+
261
+ /**
262
+ * Create multiple users
263
+ */
264
+ createMany: async (count: number, overrides: Partial<User> = {}) => {
265
+ return Promise.all(
266
+ Array.from({ length: count }, () => UserFactory.create(overrides))
267
+ );
268
+ },
269
+ };
270
+
271
+ // Uso en tests
272
+ test('can list users', async () => {
273
+ await UserFactory.createMany(5);
274
+
275
+ const users = await User.findAll();
276
+ expect(users).toHaveLength(5);
277
+ });
278
+
279
+ test('can create admin user', async () => {
280
+ const admin = await UserFactory.create({ role: 'admin' });
281
+ expect(admin.role).toBe('admin');
282
+ });
283
+ ```
284
+
285
+ ---
286
+
287
+ ## ๐Ÿ”— Integration Tests (20%)
288
+
289
+ ### API Integration Tests (TypeScript + Hono)
290
+
291
+ ```typescript
292
+ // tests/integration/auth.test.ts
293
+ import { test, expect, beforeAll, afterAll } from 'bun:test';
294
+ import { app } from '@/index';
295
+ import { db } from '@/db';
296
+ import { UserFactory } from '../factories/user.factory';
297
+
298
+ // Setup test database
299
+ beforeAll(async () => {
300
+ await db.migrate.latest();
301
+ });
302
+
303
+ afterAll(async () => {
304
+ await db.migrate.rollback();
305
+ await db.destroy();
306
+ });
307
+
308
+ test('POST /auth/register creates user', async () => {
309
+ const res = await app.request('/auth/register', {
310
+ method: 'POST',
311
+ headers: { 'Content-Type': 'application/json' },
312
+ body: JSON.stringify({
313
+ email: 'newuser@example.com',
314
+ password: 'password123',
315
+ name: 'New User',
316
+ }),
317
+ });
318
+
319
+ expect(res.status).toBe(201);
320
+
321
+ const json = await res.json();
322
+ expect(json.user.email).toBe('newuser@example.com');
323
+ expect(json.token).toBeDefined();
324
+
325
+ // Verify user in database
326
+ const user = await db.user.findUnique({
327
+ where: { email: 'newuser@example.com' },
328
+ });
329
+ expect(user).toBeDefined();
330
+ expect(user!.name).toBe('New User');
331
+ });
332
+
333
+ test('POST /auth/login returns token for valid credentials', async () => {
334
+ // Arrange: Create user
335
+ const password = 'secret123';
336
+ const user = await UserFactory.create({ password });
337
+
338
+ // Act: Login
339
+ const res = await app.request('/auth/login', {
340
+ method: 'POST',
341
+ headers: { 'Content-Type': 'application/json' },
342
+ body: JSON.stringify({
343
+ email: user.email,
344
+ password,
345
+ }),
346
+ });
347
+
348
+ // Assert
349
+ expect(res.status).toBe(200);
350
+
351
+ const json = await res.json();
352
+ expect(json.token).toBeDefined();
353
+ expect(json.user.id).toBe(user.id);
354
+ });
355
+
356
+ test('protected routes require authentication', async () => {
357
+ const res = await app.request('/api/profile', {
358
+ method: 'GET',
359
+ });
360
+
361
+ expect(res.status).toBe(401);
362
+ });
363
+
364
+ test('protected routes accept valid token', async () => {
365
+ // Create user and get token
366
+ const user = await UserFactory.create();
367
+ const token = await generateToken(user);
368
+
369
+ const res = await app.request('/api/profile', {
370
+ method: 'GET',
371
+ headers: {
372
+ 'Authorization': `Bearer ${token}`,
373
+ },
374
+ });
375
+
376
+ expect(res.status).toBe(200);
377
+
378
+ const json = await res.json();
379
+ expect(json.email).toBe(user.email);
380
+ });
381
+ ```
382
+
383
+ ### Database Integration Tests (Python)
384
+
385
+ ```python
386
+ # tests/integration/test_user_repository.py
387
+ import pytest
388
+ from app.repositories.user_repository import UserRepository
389
+ from app.models.user import User
390
+
391
+ @pytest.mark.asyncio
392
+ class TestUserRepository:
393
+ async def test_create_and_find_user(self, db_session):
394
+ repo = UserRepository(db_session)
395
+
396
+ # Create user
397
+ user = await repo.create(
398
+ email="test@example.com",
399
+ password="password123"
400
+ )
401
+
402
+ assert user.id is not None
403
+ assert user.email == "test@example.com"
404
+
405
+ # Find user
406
+ found = await repo.find_by_email("test@example.com")
407
+ assert found is not None
408
+ assert found.id == user.id
409
+
410
+ async def test_update_user(self, db_session):
411
+ repo = UserRepository(db_session)
412
+
413
+ user = await repo.create(
414
+ email="test@example.com",
415
+ password="password123"
416
+ )
417
+
418
+ # Update
419
+ updated = await repo.update(
420
+ user.id,
421
+ name="Updated Name"
422
+ )
423
+
424
+ assert updated.name == "Updated Name"
425
+ assert updated.email == "test@example.com"
426
+
427
+ async def test_delete_user(self, db_session):
428
+ repo = UserRepository(db_session)
429
+
430
+ user = await repo.create(
431
+ email="test@example.com",
432
+ password="password123"
433
+ )
434
+
435
+ # Delete
436
+ await repo.delete(user.id)
437
+
438
+ # Verify deleted
439
+ found = await repo.find_by_id(user.id)
440
+ assert found is None
441
+ ```
442
+
443
+ ---
444
+
445
+ ## ๐ŸŒ E2E Tests (10%)
446
+
447
+ ### Playwright Setup
448
+
449
+ ```typescript
450
+ // playwright.config.ts
451
+ import { defineConfig } from '@playwright/test';
452
+
453
+ export default defineConfig({
454
+ testDir: './tests/e2e',
455
+ fullyParallel: true,
456
+ forbidOnly: !!process.env.CI,
457
+ retries: process.env.CI ? 2 : 0,
458
+ workers: process.env.CI ? 1 : undefined,
459
+
460
+ use: {
461
+ baseURL: 'http://localhost:3000',
462
+ trace: 'on-first-retry',
463
+ screenshot: 'only-on-failure',
464
+ },
465
+
466
+ webServer: {
467
+ command: 'mise run dev',
468
+ port: 3000,
469
+ reuseExistingServer: !process.env.CI,
470
+ },
471
+ });
472
+ ```
473
+
474
+ ### E2E Test Examples
475
+
476
+ ```typescript
477
+ // tests/e2e/auth-flow.spec.ts
478
+ import { test, expect } from '@playwright/test';
479
+
480
+ test.describe('Complete Authentication Flow', () => {
481
+ test('user can register, login, and access protected pages', async ({ page }) => {
482
+ // Register
483
+ await page.goto('/register');
484
+ await page.fill('[name="email"]', 'user@test.com');
485
+ await page.fill('[name="password"]', 'password123');
486
+ await page.fill('[name="name"]', 'Test User');
487
+ await page.click('button[type="submit"]');
488
+
489
+ // Should redirect to dashboard
490
+ await expect(page).toHaveURL('/dashboard');
491
+ await expect(page.locator('h1')).toContainText('Welcome, Test User');
492
+
493
+ // Logout
494
+ await page.click('[data-testid="logout-button"]');
495
+ await expect(page).toHaveURL('/login');
496
+
497
+ // Login again
498
+ await page.fill('[name="email"]', 'user@test.com');
499
+ await page.fill('[name="password"]', 'password123');
500
+ await page.click('button[type="submit"]');
501
+
502
+ // Should be logged in
503
+ await expect(page).toHaveURL('/dashboard');
504
+ });
505
+
506
+ test('shows error for invalid credentials', async ({ page }) => {
507
+ await page.goto('/login');
508
+ await page.fill('[name="email"]', 'wrong@test.com');
509
+ await page.fill('[name="password"]', 'wrongpass');
510
+ await page.click('button[type="submit"]');
511
+
512
+ await expect(page.locator('[role="alert"]'))
513
+ .toContainText('Invalid credentials');
514
+ });
515
+ });
516
+
517
+ // tests/e2e/task-management.spec.ts
518
+ test.describe('Task Management', () => {
519
+ test.beforeEach(async ({ page }) => {
520
+ // Login before each test
521
+ await page.goto('/login');
522
+ await page.fill('[name="email"]', 'user@test.com');
523
+ await page.fill('[name="password"]', 'password123');
524
+ await page.click('button[type="submit"]');
525
+ });
526
+
527
+ test('can create, edit, and delete task', async ({ page }) => {
528
+ // Create task
529
+ await page.goto('/tasks');
530
+ await page.click('[data-testid="new-task"]');
531
+ await page.fill('[name="title"]', 'My New Task');
532
+ await page.fill('[name="description"]', 'Task description');
533
+ await page.click('button[type="submit"]');
534
+
535
+ await expect(page.locator('[data-testid="task-item"]'))
536
+ .toContainText('My New Task');
537
+
538
+ // Edit task
539
+ await page.click('[data-testid="edit-task"]');
540
+ await page.fill('[name="title"]', 'Updated Task');
541
+ await page.click('button[type="submit"]');
542
+
543
+ await expect(page.locator('[data-testid="task-item"]'))
544
+ .toContainText('Updated Task');
545
+
546
+ // Delete task
547
+ await page.click('[data-testid="delete-task"]');
548
+ await page.click('[data-testid="confirm-delete"]');
549
+
550
+ await expect(page.locator('[data-testid="task-item"]'))
551
+ .not.toBeVisible();
552
+ });
553
+ });
554
+ ```
555
+
556
+ ---
557
+
558
+ ## ๐ŸŽฏ Mise Tasks para Testing
559
+
560
+ ```toml
561
+ # .mise.toml
562
+
563
+ [tasks."test:unit"]
564
+ description = "Run unit tests"
565
+ run = """
566
+ #!/usr/bin/env bash
567
+ if mise current node &> /dev/null; then
568
+ bun test tests/unit/
569
+ elif mise current python &> /dev/null; then
570
+ pytest tests/unit/ -v
571
+ elif mise current go &> /dev/null; then
572
+ go test ./... -short
573
+ fi
574
+ """
575
+ alias = "tu"
576
+
577
+ [tasks."test:integration"]
578
+ description = "Run integration tests (requires DB)"
579
+ run = """
580
+ #!/usr/bin/env bash
581
+
582
+ # Start test database
583
+ docker compose -f docker-compose.test.yml up -d
584
+ echo "โณ Waiting for database..."
585
+ sleep 5
586
+
587
+ # Run migrations
588
+ mise run db:migrate
589
+
590
+ # Run tests
591
+ if mise current node &> /dev/null; then
592
+ bun test tests/integration/
593
+ elif mise current python &> /dev/null; then
594
+ pytest tests/integration/ -v
595
+ elif mise current go &> /dev/null; then
596
+ go test ./... -run Integration
597
+ fi
598
+
599
+ # Cleanup
600
+ docker compose -f docker-compose.test.yml down
601
+ """
602
+ alias = "ti"
603
+
604
+ [tasks."test:e2e"]
605
+ description = "Run E2E tests with Playwright"
606
+ run = """
607
+ #!/usr/bin/env bash
608
+ # Start app in background
609
+ mise run dev &
610
+ APP_PID=$!
611
+
612
+ # Wait for app to be ready
613
+ sleep 5
614
+
615
+ # Run E2E tests
616
+ playwright test
617
+
618
+ # Cleanup
619
+ kill $APP_PID
620
+ """
621
+ alias = "te"
622
+
623
+ [tasks."test:watch"]
624
+ description = "Run tests in watch mode"
625
+ run = """
626
+ #!/usr/bin/env bash
627
+ if mise current node &> /dev/null; then
628
+ bun test --watch
629
+ elif mise current python &> /dev/null; then
630
+ ptw tests/ --
631
+ elif mise current go &> /dev/null; then
632
+ gotestsum --watch
633
+ fi
634
+ """
635
+ alias = "tw"
636
+
637
+ [tasks."test:coverage"]
638
+ description = "Run tests with coverage report"
639
+ run = """
640
+ #!/usr/bin/env bash
641
+ if mise current node &> /dev/null; then
642
+ bun test --coverage
643
+ elif mise current python &> /dev/null; then
644
+ pytest --cov=app --cov-report=html --cov-report=term
645
+ elif mise current go &> /dev/null; then
646
+ go test -coverprofile=coverage.out ./...
647
+ go tool cover -html=coverage.out -o coverage.html
648
+ fi
649
+
650
+ echo "โœ… Coverage report generated"
651
+ """
652
+ alias = "tc"
653
+
654
+ [tasks.test]
655
+ description = "Run all tests (unit + integration)"
656
+ run = """
657
+ mise run test:unit
658
+ mise run test:integration
659
+ """
660
+ alias = "t"
661
+ ```
662
+
663
+ ### Docker Compose para Tests
664
+
665
+ ```yaml
666
+ # docker-compose.test.yml
667
+ version: '3.8'
668
+
669
+ services:
670
+ db-test:
671
+ image: postgres:16-alpine
672
+ environment:
673
+ POSTGRES_DB: testdb
674
+ POSTGRES_USER: testuser
675
+ POSTGRES_PASSWORD: testpass
676
+ ports:
677
+ - "5433:5432"
678
+ tmpfs:
679
+ - /var/lib/postgresql/data # Usar RAM para tests (mรกs rรกpido)
680
+
681
+ redis-test:
682
+ image: redis:7-alpine
683
+ ports:
684
+ - "6380:6379"
685
+ ```
686
+
687
+ ---
688
+
689
+ ## ๐Ÿ“Š Coverage Configuration
690
+
691
+ ### TypeScript (Bun)
692
+
693
+ ```json
694
+ // package.json
695
+ {
696
+ "scripts": {
697
+ "test:coverage": "bun test --coverage"
698
+ }
699
+ }
700
+ ```
701
+
702
+ ### Python (pytest)
703
+
704
+ ```ini
705
+ # pytest.ini
706
+ [pytest]
707
+ testpaths = tests
708
+ python_files = test_*.py
709
+ python_classes = Test*
710
+ python_functions = test_*
711
+
712
+ addopts =
713
+ -v
714
+ --strict-markers
715
+ --cov=app
716
+ --cov-report=html
717
+ --cov-report=term-missing
718
+ --cov-fail-under=80
719
+
720
+ markers =
721
+ slow: marks tests as slow
722
+ integration: marks tests as integration tests
723
+ e2e: marks tests as end-to-end tests
724
+ ```
725
+
726
+ ### Go
727
+
728
+ ```bash
729
+ # Makefile o mise task
730
+ go test -coverprofile=coverage.out ./...
731
+ go tool cover -func=coverage.out
732
+ go tool cover -html=coverage.out -o coverage.html
733
+ ```
734
+
735
+ ---
736
+
737
+ ## ๐ŸŽฏ Best Practices
738
+
739
+ ### 1. Test Naming Conventions
740
+
741
+ ```typescript
742
+ // โŒ Bad
743
+ test('test1', () => {});
744
+
745
+ // โœ… Good
746
+ test('UserService.create rejects invalid email', () => {});
747
+ test('POST /users returns 201 for valid data', () => {});
748
+ ```
749
+
750
+ ### 2. Arrange-Act-Assert Pattern
751
+
752
+ ```typescript
753
+ test('user can update profile', async () => {
754
+ // Arrange
755
+ const user = await UserFactory.create();
756
+ const token = await generateToken(user);
757
+
758
+ // Act
759
+ const res = await app.request('/api/profile', {
760
+ method: 'PATCH',
761
+ headers: { Authorization: `Bearer ${token}` },
762
+ body: JSON.stringify({ name: 'New Name' }),
763
+ });
764
+
765
+ // Assert
766
+ expect(res.status).toBe(200);
767
+ const updated = await res.json();
768
+ expect(updated.name).toBe('New Name');
769
+ });
770
+ ```
771
+
772
+ ### 3. Test Isolation
773
+
774
+ ```typescript
775
+ // โŒ Bad: Tests depend on each other
776
+ let userId: string;
777
+
778
+ test('create user', async () => {
779
+ const res = await createUser();
780
+ userId = res.id; // Shared state!
781
+ });
782
+
783
+ test('update user', async () => {
784
+ await updateUser(userId); // Depends on previous test
785
+ });
786
+
787
+ // โœ… Good: Each test is independent
788
+ test('can update user', async () => {
789
+ const user = await UserFactory.create(); // Fresh user
790
+ await updateUser(user.id);
791
+ });
792
+ ```
793
+
794
+ ### 4. Mock External Services
795
+
796
+ ```typescript
797
+ // tests/mocks/email.mock.ts
798
+ export const emailService = {
799
+ send: vi.fn().mockResolvedValue({ success: true }),
800
+ };
801
+
802
+ // In test
803
+ test('sends welcome email on registration', async () => {
804
+ await registerUser({ email: 'test@example.com' });
805
+
806
+ expect(emailService.send).toHaveBeenCalledWith({
807
+ to: 'test@example.com',
808
+ template: 'welcome',
809
+ });
810
+ });
811
+ ```
812
+
813
+ ---
814
+
815
+ ## ๐Ÿงช Testcontainers (Tests Reales sin Mocks)
816
+
817
+ ### Filosofรญa: Tests Reales > Mocks Frรกgiles
818
+
819
+ **Problema con Mocks:**
820
+ ```typescript
821
+ โŒ Frรกgiles - Se rompen al cambiar implementaciรณn
822
+ โŒ No prueban queries SQL reales
823
+ โŒ Mantenimiento costoso (mock de cada mรฉtodo)
824
+ โŒ Falsa sensaciรณn de seguridad
825
+ ```
826
+
827
+ **Con Testcontainers:**
828
+ ```typescript
829
+ โœ… Prueban contra base de datos REAL
830
+ โœ… Queries SQL ejecutados realmente
831
+ โœ… Menos cรณdigo de test (no mockear)
832
+ โœ… Mayor confianza en tests
833
+ โŒ Mรกs lentos (pero cacheables)
834
+ ```
835
+
836
+ ### Cuรกndo Usar Cada Enfoque
837
+
838
+ ```
839
+ Unit Tests (70%):
840
+ โœ… Lรณgica de negocio pura
841
+ โœ… Funciones sin side effects
842
+ โœ… Validaciones
843
+ โ†’ NO necesitan DB ni mocks
844
+
845
+ Integration Tests (20%):
846
+ โœ… Repository layer
847
+ โœ… Queries complejas
848
+ โœ… Transactions
849
+ โ†’ Testcontainers
850
+
851
+ E2E Tests (10%):
852
+ โœ… User flows completos
853
+ โ†’ Playwright + Testcontainers
854
+ ```
855
+
856
+ ---
857
+
858
+ ## TypeScript + Bun + Testcontainers
859
+
860
+ ### Setup
861
+
862
+ ```bash
863
+ # Instalar
864
+ bun add -d @testcontainers/postgresql
865
+ ```
866
+
867
+ ### Test Setup
868
+
869
+ ```typescript
870
+ // tests/integration/setup.ts
871
+ import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';
872
+ import { drizzle } from 'drizzle-orm/postgres-js';
873
+ import postgres from 'postgres';
874
+ import * as schema from '@/db/schema';
875
+
876
+ let container: StartedPostgreSqlContainer;
877
+ let db: ReturnType<typeof drizzle>;
878
+
879
+ export async function setupTestDB() {
880
+ // Iniciar container (reutilizable entre tests)
881
+ container = await new PostgreSqlContainer('postgres:16-alpine')
882
+ .withDatabase('testdb')
883
+ .withUsername('test')
884
+ .withPassword('test')
885
+ .withReuse() // โ† IMPORTANTE: Reutilizar = mรกs rรกpido
886
+ .start();
887
+
888
+ // Conectar
889
+ const connectionString = container.getConnectionUri();
890
+ const client = postgres(connectionString);
891
+ db = drizzle(client, { schema });
892
+
893
+ // Aplicar migraciones
894
+ await runMigrations(db);
895
+
896
+ return { db, connectionString };
897
+ }
898
+
899
+ export async function teardownTestDB() {
900
+ await container.stop();
901
+ }
902
+
903
+ export async function resetTestDB() {
904
+ // Limpiar todas las tablas entre tests
905
+ await db.delete(schema.users);
906
+ await db.delete(schema.tasks);
907
+ }
908
+ ```
909
+
910
+ ### Tests de Integraciรณn
911
+
912
+ ```typescript
913
+ // tests/integration/user-repository.test.ts
914
+ import { test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
915
+ import { setupTestDB, teardownTestDB, resetTestDB } from './setup';
916
+ import { UserRepository } from '@/repositories/UserRepository';
917
+
918
+ let db: any;
919
+ let userRepo: UserRepository;
920
+
921
+ beforeAll(async () => {
922
+ const setup = await setupTestDB();
923
+ db = setup.db;
924
+ userRepo = new UserRepository(db);
925
+ });
926
+
927
+ afterAll(async () => {
928
+ await teardownTestDB();
929
+ });
930
+
931
+ beforeEach(async () => {
932
+ await resetTestDB(); // Fresh DB para cada test
933
+ });
934
+
935
+ test('UserRepository.create inserts into real database', async () => {
936
+ // Arrange
937
+ const userData = {
938
+ email: 'test@example.com',
939
+ name: 'Test User',
940
+ password: 'hashed_password',
941
+ };
942
+
943
+ // Act
944
+ const user = await userRepo.create(userData);
945
+
946
+ // Assert
947
+ expect(user.id).toBeDefined();
948
+ expect(user.email).toBe('test@example.com');
949
+
950
+ // Verificar en DB REAL (no mock)
951
+ const found = await userRepo.findById(user.id);
952
+ expect(found).toBeDefined();
953
+ expect(found!.name).toBe('Test User');
954
+ });
955
+
956
+ test('complex query with joins works correctly', async () => {
957
+ // Arrange
958
+ const user = await userRepo.create({
959
+ email: 'test@example.com',
960
+ name: 'Test',
961
+ password: 'pass'
962
+ });
963
+
964
+ const task = await taskRepo.create({
965
+ userId: user.id,
966
+ title: 'Test Task',
967
+ completed: false,
968
+ });
969
+
970
+ // Act - Query complejo con JOIN
971
+ const userWithTasks = await userRepo.findWithTasks(user.id);
972
+
973
+ // Assert - Verifica query SQL real
974
+ expect(userWithTasks.tasks).toHaveLength(1);
975
+ expect(userWithTasks.tasks[0].title).toBe('Test Task');
976
+ });
977
+ ```
978
+
979
+ ---
980
+
981
+ ## Python + pytest + Testcontainers
982
+
983
+ ### Setup
984
+
985
+ ```bash
986
+ # Instalar
987
+ uv add --dev testcontainers
988
+ ```
989
+
990
+ ### Test Setup
991
+
992
+ ```python
993
+ # tests/integration/conftest.py
994
+ import pytest
995
+ from testcontainers.postgres import PostgresContainer
996
+ from sqlalchemy import create_engine
997
+ from sqlalchemy.orm import sessionmaker
998
+ from app.models import Base
999
+
1000
+ @pytest.fixture(scope="session")
1001
+ def postgres_container():
1002
+ """PostgreSQL container (reutilizado en toda la sesiรณn)"""
1003
+ with PostgresContainer("postgres:16-alpine") as postgres:
1004
+ yield postgres
1005
+
1006
+ @pytest.fixture(scope="session")
1007
+ def engine(postgres_container):
1008
+ """SQLAlchemy engine"""
1009
+ engine = create_engine(postgres_container.get_connection_url())
1010
+ Base.metadata.create_all(engine)
1011
+ return engine
1012
+
1013
+ @pytest.fixture(scope="function")
1014
+ def db_session(engine):
1015
+ """Fresh database session para cada test"""
1016
+ Session = sessionmaker(bind=engine)
1017
+ session = Session()
1018
+
1019
+ try:
1020
+ yield session
1021
+ finally:
1022
+ session.rollback()
1023
+ session.close()
1024
+
1025
+ # Limpiar tablas
1026
+ for table in reversed(Base.metadata.sorted_tables):
1027
+ engine.execute(table.delete())
1028
+ ```
1029
+
1030
+ ### Tests de Integraciรณn
1031
+
1032
+ ```python
1033
+ # tests/integration/test_user_repository.py
1034
+ import pytest
1035
+ from app.repositories.user_repository import UserRepository
1036
+
1037
+ class TestUserRepository:
1038
+ def test_create_inserts_into_real_database(self, db_session):
1039
+ # Arrange
1040
+ repo = UserRepository(db_session)
1041
+ user_data = {
1042
+ "email": "test@example.com",
1043
+ "name": "Test User",
1044
+ "password": "hashed"
1045
+ }
1046
+
1047
+ # Act
1048
+ user = repo.create(**user_data)
1049
+ db_session.commit()
1050
+
1051
+ # Assert
1052
+ assert user.id is not None
1053
+
1054
+ # Verificar en DB real
1055
+ found = repo.find_by_id(user.id)
1056
+ assert found.email == "test@example.com"
1057
+
1058
+ def test_complex_query_with_joins(self, db_session):
1059
+ repo = UserRepository(db_session)
1060
+ task_repo = TaskRepository(db_session)
1061
+
1062
+ # Create user y tasks
1063
+ user = repo.create(email="test@example.com", name="Test", password="pass")
1064
+ task = task_repo.create(user_id=user.id, title="Task 1")
1065
+ db_session.commit()
1066
+
1067
+ # Query con JOIN
1068
+ user_with_tasks = repo.find_with_tasks(user.id)
1069
+
1070
+ # Assert SQL real ejecutado
1071
+ assert len(user_with_tasks.tasks) == 1
1072
+ assert user_with_tasks.tasks[0].title == "Task 1"
1073
+ ```
1074
+
1075
+ ---
1076
+
1077
+ ## Go + testcontainers-go
1078
+
1079
+ ### Setup
1080
+
1081
+ ```bash
1082
+ go get github.com/testcontainers/testcontainers-go
1083
+ go get github.com/testcontainers/testcontainers-go/modules/postgres
1084
+ ```
1085
+
1086
+ ### Test Setup
1087
+
1088
+ ```go
1089
+ // internal/repository/setup_test.go
1090
+ package repository
1091
+
1092
+ import (
1093
+ "context"
1094
+ "database/sql"
1095
+ "testing"
1096
+
1097
+ "github.com/testcontainers/testcontainers-go/modules/postgres"
1098
+ )
1099
+
1100
+ var testDB *sql.DB
1101
+
1102
+ func setupTestDB(t *testing.T) *sql.DB {
1103
+ ctx := context.Background()
1104
+
1105
+ // Start container
1106
+ pgContainer, err := postgres.RunContainer(ctx,
1107
+ testcontainers.WithImage("postgres:16-alpine"),
1108
+ postgres.WithDatabase("testdb"),
1109
+ postgres.WithUsername("test"),
1110
+ postgres.WithPassword("test"),
1111
+ )
1112
+ if err != nil {
1113
+ t.Fatal(err)
1114
+ }
1115
+
1116
+ t.Cleanup(func() {
1117
+ pgContainer.Terminate(ctx)
1118
+ })
1119
+
1120
+ // Connect
1121
+ connStr, _ := pgContainer.ConnectionString(ctx, "sslmode=disable")
1122
+ db, err := sql.Open("postgres", connStr)
1123
+ if err != nil {
1124
+ t.Fatal(err)
1125
+ }
1126
+
1127
+ // Run migrations
1128
+ runMigrations(db)
1129
+
1130
+ return db
1131
+ }
1132
+
1133
+ func resetDB(t *testing.T, db *sql.DB) {
1134
+ _, err := db.Exec("TRUNCATE users, tasks CASCADE")
1135
+ if err != nil {
1136
+ t.Fatal(err)
1137
+ }
1138
+ }
1139
+ ```
1140
+
1141
+ ### Tests
1142
+
1143
+ ```go
1144
+ // internal/repository/user_repository_test.go
1145
+ func TestUserRepository_Create(t *testing.T) {
1146
+ db := setupTestDB(t)
1147
+ defer db.Close()
1148
+ resetDB(t, db)
1149
+
1150
+ repo := NewUserRepository(db)
1151
+
1152
+ // Arrange
1153
+ user := &User{
1154
+ Email: "test@example.com",
1155
+ Name: "Test User",
1156
+ Password: "hashed",
1157
+ }
1158
+
1159
+ // Act
1160
+ err := repo.Create(context.Background(), user)
1161
+
1162
+ // Assert
1163
+ assert.NoError(t, err)
1164
+ assert.NotZero(t, user.ID)
1165
+
1166
+ // Verify en DB real
1167
+ found, err := repo.FindByID(context.Background(), user.ID)
1168
+ require.NoError(t, err)
1169
+ assert.Equal(t, "test@example.com", found.Email)
1170
+ }
1171
+ ```
1172
+
1173
+ ---
1174
+
1175
+ ## Performance Tips
1176
+
1177
+ ### 1. Reutilizar Containers
1178
+
1179
+ ```typescript
1180
+ // โŒ Lento (inicia container por cada test suite)
1181
+ const container = await new PostgreSqlContainer().start();
1182
+
1183
+ // โœ… Rรกpido (reutiliza container)
1184
+ const container = await new PostgreSqlContainer()
1185
+ .withReuse() // โ† IMPORTANTE
1186
+ .start();
1187
+
1188
+ // Mejora: 5-10s โ†’ 1-2s por suite
1189
+ ```
1190
+
1191
+ ### 2. Usar tmpfs para Datos
1192
+
1193
+ ```yaml
1194
+ # docker-compose.test.yml
1195
+ services:
1196
+ test-db:
1197
+ image: postgres:16-alpine
1198
+ tmpfs:
1199
+ - /var/lib/postgresql/data # RAM disk = mรกs rรกpido
1200
+
1201
+ # Mejora: 30-50% mรกs rรกpido
1202
+ ```
1203
+
1204
+ ### 3. Parallel Tests con Mรบltiples Containers
1205
+
1206
+ ```typescript
1207
+ // Cada worker de test obtiene su propio container
1208
+ test.concurrent('test 1', async () => {
1209
+ const container = await getOrCreateContainer();
1210
+ // ...
1211
+ });
1212
+ ```
1213
+
1214
+ ---
1215
+
1216
+ ## Mise Tasks para Testcontainers
1217
+
1218
+ ```toml
1219
+ # .mise.toml
1220
+
1221
+ [tasks."test:integration"]
1222
+ description = "Run integration tests with Testcontainers"
1223
+ run = """
1224
+ #!/usr/bin/env bash
1225
+
1226
+ # Verificar Docker
1227
+ if ! docker info > /dev/null 2>&1; then
1228
+ echo "โŒ Docker no estรก corriendo"
1229
+ echo "Inicia Docker y vuelve a intentar"
1230
+ exit 1
1231
+ fi
1232
+
1233
+ echo "๐Ÿณ Starting Testcontainers..."
1234
+
1235
+ if [ -f "package.json" ]; then
1236
+ bun test tests/integration/
1237
+ elif [ -f "pyproject.toml" ]; then
1238
+ pytest tests/integration/ -v
1239
+ elif [ -f "go.mod" ]; then
1240
+ go test ./internal/... -tags=integration
1241
+ fi
1242
+
1243
+ echo "โœ… Integration tests completed"
1244
+ """
1245
+
1246
+ [tasks."test:integration:watch"]
1247
+ description = "Watch mode for integration tests"
1248
+ run = """
1249
+ if [ -f "package.json" ]; then
1250
+ bun test --watch tests/integration/
1251
+ elif [ -f "pyproject.toml" ]; then
1252
+ ptw tests/integration/
1253
+ fi
1254
+ """
1255
+ ```
1256
+
1257
+ ---
1258
+
1259
+ ## Comparaciรณn: Mocks vs Testcontainers
1260
+
1261
+ | Aspecto | Mocks | Testcontainers |
1262
+ |---------|-------|----------------|
1263
+ | **Velocidad** | โšก 1-2ms | ๐Ÿข 100-500ms (con cache: 10-50ms) |
1264
+ | **Confianza** | ๐ŸŸก Media | โœ… Alta |
1265
+ | **Mantenimiento** | โŒ Alto | โœ… Bajo |
1266
+ | **Queries reales** | โŒ No | โœ… Sรญ |
1267
+ | **Setup** | ๐ŸŸก Medio | โœ… Simple |
1268
+ | **CI/CD** | โœ… Rรกpido | ๐ŸŸก Necesita Docker |
1269
+
1270
+ ### Recomendaciรณn
1271
+
1272
+ ```
1273
+ Unit tests (70%): Sin DB, sin mocks
1274
+ Integration tests (20%): Testcontainers
1275
+ E2E tests (10%): Testcontainers + Playwright
1276
+ ```
1277
+
1278
+ ---
1279
+
1280
+ ## ๐Ÿงช Testcontainers - Tests Reales Sin Mocks
1281
+
1282
+ ### Filosofรญa: Tests Reales > Mocks Frรกgiles
1283
+
1284
+ **Problema con Mocks:**
1285
+ ```typescript
1286
+ โŒ Frรกgiles - Se rompen con cambios de implementaciรณn
1287
+ โŒ No prueban queries SQL reales
1288
+ โŒ Mantenimiento costoso (mock hell)
1289
+ โŒ Falsa sensaciรณn de seguridad
1290
+ โŒ Diferencias entre mock y DB real
1291
+ ```
1292
+
1293
+ **Con Testcontainers:**
1294
+ ```typescript
1295
+ โœ… Prueban contra DB real en Docker
1296
+ โœ… Queries SQL reales ejecutadas
1297
+ โœ… Menos mantenimiento a largo plazo
1298
+ โœ… Mayor confianza en producciรณn
1299
+ โœ… Detecta problemas de performance
1300
+ โš ๏ธ Mรกs lentos (pero con cache ~2 segundos)
1301
+ ```
1302
+
1303
+ ### Cuรกndo Usar Cada Estrategia
1304
+
1305
+ ```
1306
+ Unit Tests (70%):
1307
+ โœ… Lรณgica de negocio pura
1308
+ โœ… Funciones sin side effects
1309
+ โœ… Validaciones
1310
+ โ†’ NO mocks, NO DB, SOLO lรณgica
1311
+
1312
+ Integration Tests (20%):
1313
+ โœ… Repository layer
1314
+ โœ… Database queries
1315
+ โœ… API endpoints completos
1316
+ โ†’ TESTCONTAINERS (DB real)
1317
+
1318
+ E2E Tests (10%):
1319
+ โœ… User flows completos
1320
+ โœ… UI + API + DB
1321
+ โ†’ Playwright + Testcontainers
1322
+ ```
1323
+
1324
+ ---
1325
+
1326
+ ## TypeScript + Bun + PostgreSQL + Testcontainers
1327
+
1328
+ ### Instalaciรณn
1329
+
1330
+ ```bash
1331
+ bun add -d @testcontainers/postgresql testcontainers
1332
+ ```
1333
+
1334
+ ### Setup de Tests Integrados
1335
+
1336
+ ```typescript
1337
+ // tests/integration/setup.ts
1338
+ import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';
1339
+ import { drizzle } from 'drizzle-orm/postgres-js';
1340
+ import postgres from 'postgres';
1341
+ import * as schema from '@/db/schema';
1342
+
1343
+ let container: StartedPostgreSqlContainer;
1344
+ let db: ReturnType<typeof drizzle>;
1345
+
1346
+ export async function setupTestDB() {
1347
+ console.log('๐Ÿณ Starting PostgreSQL container...');
1348
+
1349
+ // Iniciar container (con reuse para speed)
1350
+ container = await new PostgreSqlContainer('postgres:16-alpine')
1351
+ .withDatabase('testdb')
1352
+ .withUsername('test')
1353
+ .withPassword('test')
1354
+ .withReuse() // โ† IMPORTANTE: Reutiliza container entre test suites
1355
+ .start();
1356
+
1357
+ console.log('โœ… PostgreSQL container started');
1358
+
1359
+ // Conectar a la DB
1360
+ const connectionString = container.getConnectionUri();
1361
+ const client = postgres(connectionString);
1362
+ db = drizzle(client, { schema });
1363
+
1364
+ // Aplicar migraciones
1365
+ console.log('๐Ÿ”„ Applying migrations...');
1366
+ await runMigrations(db);
1367
+ console.log('โœ… Migrations applied');
1368
+
1369
+ return { db, connectionString };
1370
+ }
1371
+
1372
+ export async function teardownTestDB() {
1373
+ console.log('๐Ÿ›‘ Stopping PostgreSQL container...');
1374
+ await container.stop();
1375
+ console.log('โœ… Container stopped');
1376
+ }
1377
+
1378
+ export async function resetTestDB() {
1379
+ // Limpiar todas las tablas para cada test
1380
+ await db.delete(schema.users);
1381
+ await db.delete(schema.tasks);
1382
+ await db.delete(schema.sessions);
1383
+ }
1384
+
1385
+ async function runMigrations(db: any) {
1386
+ // Aplicar migraciones desde carpeta migrations/
1387
+ const { migrate } = await import('drizzle-orm/postgres-js/migrator');
1388
+ await migrate(db, { migrationsFolder: './migrations' });
1389
+ }
1390
+ ```
1391
+
1392
+ ### Ejemplo de Test con DB Real
1393
+
1394
+ ```typescript
1395
+ // tests/integration/user-repository.test.ts
1396
+ import { test, expect, beforeAll, afterAll, beforeEach, describe } from 'bun:test';
1397
+ import { setupTestDB, teardownTestDB, resetTestDB } from './setup';
1398
+ import { UserRepository } from '@/repositories/UserRepository';
1399
+
1400
+ let db: any;
1401
+ let userRepo: UserRepository;
1402
+
1403
+ beforeAll(async () => {
1404
+ const setup = await setupTestDB();
1405
+ db = setup.db;
1406
+ userRepo = new UserRepository(db);
1407
+ }, 30000); // Timeout mรกs alto para container startup
1408
+
1409
+ afterAll(async () => {
1410
+ await teardownTestDB();
1411
+ });
1412
+
1413
+ beforeEach(async () => {
1414
+ await resetTestDB(); // Fresh DB para cada test
1415
+ });
1416
+
1417
+ describe('UserRepository', () => {
1418
+ test('create inserts user into database', async () => {
1419
+ // Arrange
1420
+ const userData = {
1421
+ email: 'test@example.com',
1422
+ name: 'Test User',
1423
+ password: 'hashed_password_here',
1424
+ };
1425
+
1426
+ // Act
1427
+ const user = await userRepo.create(userData);
1428
+
1429
+ // Assert
1430
+ expect(user.id).toBeDefined();
1431
+ expect(user.email).toBe('test@example.com');
1432
+ expect(user.name).toBe('Test User');
1433
+
1434
+ // Verify en DB REAL (no mock!)
1435
+ const found = await userRepo.findById(user.id);
1436
+ expect(found).toBeDefined();
1437
+ expect(found!.email).toBe('test@example.com');
1438
+ });
1439
+
1440
+ test('findByEmail returns null for non-existent user', async () => {
1441
+ const user = await userRepo.findByEmail('notfound@example.com');
1442
+ expect(user).toBeNull();
1443
+ });
1444
+
1445
+ test('findByEmail returns user when exists', async () => {
1446
+ // Arrange - Crear usuario primero
1447
+ await userRepo.create({
1448
+ email: 'exists@example.com',
1449
+ name: 'Exists',
1450
+ password: 'hashed',
1451
+ });
1452
+
1453
+ // Act
1454
+ const found = await userRepo.findByEmail('exists@example.com');
1455
+
1456
+ // Assert
1457
+ expect(found).toBeDefined();
1458
+ expect(found!.name).toBe('Exists');
1459
+ });
1460
+
1461
+ test('update modifies user data', async () => {
1462
+ // Arrange
1463
+ const user = await userRepo.create({
1464
+ email: 'test@example.com',
1465
+ name: 'Original Name',
1466
+ password: 'pass',
1467
+ });
1468
+
1469
+ // Act
1470
+ const updated = await userRepo.update(user.id, {
1471
+ name: 'Updated Name',
1472
+ });
1473
+
1474
+ // Assert
1475
+ expect(updated.name).toBe('Updated Name');
1476
+ expect(updated.email).toBe('test@example.com'); // No cambiรณ
1477
+
1478
+ // Verify en DB
1479
+ const verified = await userRepo.findById(user.id);
1480
+ expect(verified!.name).toBe('Updated Name');
1481
+ });
1482
+
1483
+ test('delete removes user from database', async () => {
1484
+ // Arrange
1485
+ const user = await userRepo.create({
1486
+ email: 'delete@example.com',
1487
+ name: 'To Delete',
1488
+ password: 'pass',
1489
+ });
1490
+
1491
+ // Act
1492
+ await userRepo.delete(user.id);
1493
+
1494
+ // Assert
1495
+ const found = await userRepo.findById(user.id);
1496
+ expect(found).toBeNull();
1497
+ });
1498
+
1499
+ test('query performance is acceptable', async () => {
1500
+ // Arrange - Crear 100 usuarios
1501
+ const users = await Promise.all(
1502
+ Array.from({ length: 100 }, (_, i) =>
1503
+ userRepo.create({
1504
+ email: `user${i}@example.com`,
1505
+ name: `User ${i}`,
1506
+ password: 'pass',
1507
+ })
1508
+ )
1509
+ );
1510
+
1511
+ // Act - Buscar todos
1512
+ const start = Date.now();
1513
+ const allUsers = await userRepo.findAll();
1514
+ const duration = Date.now() - start;
1515
+
1516
+ // Assert
1517
+ expect(allUsers.length).toBe(100);
1518
+ expect(duration).toBeLessThan(100); // Menos de 100ms
1519
+ });
1520
+ });
1521
+ ```
1522
+
1523
+ ---
1524
+
1525
+ ## Python + pytest + PostgreSQL + Testcontainers
1526
+
1527
+ ### Instalaciรณn
1528
+
1529
+ ```bash
1530
+ uv add --dev testcontainers pytest-asyncio
1531
+ ```
1532
+
1533
+ ### Setup
1534
+
1535
+ ```python
1536
+ # tests/integration/conftest.py
1537
+ import pytest
1538
+ from testcontainers.postgres import PostgresContainer
1539
+ from sqlalchemy import create_engine
1540
+ from sqlalchemy.orm import sessionmaker
1541
+ from app.models import Base
1542
+
1543
+ @pytest.fixture(scope="session")
1544
+ def postgres_container():
1545
+ """
1546
+ Start PostgreSQL container (reused across all tests in session)
1547
+ """
1548
+ with PostgresContainer("postgres:16-alpine") as postgres:
1549
+ yield postgres
1550
+
1551
+ @pytest.fixture(scope="session")
1552
+ def engine(postgres_container):
1553
+ """
1554
+ Create SQLAlchemy engine
1555
+ """
1556
+ engine = create_engine(postgres_container.get_connection_url())
1557
+
1558
+ # Create all tables
1559
+ Base.metadata.create_all(engine)
1560
+
1561
+ return engine
1562
+
1563
+ @pytest.fixture(scope="function")
1564
+ def db_session(engine):
1565
+ """
1566
+ Fresh database session for each test
1567
+ """
1568
+ Session = sessionmaker(bind=engine)
1569
+ session = Session()
1570
+
1571
+ try:
1572
+ yield session
1573
+ finally:
1574
+ session.rollback()
1575
+ session.close()
1576
+
1577
+ # Limpiar todas las tablas para next test
1578
+ for table in reversed(Base.metadata.sorted_tables):
1579
+ engine.execute(table.delete())
1580
+ ```
1581
+
1582
+ ### Ejemplo de Test
1583
+
1584
+ ```python
1585
+ # tests/integration/test_user_repository.py
1586
+ import pytest
1587
+ from app.repositories.user_repository import UserRepository
1588
+ from app.models.user import User
1589
+
1590
+ class TestUserRepository:
1591
+ """Integration tests para UserRepository con DB real"""
1592
+
1593
+ def test_create_inserts_user_into_database(self, db_session):
1594
+ # Arrange
1595
+ repo = UserRepository(db_session)
1596
+ user_data = {
1597
+ "email": "test@example.com",
1598
+ "name": "Test User",
1599
+ "password": "hashed_password"
1600
+ }
1601
+
1602
+ # Act
1603
+ user = repo.create(**user_data)
1604
+ db_session.commit()
1605
+
1606
+ # Assert
1607
+ assert user.id is not None
1608
+ assert user.email == "test@example.com"
1609
+
1610
+ # Verify en DB REAL
1611
+ found = repo.find_by_id(user.id)
1612
+ assert found is not None
1613
+ assert found.name == "Test User"
1614
+
1615
+ def test_find_by_email_returns_none_for_nonexistent(self, db_session):
1616
+ repo = UserRepository(db_session)
1617
+ user = repo.find_by_email("notfound@example.com")
1618
+ assert user is None
1619
+
1620
+ def test_find_by_email_returns_user_when_exists(self, db_session):
1621
+ # Arrange
1622
+ repo = UserRepository(db_session)
1623
+ created = repo.create(
1624
+ email="exists@example.com",
1625
+ name="Exists",
1626
+ password="hashed"
1627
+ )
1628
+ db_session.commit()
1629
+
1630
+ # Act
1631
+ found = repo.find_by_email("exists@example.com")
1632
+
1633
+ # Assert
1634
+ assert found is not None
1635
+ assert found.id == created.id
1636
+ assert found.name == "Exists"
1637
+
1638
+ def test_update_modifies_user_data(self, db_session):
1639
+ # Arrange
1640
+ repo = UserRepository(db_session)
1641
+ user = repo.create(
1642
+ email="test@example.com",
1643
+ name="Original",
1644
+ password="pass"
1645
+ )
1646
+ db_session.commit()
1647
+
1648
+ # Act
1649
+ updated = repo.update(user.id, name="Updated")
1650
+ db_session.commit()
1651
+
1652
+ # Assert
1653
+ assert updated.name == "Updated"
1654
+ assert updated.email == "test@example.com"
1655
+
1656
+ @pytest.mark.parametrize("count", [10, 50, 100])
1657
+ def test_bulk_operations_performance(self, db_session, count):
1658
+ """Test performance con diferentes volรบmenes"""
1659
+ import time
1660
+
1661
+ repo = UserRepository(db_session)
1662
+
1663
+ # Arrange - Crear mรบltiples usuarios
1664
+ start = time.time()
1665
+ users = [
1666
+ repo.create(
1667
+ email=f"user{i}@example.com",
1668
+ name=f"User {i}",
1669
+ password="pass"
1670
+ )
1671
+ for i in range(count)
1672
+ ]
1673
+ db_session.commit()
1674
+ duration = time.time() - start
1675
+
1676
+ # Assert
1677
+ assert len(users) == count
1678
+ assert duration < (count * 0.01) # < 10ms por usuario
1679
+ ```
1680
+
1681
+ ---
1682
+
1683
+ ## Go + testcontainers-go
1684
+
1685
+ ### Instalaciรณn
1686
+
1687
+ ```bash
1688
+ go get github.com/testcontainers/testcontainers-go
1689
+ go get github.com/testcontainers/testcontainers-go/modules/postgres
1690
+ ```
1691
+
1692
+ ### Setup
1693
+
1694
+ ```go
1695
+ // internal/repository/repository_test.go
1696
+ package repository
1697
+
1698
+ import (
1699
+ "context"
1700
+ "database/sql"
1701
+ "testing"
1702
+ "time"
1703
+
1704
+ "github.com/stretchr/testify/assert"
1705
+ "github.com/stretchr/testify/require"
1706
+ "github.com/testcontainers/testcontainers-go"
1707
+ "github.com/testcontainers/testcontainers-go/modules/postgres"
1708
+ "github.com/testcontainers/testcontainers-go/wait"
1709
+ )
1710
+
1711
+ var (
1712
+ testDB *sql.DB
1713
+ repo *UserRepository
1714
+ )
1715
+
1716
+ func TestMain(m *testing.M) {
1717
+ ctx := context.Background()
1718
+
1719
+ // Start PostgreSQL container
1720
+ pgContainer, err := postgres.RunContainer(ctx,
1721
+ testcontainers.WithImage("postgres:16-alpine"),
1722
+ postgres.WithDatabase("testdb"),
1723
+ postgres.WithUsername("test"),
1724
+ postgres.WithPassword("test"),
1725
+ testcontainers.WithWaitStrategy(
1726
+ wait.ForLog("database system is ready to accept connections").
1727
+ WithOccurrence(2).
1728
+ WithStartupTimeout(5*time.Second),
1729
+ ),
1730
+ )
1731
+ if err != nil {
1732
+ panic(err)
1733
+ }
1734
+ defer pgContainer.Terminate(ctx)
1735
+
1736
+ // Connect to database
1737
+ connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
1738
+ if err != nil {
1739
+ panic(err)
1740
+ }
1741
+
1742
+ testDB, err = sql.Open("postgres", connStr)
1743
+ if err != nil {
1744
+ panic(err)
1745
+ }
1746
+
1747
+ // Run migrations
1748
+ if err := runMigrations(testDB); err != nil {
1749
+ panic(err)
1750
+ }
1751
+
1752
+ // Initialize repository
1753
+ repo = NewUserRepository(testDB)
1754
+
1755
+ // Run tests
1756
+ code := m.Run()
1757
+
1758
+ // Cleanup
1759
+ testDB.Close()
1760
+ os.Exit(code)
1761
+ }
1762
+
1763
+ func resetDB(t *testing.T) {
1764
+ _, err := testDB.Exec("TRUNCATE users, tasks CASCADE")
1765
+ require.NoError(t, err)
1766
+ }
1767
+ ```
1768
+
1769
+ ### Ejemplo de Test
1770
+
1771
+ ```go
1772
+ // internal/repository/user_repository_test.go
1773
+ package repository
1774
+
1775
+ import (
1776
+ "context"
1777
+ "testing"
1778
+
1779
+ "github.com/stretchr/testify/assert"
1780
+ "github.com/stretchr/testify/require"
1781
+ )
1782
+
1783
+ func TestUserRepository_Create(t *testing.T) {
1784
+ resetDB(t)
1785
+ ctx := context.Background()
1786
+
1787
+ // Arrange
1788
+ user := &User{
1789
+ Email: "test@example.com",
1790
+ Name: "Test User",
1791
+ Password: "hashed_password",
1792
+ }
1793
+
1794
+ // Act
1795
+ err := repo.Create(ctx, user)
1796
+
1797
+ // Assert
1798
+ assert.NoError(t, err)
1799
+ assert.NotZero(t, user.ID)
1800
+
1801
+ // Verify en DB REAL
1802
+ found, err := repo.FindByID(ctx, user.ID)
1803
+ require.NoError(t, err)
1804
+ assert.Equal(t, "test@example.com", found.Email)
1805
+ assert.Equal(t, "Test User", found.Name)
1806
+ }
1807
+
1808
+ func TestUserRepository_FindByEmail(t *testing.T) {
1809
+ resetDB(t)
1810
+ ctx := context.Background()
1811
+
1812
+ t.Run("returns nil for non-existent user", func(t *testing.T) {
1813
+ user, err := repo.FindByEmail(ctx, "notfound@example.com")
1814
+ assert.NoError(t, err)
1815
+ assert.Nil(t, user)
1816
+ })
1817
+
1818
+ t.Run("returns user when exists", func(t *testing.T) {
1819
+ // Arrange
1820
+ created := &User{
1821
+ Email: "exists@example.com",
1822
+ Name: "Exists",
1823
+ Password: "pass",
1824
+ }
1825
+ require.NoError(t, repo.Create(ctx, created))
1826
+
1827
+ // Act
1828
+ found, err := repo.FindByEmail(ctx, "exists@example.com")
1829
+
1830
+ // Assert
1831
+ assert.NoError(t, err)
1832
+ assert.NotNil(t, found)
1833
+ assert.Equal(t, created.ID, found.ID)
1834
+ })
1835
+ }
1836
+
1837
+ func BenchmarkUserRepository_Create(b *testing.B) {
1838
+ resetDB(&testing.T{})
1839
+ ctx := context.Background()
1840
+
1841
+ b.ResetTimer()
1842
+ for i := 0; i < b.N; i++ {
1843
+ user := &User{
1844
+ Email: fmt.Sprintf("bench%d@example.com", i),
1845
+ Name: "Bench User",
1846
+ Password: "pass",
1847
+ }
1848
+ _ = repo.Create(ctx, user)
1849
+ }
1850
+ }
1851
+ ```
1852
+
1853
+ ---
1854
+
1855
+ ## ๐ŸŽฏ Mise Tasks para Testcontainers
1856
+
1857
+ ```toml
1858
+ # .mise.toml
1859
+
1860
+ [tasks."test:integration:tc"]
1861
+ description = "Run integration tests with Testcontainers"
1862
+ run = """
1863
+ #!/usr/bin/env bash
1864
+
1865
+ # Verificar que Docker estรก corriendo
1866
+ if ! docker info > /dev/null 2>&1; then
1867
+ echo "โŒ Docker no estรก corriendo"
1868
+ echo "Por favor inicia Docker Desktop"
1869
+ exit 1
1870
+ fi
1871
+
1872
+ echo "๐Ÿณ Running integration tests with Testcontainers..."
1873
+ echo "(This may take 5-10 seconds on first run to download images)"
1874
+ echo ""
1875
+
1876
+ if [ -f "package.json" ]; then
1877
+ bun test tests/integration/
1878
+ elif [ -f "pyproject.toml" ]; then
1879
+ pytest tests/integration/ -v --tb=short
1880
+ elif [ -f "go.mod" ]; then
1881
+ go test ./internal/... -tags=integration -v
1882
+ fi
1883
+
1884
+ echo ""
1885
+ echo "โœ… Integration tests with Testcontainers completed"
1886
+ """
1887
+
1888
+ [tasks."test:integration:tc:watch"]
1889
+ description = "Watch mode for integration tests"
1890
+ run = """
1891
+ if [ -f "package.json" ]; then
1892
+ bun test --watch tests/integration/
1893
+ elif [ -f "pyproject.toml" ]; then
1894
+ ptw tests/integration/ -- -v
1895
+ fi
1896
+ """
1897
+ ```
1898
+
1899
+ ---
1900
+
1901
+ ## โšก Performance Tips
1902
+
1903
+ ### 1. Reuse Containers (MรS IMPORTANTE)
1904
+
1905
+ ```typescript
1906
+ // โœ… CON REUSE: ~1-2 segundos por test suite
1907
+ const container = await new PostgreSqlContainer()
1908
+ .withReuse() // โ† CRรTICO
1909
+ .start();
1910
+
1911
+ // โŒ SIN REUSE: ~5-10 segundos por test suite
1912
+ ```
1913
+
1914
+ ### 2. Use tmpfs para DB en RAM
1915
+
1916
+ ```typescript
1917
+ const container = await new PostgreSqlContainer()
1918
+ .withTmpFs({ '/var/lib/postgresql/data': 'rw' }) // RAM disk
1919
+ .start();
1920
+
1921
+ // 2-3x mรกs rรกpido para tests
1922
+ ```
1923
+
1924
+ ### 3. Parallel Test Execution
1925
+
1926
+ ```bash
1927
+ # Bun (paralelo por defecto)
1928
+ bun test --concurrent
1929
+
1930
+ # Pytest
1931
+ pytest -n auto # Usa todos los cores
1932
+
1933
+ # Go
1934
+ go test -parallel 4 ./...
1935
+ ```
1936
+
1937
+ ### 4. Cleanup Eficiente
1938
+
1939
+ ```typescript
1940
+ // โœ… Mejor: Truncar tablas (rรกpido)
1941
+ beforeEach(async () => {
1942
+ await db.delete(schema.users);
1943
+ await db.delete(schema.tasks);
1944
+ });
1945
+
1946
+ // โŒ Lento: Recrear DB entera
1947
+ beforeEach(async () => {
1948
+ await dropDatabase();
1949
+ await createDatabase();
1950
+ await runMigrations();
1951
+ });
1952
+ ```
1953
+
1954
+ ---
1955
+
1956
+ ## ๐Ÿ“Š Benchmarks Reales
1957
+
1958
+ ```
1959
+ Setup (primera vez):
1960
+ Download image: ~30 segundos
1961
+ Start container: ~3 segundos
1962
+ Apply migrations: ~1 segundo
1963
+ Total: ~34 segundos (solo primera vez)
1964
+
1965
+ Subsequent runs (con reuse):
1966
+ Start container: ~1 segundo
1967
+ Apply migrations: ~0.5 segundos
1968
+ Per test: ~50-100ms
1969
+ Total suite (50): ~5 segundos
1970
+
1971
+ Sin Testcontainers (mocks):
1972
+ Setup: 0 segundos
1973
+ Per test: ~5ms
1974
+ Total suite (50): ~250ms
1975
+
1976
+ Trade-off:
1977
+ โœ… 20x mรกs lento pero 100x mรกs confianza
1978
+ โœ… Detecta bugs reales que mocks no detectan
1979
+ โœ… Menos mantenimiento a largo plazo
1980
+ ```
1981
+
1982
+ ---
1983
+
1984
+ ## ๐Ÿ—„๏ธ Database Migrations Strategy
1985
+
1986
+ ### Filosofรญa de Migraciones
1987
+
1988
+ ```
1989
+ โœ… DO:
1990
+ - Migraciones son cรณdigo (versiรณn controlada)
1991
+ - Siempre hacia adelante (no editar migraciones existentes)
1992
+ - Rollback strategy clara
1993
+ - Probar en staging primero
1994
+
1995
+ โŒ DON'T:
1996
+ - Editar migraciones despuรฉs de merge
1997
+ - Rollback manual en producciรณn
1998
+ - Migrations que dependen de datos
1999
+ ```
2000
+
2001
+ ---
2002
+
2003
+ ## TypeScript: Drizzle ORM (Recomendado)
2004
+
2005
+ ### Setup
2006
+
2007
+ ```bash
2008
+ # Instalar
2009
+ bun add drizzle-orm postgres
2010
+ bun add -d drizzle-kit
2011
+ ```
2012
+
2013
+ ```typescript
2014
+ // drizzle.config.ts
2015
+ import type { Config } from 'drizzle-kit';
2016
+
2017
+ export default {
2018
+ schema: './src/db/schema.ts',
2019
+ out: './migrations',
2020
+ driver: 'pg',
2021
+ dbCredentials: {
2022
+ connectionString: process.env.DATABASE_URL!,
2023
+ },
2024
+ } satisfies Config;
2025
+ ```
2026
+
2027
+ ### Schema Definition
2028
+
2029
+ ```typescript
2030
+ // src/db/schema.ts
2031
+ import { pgTable, serial, text, timestamp, boolean, integer } from 'drizzle-orm/pg-core';
2032
+
2033
+ export const users = pgTable('users', {
2034
+ id: serial('id').primaryKey(),
2035
+ email: text('email').notNull().unique(),
2036
+ name: text('name'),
2037
+ password: text('password').notNull(),
2038
+ emailVerified: boolean('email_verified').default(false),
2039
+ createdAt: timestamp('created_at').defaultNow(),
2040
+ updatedAt: timestamp('updated_at').defaultNow(),
2041
+ });
2042
+
2043
+ export const tasks = pgTable('tasks', {
2044
+ id: serial('id').primaryKey(),
2045
+ title: text('title').notNull(),
2046
+ description: text('description'),
2047
+ completed: boolean('completed').default(false),
2048
+ userId: integer('user_id')
2049
+ .references(() => users.id, { onDelete: 'cascade' })
2050
+ .notNull(),
2051
+ createdAt: timestamp('created_at').defaultNow(),
2052
+ updatedAt: timestamp('updated_at').defaultNow(),
2053
+ });
2054
+
2055
+ // Type inference
2056
+ export type User = typeof users.$inferSelect;
2057
+ export type NewUser = typeof users.$inferInsert;
2058
+ export type Task = typeof tasks.$inferSelect;
2059
+ export type NewTask = typeof tasks.$inferInsert;
2060
+ ```
2061
+
2062
+ ### Client Setup
2063
+
2064
+ ```typescript
2065
+ // src/db/index.ts
2066
+ import { drizzle } from 'drizzle-orm/postgres-js';
2067
+ import postgres from 'postgres';
2068
+ import * as schema from './schema';
2069
+
2070
+ const connectionString = process.env.DATABASE_URL!;
2071
+
2072
+ // Para queries
2073
+ const queryClient = postgres(connectionString);
2074
+ export const db = drizzle(queryClient, { schema });
2075
+
2076
+ // Para migrations
2077
+ export const migrationClient = postgres(connectionString, { max: 1 });
2078
+ ```
2079
+
2080
+ ### Workflow de Migraciones
2081
+
2082
+ ```bash
2083
+ # 1. Cambiar schema.ts (agregar columna, tabla, etc.)
2084
+
2085
+ # 2. Generar migration
2086
+ mise run db:generate
2087
+
2088
+ # 3. Revisar SQL generado en migrations/
2089
+ # migrations/0001_add_email_verified.sql
2090
+
2091
+ # 4. Aplicar migration
2092
+ mise run db:migrate
2093
+
2094
+ # 5. Rollback si algo falla (manual)
2095
+ # Editar migration SQL o crear nueva para revertir
2096
+ ```
2097
+
2098
+ ### Migrations con Datos
2099
+
2100
+ ```typescript
2101
+ // migrations/0002_seed_default_roles.ts
2102
+ import { db } from '../src/db';
2103
+ import { roles } from '../src/db/schema';
2104
+
2105
+ export async function up() {
2106
+ await db.insert(roles).values([
2107
+ { name: 'admin', permissions: ['all'] },
2108
+ { name: 'user', permissions: ['read', 'write'] },
2109
+ { name: 'guest', permissions: ['read'] },
2110
+ ]);
2111
+ }
2112
+
2113
+ export async function down() {
2114
+ await db.delete(roles);
2115
+ }
2116
+ ```
2117
+
2118
+ ---
2119
+
2120
+ ## Python: Alembic (con SQLAlchemy)
2121
+
2122
+ ### Setup
2123
+
2124
+ ```bash
2125
+ # Instalar
2126
+ uv add alembic sqlalchemy psycopg2-binary
2127
+
2128
+ # Inicializar
2129
+ alembic init migrations
2130
+ ```
2131
+
2132
+ ### Configuration
2133
+
2134
+ ```python
2135
+ # alembic.ini
2136
+ [alembic]
2137
+ script_location = migrations
2138
+ sqlalchemy.url = driver://user:pass@localhost/dbname
2139
+
2140
+ # Use env variable
2141
+ # sqlalchemy.url =
2142
+
2143
+ # migrations/env.py
2144
+ from logging.config import fileConfig
2145
+ from sqlalchemy import engine_from_config, pool
2146
+ from alembic import context
2147
+ import os
2148
+
2149
+ # Import your models
2150
+ from app.models import Base
2151
+
2152
+ config = context.config
2153
+
2154
+ # Override sqlalchemy.url from environment
2155
+ config.set_main_option(
2156
+ 'sqlalchemy.url',
2157
+ os.getenv('DATABASE_URL')
2158
+ )
2159
+
2160
+ target_metadata = Base.metadata
2161
+
2162
+ def run_migrations_online():
2163
+ connectable = engine_from_config(
2164
+ config.get_section(config.config_ini_section),
2165
+ prefix='sqlalchemy.',
2166
+ poolclass=pool.NullPool,
2167
+ )
2168
+
2169
+ with connectable.connect() as connection:
2170
+ context.configure(
2171
+ connection=connection,
2172
+ target_metadata=target_metadata
2173
+ )
2174
+
2175
+ with context.begin_transaction():
2176
+ context.run_migrations()
2177
+
2178
+ run_migrations_online()
2179
+ ```
2180
+
2181
+ ### Models Definition
2182
+
2183
+ ```python
2184
+ # app/models/user.py
2185
+ from sqlalchemy import Column, Integer, String, Boolean, DateTime
2186
+ from sqlalchemy.sql import func
2187
+ from app.database import Base
2188
+
2189
+ class User(Base):
2190
+ __tablename__ = "users"
2191
+
2192
+ id = Column(Integer, primary_key=True, index=True)
2193
+ email = Column(String, unique=True, index=True, nullable=False)
2194
+ name = Column(String)
2195
+ password = Column(String, nullable=False)
2196
+ email_verified = Column(Boolean, default=False)
2197
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
2198
+ updated_at = Column(DateTime(timezone=True), onupdate=func.now())
2199
+
2200
+ # Relationships
2201
+ tasks = relationship("Task", back_populates="user", cascade="all, delete-orphan")
2202
+ ```
2203
+
2204
+ ### Workflow de Migraciones
2205
+
2206
+ ```bash
2207
+ # 1. Modificar models en app/models/
2208
+
2209
+ # 2. Generar migration automรกtica
2210
+ alembic revision --autogenerate -m "add email_verified column"
2211
+
2212
+ # 3. Revisar migration generada
2213
+ # migrations/versions/xxxx_add_email_verified.py
2214
+
2215
+ # 4. Aplicar migration
2216
+ alembic upgrade head
2217
+
2218
+ # 5. Rollback si es necesario
2219
+ alembic downgrade -1
2220
+ ```
2221
+
2222
+ ### Migration Example
2223
+
2224
+ ```python
2225
+ # migrations/versions/0001_create_users_table.py
2226
+ """create users table
2227
+
2228
+ Revision ID: 0001
2229
+ Revises:
2230
+ Create Date: 2025-12-23
2231
+ """
2232
+ from alembic import op
2233
+ import sqlalchemy as sa
2234
+
2235
+ revision = '0001'
2236
+ down_revision = None
2237
+ branch_labels = None
2238
+ depends_on = None
2239
+
2240
+ def upgrade():
2241
+ op.create_table(
2242
+ 'users',
2243
+ sa.Column('id', sa.Integer(), nullable=False),
2244
+ sa.Column('email', sa.String(), nullable=False),
2245
+ sa.Column('name', sa.String()),
2246
+ sa.Column('password', sa.String(), nullable=False),
2247
+ sa.Column('email_verified', sa.Boolean(), server_default='false'),
2248
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()')),
2249
+ sa.Column('updated_at', sa.DateTime(timezone=True)),
2250
+ sa.PrimaryKeyConstraint('id')
2251
+ )
2252
+ op.create_index('ix_users_email', 'users', ['email'], unique=True)
2253
+
2254
+ def downgrade():
2255
+ op.drop_index('ix_users_email', table_name='users')
2256
+ op.drop_table('users')
2257
+ ```
2258
+
2259
+ ---
2260
+
2261
+ ## Go: golang-migrate
2262
+
2263
+ ### Setup
2264
+
2265
+ ```bash
2266
+ # Instalar CLI
2267
+ brew install golang-migrate
2268
+
2269
+ # O como Go tool
2270
+ go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
2271
+ ```
2272
+
2273
+ ### Estructura
2274
+
2275
+ ```
2276
+ migrations/
2277
+ โ”œโ”€โ”€ 000001_create_users_table.up.sql
2278
+ โ”œโ”€โ”€ 000001_create_users_table.down.sql
2279
+ โ”œโ”€โ”€ 000002_create_tasks_table.up.sql
2280
+ โ””โ”€โ”€ 000002_create_tasks_table.down.sql
2281
+ ```
2282
+
2283
+ ### Migration Files
2284
+
2285
+ ```sql
2286
+ -- migrations/000001_create_users_table.up.sql
2287
+ CREATE TABLE IF NOT EXISTS users (
2288
+ id SERIAL PRIMARY KEY,
2289
+ email VARCHAR(255) NOT NULL UNIQUE,
2290
+ name VARCHAR(255),
2291
+ password VARCHAR(255) NOT NULL,
2292
+ email_verified BOOLEAN DEFAULT FALSE,
2293
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
2294
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
2295
+ );
2296
+
2297
+ CREATE INDEX idx_users_email ON users(email);
2298
+
2299
+ -- migrations/000001_create_users_table.down.sql
2300
+ DROP INDEX IF EXISTS idx_users_email;
2301
+ DROP TABLE IF EXISTS users;
2302
+ ```
2303
+
2304
+ ```sql
2305
+ -- migrations/000002_create_tasks_table.up.sql
2306
+ CREATE TABLE IF NOT EXISTS tasks (
2307
+ id SERIAL PRIMARY KEY,
2308
+ title VARCHAR(255) NOT NULL,
2309
+ description TEXT,
2310
+ completed BOOLEAN DEFAULT FALSE,
2311
+ user_id INTEGER NOT NULL,
2312
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
2313
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
2314
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
2315
+ );
2316
+
2317
+ CREATE INDEX idx_tasks_user_id ON tasks(user_id);
2318
+
2319
+ -- migrations/000002_create_tasks_table.down.sql
2320
+ DROP INDEX IF EXISTS idx_tasks_user_id;
2321
+ DROP TABLE IF EXISTS tasks;
2322
+ ```
2323
+
2324
+ ### Programmatic Usage
2325
+
2326
+ ```go
2327
+ // internal/database/migrate.go
2328
+ package database
2329
+
2330
+ import (
2331
+ "database/sql"
2332
+ "github.com/golang-migrate/migrate/v4"
2333
+ "github.com/golang-migrate/migrate/v4/database/postgres"
2334
+ _ "github.com/golang-migrate/migrate/v4/source/file"
2335
+ )
2336
+
2337
+ func RunMigrations(db *sql.DB) error {
2338
+ driver, err := postgres.WithInstance(db, &postgres.Config{})
2339
+ if err != nil {
2340
+ return err
2341
+ }
2342
+
2343
+ m, err := migrate.NewWithDatabaseInstance(
2344
+ "file://migrations",
2345
+ "postgres",
2346
+ driver,
2347
+ )
2348
+ if err != nil {
2349
+ return err
2350
+ }
2351
+
2352
+ if err := m.Up(); err != nil && err != migrate.ErrNoChange {
2353
+ return err
2354
+ }
2355
+
2356
+ return nil
2357
+ }
2358
+ ```
2359
+
2360
+ ### Workflow
2361
+
2362
+ ```bash
2363
+ # Crear nueva migration
2364
+ migrate create -ext sql -dir migrations -seq create_tasks_table
2365
+
2366
+ # Aplicar migrations
2367
+ migrate -path migrations -database "$DATABASE_URL" up
2368
+
2369
+ # Rollback
2370
+ migrate -path migrations -database "$DATABASE_URL" down 1
2371
+
2372
+ # Ver estado
2373
+ migrate -path migrations -database "$DATABASE_URL" version
2374
+ ```
2375
+
2376
+ ---
2377
+
2378
+ ## Java/Kotlin: Flyway
2379
+
2380
+ ### Setup (Gradle)
2381
+
2382
+ ```kotlin
2383
+ // build.gradle.kts
2384
+ plugins {
2385
+ id("org.flywaydb.flyway") version "10.4.1"
2386
+ }
2387
+
2388
+ dependencies {
2389
+ implementation("org.flywaydb:flyway-core:10.4.1")
2390
+ implementation("org.flywaydb:flyway-database-postgresql:10.4.1")
2391
+ }
2392
+
2393
+ flyway {
2394
+ url = "jdbc:postgresql://localhost:5432/mydb"
2395
+ user = "postgres"
2396
+ password = "postgres"
2397
+ locations = arrayOf("classpath:db/migration")
2398
+ }
2399
+ ```
2400
+
2401
+ ### Migration Files
2402
+
2403
+ ```
2404
+ src/main/resources/db/migration/
2405
+ โ”œโ”€โ”€ V1__create_users_table.sql
2406
+ โ”œโ”€โ”€ V2__create_tasks_table.sql
2407
+ โ””โ”€โ”€ V3__add_email_verified.sql
2408
+ ```
2409
+
2410
+ ```sql
2411
+ -- V1__create_users_table.sql
2412
+ CREATE TABLE users (
2413
+ id SERIAL PRIMARY KEY,
2414
+ email VARCHAR(255) NOT NULL UNIQUE,
2415
+ name VARCHAR(255),
2416
+ password VARCHAR(255) NOT NULL,
2417
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
2418
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
2419
+ );
2420
+
2421
+ CREATE INDEX idx_users_email ON users(email);
2422
+ ```
2423
+
2424
+ ### Programmatic Usage
2425
+
2426
+ ```kotlin
2427
+ // src/main/kotlin/com/example/config/DatabaseConfig.kt
2428
+ import org.flywaydb.core.Flyway
2429
+ import org.springframework.context.annotation.Bean
2430
+ import org.springframework.context.annotation.Configuration
2431
+ import javax.sql.DataSource
2432
+
2433
+ @Configuration
2434
+ class DatabaseConfig {
2435
+
2436
+ @Bean
2437
+ fun flyway(dataSource: DataSource): Flyway {
2438
+ val flyway = Flyway.configure()
2439
+ .dataSource(dataSource)
2440
+ .locations("classpath:db/migration")
2441
+ .load()
2442
+
2443
+ flyway.migrate()
2444
+ return flyway
2445
+ }
2446
+ }
2447
+ ```
2448
+
2449
+ ---
2450
+
2451
+ ## ๐ŸŽฏ Mise Tasks Universales para Migraciones
2452
+
2453
+ ```toml
2454
+ # .mise.toml
2455
+
2456
+ [tasks."db:generate"]
2457
+ description = "Generate new migration"
2458
+ run = """
2459
+ #!/usr/bin/env bash
2460
+ set -e
2461
+
2462
+ if [ -f "drizzle.config.ts" ]; then
2463
+ # TypeScript + Drizzle
2464
+ echo "๐Ÿ“ Generating Drizzle migration..."
2465
+ bun drizzle-kit generate:pg
2466
+
2467
+ elif [ -f "alembic.ini" ]; then
2468
+ # Python + Alembic
2469
+ echo "๐Ÿ“ Generating Alembic migration..."
2470
+ read -p "Migration message: " message
2471
+ alembic revision --autogenerate -m "$message"
2472
+
2473
+ elif [ -f "go.mod" ]; then
2474
+ # Go + golang-migrate
2475
+ echo "๐Ÿ“ Creating golang-migrate migration..."
2476
+ read -p "Migration name: " name
2477
+ migrate create -ext sql -dir migrations -seq "$name"
2478
+
2479
+ elif [ -f "build.gradle.kts" ]; then
2480
+ # Java + Flyway
2481
+ echo "๐Ÿ“ Creating Flyway migration..."
2482
+ read -p "Migration name: " name
2483
+ touch "src/main/resources/db/migration/V$(date +%Y%m%d%H%M%S)__${name}.sql"
2484
+ fi
2485
+
2486
+ echo "โœ… Migration generated. Review before applying!"
2487
+ """
2488
+
2489
+ [tasks."db:migrate"]
2490
+ description = "Apply pending migrations"
2491
+ run = """
2492
+ #!/usr/bin/env bash
2493
+ set -e
2494
+
2495
+ if [ -f "drizzle.config.ts" ]; then
2496
+ echo "๐Ÿš€ Applying Drizzle migrations..."
2497
+ bun drizzle-kit push:pg
2498
+
2499
+ elif [ -f "alembic.ini" ]; then
2500
+ echo "๐Ÿš€ Applying Alembic migrations..."
2501
+ alembic upgrade head
2502
+
2503
+ elif [ -f "go.mod" ]; then
2504
+ echo "๐Ÿš€ Applying golang-migrate migrations..."
2505
+ migrate -path migrations -database "$DATABASE_URL" up
2506
+
2507
+ elif [ -f "build.gradle.kts" ]; then
2508
+ echo "๐Ÿš€ Applying Flyway migrations..."
2509
+ ./gradlew flywayMigrate
2510
+ fi
2511
+
2512
+ echo "โœ… Migrations applied successfully!"
2513
+ """
2514
+
2515
+ [tasks."db:rollback"]
2516
+ description = "Rollback last migration"
2517
+ run = """
2518
+ #!/usr/bin/env bash
2519
+ set -e
2520
+
2521
+ echo "โš ๏ธ Rolling back last migration..."
2522
+
2523
+ if [ -f "alembic.ini" ]; then
2524
+ alembic downgrade -1
2525
+
2526
+ elif [ -f "go.mod" ]; then
2527
+ migrate -path migrations -database "$DATABASE_URL" down 1
2528
+
2529
+ elif [ -f "build.gradle.kts" ]; then
2530
+ ./gradlew flywayUndo
2531
+
2532
+ else
2533
+ echo "โŒ Rollback not supported for this stack"
2534
+ echo "๐Ÿ’ก Consider creating a new migration to revert changes"
2535
+ exit 1
2536
+ fi
2537
+
2538
+ echo "โœ… Rollback complete"
2539
+ """
2540
+
2541
+ [tasks."db:status"]
2542
+ description = "Show migration status"
2543
+ run = """
2544
+ #!/usr/bin/env bash
2545
+
2546
+ if [ -f "alembic.ini" ]; then
2547
+ alembic current
2548
+ alembic history
2549
+
2550
+ elif [ -f "go.mod" ]; then
2551
+ migrate -path migrations -database "$DATABASE_URL" version
2552
+
2553
+ elif [ -f "build.gradle.kts" ]; then
2554
+ ./gradlew flywayInfo
2555
+ fi
2556
+ """
2557
+
2558
+ [tasks."db:reset"]
2559
+ description = "Drop all tables and re-run migrations (DEV ONLY)"
2560
+ run = """
2561
+ #!/usr/bin/env bash
2562
+ set -e
2563
+
2564
+ if [ "$NODE_ENV" = "production" ]; then
2565
+ echo "โŒ Cannot reset database in production!"
2566
+ exit 1
2567
+ fi
2568
+
2569
+ echo "โš ๏ธ This will DELETE ALL DATA. Are you sure? (yes/no)"
2570
+ read -r confirm
2571
+
2572
+ if [ "$confirm" != "yes" ]; then
2573
+ echo "Cancelled"
2574
+ exit 0
2575
+ fi
2576
+
2577
+ # Drop database
2578
+ psql "$DATABASE_URL" -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
2579
+
2580
+ # Re-run migrations
2581
+ mise run db:migrate
2582
+
2583
+ echo "โœ… Database reset complete"
2584
+ """
2585
+ ```
2586
+
2587
+ ---
2588
+
2589
+ ## ๐Ÿ” Secrets Management
2590
+
2591
+ ### Niveles de Secrets
2592
+
2593
+ ```
2594
+ Level 1: Local Dev โ†’ .env (not committed)
2595
+ Level 2: Team Shared โ†’ Doppler/Infisical
2596
+ Level 3: CI/CD โ†’ GitHub Secrets
2597
+ Level 4: Production โ†’ AWS Secrets Manager / Vault
2598
+ ```
2599
+
2600
+ ---
2601
+
2602
+ ## ๐Ÿ  Local Development
2603
+
2604
+ ### Opciรณn 1: .env Simple (Para empezar)
2605
+
2606
+ ```bash
2607
+ # .env.example (COMMITTED al repo)
2608
+ # Copiar y renombrar a .env
2609
+ DATABASE_URL=postgresql://localhost:5432/mydb
2610
+ REDIS_URL=redis://localhost:6379
2611
+ JWT_SECRET=change-me-in-development
2612
+ API_KEY=
2613
+
2614
+ # Production services (dejar vacรญo en local)
2615
+ STRIPE_SECRET_KEY=
2616
+ SENDGRID_API_KEY=
2617
+ AWS_ACCESS_KEY_ID=
2618
+ AWS_SECRET_ACCESS_KEY=
2619
+ ```
2620
+
2621
+ ```bash
2622
+ # .env (NOT COMMITTED - en .gitignore)
2623
+ DATABASE_URL=postgresql://localhost:5432/mydb
2624
+ REDIS_URL=redis://localhost:6379
2625
+ JWT_SECRET=local-dev-secret-key-123
2626
+ API_KEY=sk-test-1234567890
2627
+
2628
+ # Real API keys para testing
2629
+ STRIPE_SECRET_KEY=sk_test_real_key_here
2630
+ SENDGRID_API_KEY=SG.real_key_here
2631
+ ```
2632
+
2633
+ ```bash