@luanpdd/kit-mcp 1.29.0 → 1.30.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 (330) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +168 -168
  3. package/gates/agent-no-recursive-dispatch.md +82 -82
  4. package/kit/COMANDOS.md +138 -138
  5. package/kit/README.md +76 -76
  6. package/kit/agents/advisor-researcher.md +106 -106
  7. package/kit/agents/assumptions-analyzer.md +107 -107
  8. package/kit/agents/audit-log-implementer.md +313 -313
  9. package/kit/agents/auditor-consistencia-isolamento.md +413 -413
  10. package/kit/agents/b2b-saas-architect.md +156 -156
  11. package/kit/agents/cascading-failures-auditor.md +298 -298
  12. package/kit/agents/codebase-mapper.md +768 -768
  13. package/kit/agents/crm-pipeline-implementer.md +256 -256
  14. package/kit/agents/debugger.md +813 -813
  15. package/kit/agents/detector-tenant-quente.md +337 -337
  16. package/kit/agents/evolution-go-integrator.md +200 -200
  17. package/kit/agents/example-reviewer.md +21 -21
  18. package/kit/agents/executor.md +564 -564
  19. package/kit/agents/integration-checker.md +200 -200
  20. package/kit/agents/invite-flow-implementer.md +189 -189
  21. package/kit/agents/legacy-characterizer.md +368 -368
  22. package/kit/agents/lgpd-compliance-auditor.md +295 -295
  23. package/kit/agents/multi-tenant-isolation-auditor.md +253 -253
  24. package/kit/agents/multi-tenant-rls-writer.md +340 -340
  25. package/kit/agents/nyquist-auditor.md +178 -178
  26. package/kit/agents/observability-coverage-auditor.md +315 -315
  27. package/kit/agents/org-onboarding-implementer.md +223 -223
  28. package/kit/agents/payload-capture-instrumenter.md +273 -273
  29. package/kit/agents/phase-researcher.md +696 -696
  30. package/kit/agents/plan-checker.md +272 -272
  31. package/kit/agents/planner.md +922 -922
  32. package/kit/agents/project-researcher.md +652 -652
  33. package/kit/agents/refactor-safety-auditor.md +404 -404
  34. package/kit/agents/research-synthesizer.md +245 -245
  35. package/kit/agents/roadmapper.md +677 -677
  36. package/kit/agents/seam-finder.md +359 -359
  37. package/kit/agents/shotgun-surgery-detector.md +349 -349
  38. package/kit/agents/supabase-branching-architect.md +562 -562
  39. package/kit/agents/supabase-cicd-pipeline-implementer.md +777 -777
  40. package/kit/agents/supabase-column-privileges-writer.md +399 -399
  41. package/kit/agents/supabase-edge-fn-tester.md +287 -0
  42. package/kit/agents/supabase-edge-fn-writer.md +239 -210
  43. package/kit/agents/supabase-migration-writer.md +385 -385
  44. package/kit/agents/supabase-rbac-implementer.md +392 -392
  45. package/kit/agents/supabase-realtime-implementer.md +363 -267
  46. package/kit/agents/supabase-rls-hardener.md +521 -521
  47. package/kit/agents/supabase-rls-writer.md +323 -323
  48. package/kit/agents/supabase-roles-implementer.md +355 -355
  49. package/kit/agents/super-admin-implementer.md +281 -281
  50. package/kit/agents/ui-auditor.md +437 -437
  51. package/kit/agents/ui-checker.md +302 -302
  52. package/kit/agents/ui-researcher.md +355 -355
  53. package/kit/agents/user-profiler.md +175 -175
  54. package/kit/agents/validador-evolucao-schema.md +335 -335
  55. package/kit/agents/verifier.md +728 -728
  56. package/kit/commands/adicionar-backlog.md +75 -75
  57. package/kit/commands/adicionar-fase.md +42 -42
  58. package/kit/commands/adicionar-tarefa.md +45 -45
  59. package/kit/commands/adicionar-testes.md +41 -41
  60. package/kit/commands/ajuda.md +21 -21
  61. package/kit/commands/atualizar.md +37 -37
  62. package/kit/commands/auditar-cascading.md +111 -111
  63. package/kit/commands/auditar-marco.md +179 -179
  64. package/kit/commands/auditar-observabilidade-cobertura.md +183 -183
  65. package/kit/commands/auditar-refactor.md +219 -219
  66. package/kit/commands/auditar-release.md +109 -109
  67. package/kit/commands/auditar-uat.md +23 -23
  68. package/kit/commands/autonomo.md +40 -40
  69. package/kit/commands/branch-pr.md +24 -24
  70. package/kit/commands/burn-rate-status.md +408 -408
  71. package/kit/commands/capturar-payloads.md +193 -193
  72. package/kit/commands/caracterizar.md +212 -212
  73. package/kit/commands/concluir-marco.md +247 -247
  74. package/kit/commands/configuracoes.md +36 -36
  75. package/kit/commands/dados-distribuidos.md +188 -188
  76. package/kit/commands/definir-perfil.md +10 -10
  77. package/kit/commands/depurar.md +190 -190
  78. package/kit/commands/detectar-duplicacao.md +197 -197
  79. package/kit/commands/discutir-fase.md +131 -131
  80. package/kit/commands/encontrar-seams.md +136 -136
  81. package/kit/commands/entrar-discord.md +17 -17
  82. package/kit/commands/estatisticas.md +18 -18
  83. package/kit/commands/example-greeting.md +33 -33
  84. package/kit/commands/executar-fase.md +58 -58
  85. package/kit/commands/expresso.md +56 -56
  86. package/kit/commands/fase-ui.md +34 -34
  87. package/kit/commands/fazer.md +57 -57
  88. package/kit/commands/fio.md +125 -125
  89. package/kit/commands/fluxos-trabalho.md +64 -64
  90. package/kit/commands/forense.md +176 -176
  91. package/kit/commands/gerenciador.md +38 -38
  92. package/kit/commands/inserir-fase.md +31 -31
  93. package/kit/commands/legacy.md +263 -263
  94. package/kit/commands/limpeza.md +17 -17
  95. package/kit/commands/listar-hipoteses-fase.md +45 -45
  96. package/kit/commands/listar-workspaces.md +18 -18
  97. package/kit/commands/load-shedding.md +117 -117
  98. package/kit/commands/mapear-codebase.md +70 -70
  99. package/kit/commands/multi-tenant.md +163 -163
  100. package/kit/commands/nota.md +33 -33
  101. package/kit/commands/novo-marco.md +43 -43
  102. package/kit/commands/novo-projeto.md +41 -41
  103. package/kit/commands/novo-workspace.md +43 -43
  104. package/kit/commands/pausar-trabalho.md +37 -37
  105. package/kit/commands/perfil-usuario.md +45 -45
  106. package/kit/commands/pesquisar-fase.md +195 -195
  107. package/kit/commands/planejar-fase.md +67 -67
  108. package/kit/commands/planejar-lacunas.md +33 -33
  109. package/kit/commands/plantar-ideia.md +25 -25
  110. package/kit/commands/progresso.md +24 -24
  111. package/kit/commands/proximo.md +30 -30
  112. package/kit/commands/publicar.md +490 -490
  113. package/kit/commands/rapido.md +35 -35
  114. package/kit/commands/reaplicar-patches.md +124 -124
  115. package/kit/commands/refactor-seguro.md +321 -321
  116. package/kit/commands/relatorio-sessao.md +19 -19
  117. package/kit/commands/remover-fase.md +31 -31
  118. package/kit/commands/remover-workspace.md +26 -26
  119. package/kit/commands/resumo-marco.md +50 -50
  120. package/kit/commands/retomar-trabalho.md +40 -40
  121. package/kit/commands/revisar-backlog.md +60 -60
  122. package/kit/commands/revisar-ui.md +32 -32
  123. package/kit/commands/revisar.md +37 -37
  124. package/kit/commands/saude.md +21 -21
  125. package/kit/commands/setup-notion.md +93 -93
  126. package/kit/commands/storytelling.md +179 -179
  127. package/kit/commands/supabase.md +30 -7
  128. package/kit/commands/sync-main.md +68 -68
  129. package/kit/commands/validar-fase.md +35 -35
  130. package/kit/commands/verificar-tarefas.md +44 -44
  131. package/kit/commands/verificar-trabalho.md +64 -64
  132. package/kit/file-manifest.json +14 -8
  133. package/kit/framework/bin/lib/commands.cjs +959 -959
  134. package/kit/framework/bin/lib/config.cjs +442 -442
  135. package/kit/framework/bin/lib/core.cjs +1230 -1230
  136. package/kit/framework/bin/lib/frontmatter.cjs +336 -336
  137. package/kit/framework/bin/lib/init.cjs +1442 -1442
  138. package/kit/framework/bin/lib/milestone.cjs +252 -252
  139. package/kit/framework/bin/lib/model-profiles.cjs +68 -68
  140. package/kit/framework/bin/lib/phase.cjs +888 -888
  141. package/kit/framework/bin/lib/profile-output.cjs +952 -952
  142. package/kit/framework/bin/lib/profile-pipeline.cjs +539 -539
  143. package/kit/framework/bin/lib/roadmap.cjs +329 -329
  144. package/kit/framework/bin/lib/security.cjs +382 -382
  145. package/kit/framework/bin/lib/state.cjs +1031 -1031
  146. package/kit/framework/bin/lib/template.cjs +222 -222
  147. package/kit/framework/bin/lib/uat.cjs +282 -282
  148. package/kit/framework/bin/lib/verify.cjs +888 -888
  149. package/kit/framework/bin/lib/workstream.cjs +491 -491
  150. package/kit/framework/bin/tools.cjs +918 -918
  151. package/kit/framework/commands/workstreams.md +63 -63
  152. package/kit/framework/references/checkpoints.md +778 -778
  153. package/kit/framework/references/continuation-format.md +249 -249
  154. package/kit/framework/references/decimal-phase-calculation.md +64 -64
  155. package/kit/framework/references/git-integration.md +295 -295
  156. package/kit/framework/references/git-planning-commit.md +38 -38
  157. package/kit/framework/references/model-profile-resolution.md +36 -36
  158. package/kit/framework/references/model-profiles.md +139 -139
  159. package/kit/framework/references/phase-argument-parsing.md +61 -61
  160. package/kit/framework/references/planning-config.md +202 -202
  161. package/kit/framework/references/questioning.md +162 -162
  162. package/kit/framework/references/tdd.md +263 -263
  163. package/kit/framework/references/ui-brand.md +160 -160
  164. package/kit/framework/references/user-profiling.md +657 -657
  165. package/kit/framework/references/verification-patterns.md +612 -612
  166. package/kit/framework/references/workstream-flag.md +58 -58
  167. package/kit/framework/templates/DEBUG.md +164 -164
  168. package/kit/framework/templates/UAT.md +265 -265
  169. package/kit/framework/templates/UI-SPEC.md +100 -100
  170. package/kit/framework/templates/VALIDATION.md +76 -76
  171. package/kit/framework/templates/claude-md.md +122 -122
  172. package/kit/framework/templates/codebase/architecture.md +185 -185
  173. package/kit/framework/templates/codebase/concerns.md +205 -205
  174. package/kit/framework/templates/codebase/conventions.md +204 -204
  175. package/kit/framework/templates/codebase/integrations.md +192 -192
  176. package/kit/framework/templates/codebase/stack.md +158 -158
  177. package/kit/framework/templates/codebase/structure.md +199 -199
  178. package/kit/framework/templates/codebase/testing.md +301 -301
  179. package/kit/framework/templates/config.json +44 -44
  180. package/kit/framework/templates/context.md +352 -352
  181. package/kit/framework/templates/continue-here.md +78 -78
  182. package/kit/framework/templates/copilot-instructions.md +7 -7
  183. package/kit/framework/templates/debug-subagent-prompt.md +91 -91
  184. package/kit/framework/templates/dev-preferences.md +20 -20
  185. package/kit/framework/templates/discovery.md +146 -146
  186. package/kit/framework/templates/discussion-log.md +63 -63
  187. package/kit/framework/templates/milestone-archive.md +123 -123
  188. package/kit/framework/templates/milestone.md +115 -115
  189. package/kit/framework/templates/phase-prompt.md +610 -610
  190. package/kit/framework/templates/planner-subagent-prompt.md +117 -117
  191. package/kit/framework/templates/project.md +186 -186
  192. package/kit/framework/templates/requirements.md +231 -231
  193. package/kit/framework/templates/research-project/ARCHITECTURE.md +204 -204
  194. package/kit/framework/templates/research-project/FEATURES.md +147 -147
  195. package/kit/framework/templates/research-project/PITFALLS.md +200 -200
  196. package/kit/framework/templates/research-project/STACK.md +120 -120
  197. package/kit/framework/templates/research-project/SUMMARY.md +170 -170
  198. package/kit/framework/templates/research.md +419 -419
  199. package/kit/framework/templates/retrospective.md +54 -54
  200. package/kit/framework/templates/roadmap.md +202 -202
  201. package/kit/framework/templates/state.md +176 -176
  202. package/kit/framework/templates/summary-complex.md +59 -59
  203. package/kit/framework/templates/summary-minimal.md +41 -41
  204. package/kit/framework/templates/summary-standard.md +48 -48
  205. package/kit/framework/templates/summary.md +209 -209
  206. package/kit/framework/templates/user-profile.md +146 -146
  207. package/kit/framework/templates/user-setup.md +256 -256
  208. package/kit/framework/templates/verification-report.md +258 -258
  209. package/kit/framework/workflows/add-phase.md +112 -112
  210. package/kit/framework/workflows/add-tests.md +351 -351
  211. package/kit/framework/workflows/add-todo.md +158 -158
  212. package/kit/framework/workflows/audit-milestone.md +340 -340
  213. package/kit/framework/workflows/audit-uat.md +109 -109
  214. package/kit/framework/workflows/autonomous.md +891 -891
  215. package/kit/framework/workflows/check-todos.md +177 -177
  216. package/kit/framework/workflows/cleanup.md +152 -152
  217. package/kit/framework/workflows/complete-milestone.md +696 -696
  218. package/kit/framework/workflows/diagnose-issues.md +231 -231
  219. package/kit/framework/workflows/discovery-phase.md +289 -289
  220. package/kit/framework/workflows/discuss-phase-assumptions.md +653 -653
  221. package/kit/framework/workflows/discuss-phase.md +784 -784
  222. package/kit/framework/workflows/do.md +104 -104
  223. package/kit/framework/workflows/execute-phase.md +838 -838
  224. package/kit/framework/workflows/execute-plan.md +510 -510
  225. package/kit/framework/workflows/fast.md +102 -102
  226. package/kit/framework/workflows/forensics.md +265 -265
  227. package/kit/framework/workflows/health.md +181 -181
  228. package/kit/framework/workflows/help.md +619 -619
  229. package/kit/framework/workflows/insert-phase.md +130 -130
  230. package/kit/framework/workflows/list-phase-assumptions.md +178 -178
  231. package/kit/framework/workflows/list-workspaces.md +56 -56
  232. package/kit/framework/workflows/manager.md +362 -362
  233. package/kit/framework/workflows/map-codebase.md +377 -377
  234. package/kit/framework/workflows/milestone-summary.md +223 -223
  235. package/kit/framework/workflows/new-milestone.md +486 -486
  236. package/kit/framework/workflows/new-project.md +1159 -1159
  237. package/kit/framework/workflows/new-workspace.md +237 -237
  238. package/kit/framework/workflows/next.md +97 -97
  239. package/kit/framework/workflows/node-repair.md +92 -92
  240. package/kit/framework/workflows/note.md +156 -156
  241. package/kit/framework/workflows/pause-work.md +176 -176
  242. package/kit/framework/workflows/plan-milestone-gaps.md +273 -273
  243. package/kit/framework/workflows/plan-phase.md +765 -765
  244. package/kit/framework/workflows/plant-seed.md +169 -169
  245. package/kit/framework/workflows/pr-branch.md +129 -129
  246. package/kit/framework/workflows/profile-user.md +450 -450
  247. package/kit/framework/workflows/progress.md +507 -507
  248. package/kit/framework/workflows/quick.md +757 -757
  249. package/kit/framework/workflows/remove-phase.md +155 -155
  250. package/kit/framework/workflows/remove-workspace.md +90 -90
  251. package/kit/framework/workflows/research-phase.md +82 -82
  252. package/kit/framework/workflows/resume-project.md +326 -326
  253. package/kit/framework/workflows/review.md +228 -228
  254. package/kit/framework/workflows/session-report.md +146 -146
  255. package/kit/framework/workflows/settings.md +283 -283
  256. package/kit/framework/workflows/ship.md +228 -228
  257. package/kit/framework/workflows/stats.md +60 -60
  258. package/kit/framework/workflows/transition.md +671 -671
  259. package/kit/framework/workflows/ui-phase.md +302 -302
  260. package/kit/framework/workflows/ui-review.md +165 -165
  261. package/kit/framework/workflows/update.md +323 -323
  262. package/kit/framework/workflows/validate-phase.md +174 -174
  263. package/kit/framework/workflows/verify-phase.md +252 -252
  264. package/kit/framework/workflows/verify-work.md +637 -637
  265. package/kit/hooks/check-update.js +118 -118
  266. package/kit/hooks/context-monitor.js +163 -163
  267. package/kit/hooks/prompt-guard.js +103 -103
  268. package/kit/hooks/statusline.js +125 -125
  269. package/kit/hooks/workflow-guard.js +101 -101
  270. package/kit/settings.json +45 -45
  271. package/kit/skills/_shared-supabase/glossary.md +17 -0
  272. package/kit/skills/ai-prompt-characterization/SKILL.md +335 -335
  273. package/kit/skills/armadilhas-sistemas-distribuidos/SKILL.md +447 -447
  274. package/kit/skills/audit-log-multi-tenant/SKILL.md +340 -340
  275. package/kit/skills/b2b-saas-architecture/SKILL.md +300 -300
  276. package/kit/skills/consistencia-leitura-replica/SKILL.md +385 -385
  277. package/kit/skills/crm-lead-pipeline-patterns/SKILL.md +343 -343
  278. package/kit/skills/escolha-modelo-consistencia/SKILL.md +494 -494
  279. package/kit/skills/evolucao-schema-compativel/SKILL.md +448 -448
  280. package/kit/skills/evolution-go-whatsapp-integration/SKILL.md +322 -322
  281. package/kit/skills/example-skill/SKILL.md +42 -42
  282. package/kit/skills/legacy-api-only-applications/SKILL.md +358 -358
  283. package/kit/skills/legacy-characterization-tests/SKILL.md +330 -330
  284. package/kit/skills/legacy-effect-analysis/SKILL.md +331 -331
  285. package/kit/skills/legacy-extract-class/SKILL.md +203 -203
  286. package/kit/skills/legacy-programming-by-difference/SKILL.md +252 -252
  287. package/kit/skills/legacy-seams-and-test-harness/SKILL.md +460 -460
  288. package/kit/skills/legacy-shotgun-surgery/SKILL.md +286 -286
  289. package/kit/skills/legacy-sprout-wrap-techniques/SKILL.md +434 -434
  290. package/kit/skills/legacy-storytelling-naked-crc/SKILL.md +270 -270
  291. package/kit/skills/lgpd-multi-tenant-compliance/SKILL.md +340 -340
  292. package/kit/skills/member-invite-flow/SKILL.md +305 -305
  293. package/kit/skills/member-management-react-shadcn/SKILL.md +328 -328
  294. package/kit/skills/multi-tenant-performance-scaling/SKILL.md +316 -316
  295. package/kit/skills/multi-tenant-rls-hierarchy/SKILL.md +342 -342
  296. package/kit/skills/org-onboarding-flow/SKILL.md +257 -257
  297. package/kit/skills/org-switcher-react-pattern/SKILL.md +349 -349
  298. package/kit/skills/permission-gate-react-pattern/SKILL.md +271 -271
  299. package/kit/skills/postgres-isolamento-concorrencia/SKILL.md +552 -552
  300. package/kit/skills/pre-refactor-characterization/SKILL.md +421 -421
  301. package/kit/skills/rbac-permissions-matrix-supabase/SKILL.md +338 -338
  302. package/kit/skills/streams-eventos-cdc/SKILL.md +711 -711
  303. package/kit/skills/supabase-branching-workflow/SKILL.md +544 -544
  304. package/kit/skills/supabase-ci-cd-github-actions/SKILL.md +880 -880
  305. package/kit/skills/supabase-column-level-security/SKILL.md +426 -426
  306. package/kit/skills/supabase-config-toml-remotes/SKILL.md +807 -807
  307. package/kit/skills/supabase-custom-claims-rbac/SKILL.md +472 -472
  308. package/kit/skills/supabase-edge-functions/SKILL.md +229 -141
  309. package/kit/skills/supabase-edge-functions-auth/SKILL.md +309 -0
  310. package/kit/skills/supabase-edge-functions-limits/SKILL.md +302 -0
  311. package/kit/skills/supabase-edge-functions-mcp-server/SKILL.md +279 -0
  312. package/kit/skills/supabase-edge-functions-testing/SKILL.md +277 -0
  313. package/kit/skills/supabase-edge-runtime-builtins/SKILL.md +357 -0
  314. package/kit/skills/supabase-migration-repair/SKILL.md +823 -823
  315. package/kit/skills/supabase-migrations/SKILL.md +297 -297
  316. package/kit/skills/supabase-pgtap-testing/SKILL.md +1053 -1053
  317. package/kit/skills/supabase-postgres-roles/SKILL.md +392 -392
  318. package/kit/skills/supabase-realtime/SKILL.md +460 -236
  319. package/kit/skills/supabase-rls-defense-in-depth/SKILL.md +418 -418
  320. package/kit/skills/supabase-rls-policies/SKILL.md +635 -635
  321. package/kit/skills/super-admin-platform-pattern/SKILL.md +326 -326
  322. package/kit/skills/tenant-quente-mitigacao/SKILL.md +605 -605
  323. package/kit/skills/whatsapp-conversation-state-machine/SKILL.md +287 -287
  324. package/package.json +1 -1
  325. package/src/core/kit.js +216 -216
  326. package/src/core/reflect.js +247 -247
  327. package/src/core/reverse-sync.js +372 -372
  328. package/src/core/sync.js +418 -418
  329. package/src/core/watch.js +121 -121
  330. package/src/mcp-server/index.js +693 -693
@@ -1,1053 +1,1053 @@
1
- ---
2
- name: supabase-pgtap-testing
3
- description: Use ao escrever testes de database/Edge Function em Supabase — pgTAP extension (TAP…
4
- ---
5
-
6
- # Supabase — pgTAP & Deno Testing
7
-
8
- ## Quando usar
9
-
10
- Esta skill cobre **testing-shift-left para Supabase** — testar schema, RLS, PG functions e Edge Functions ANTES de deploy via CI. Dois runners canônicos:
11
-
12
- 1. **pgTAP** (`supabase test db`) — database tests via TAP (Test Anything Protocol) Postgres extension
13
- 2. **Deno test** (`deno test --allow-all`) — Edge Functions tests via Deno runtime nativo
14
-
15
- Trigger phrases:
16
-
17
- - "testes Supabase", "test Supabase", "testing Supabase"
18
- - "pgTAP", "supabase test db", "TAP Postgres extension"
19
- - "plan ok is throws_ok results_eq pgTAP", "has_table has_column col_type_is"
20
- - "Deno test Edge Function", "deno test --allow-all"
21
- - "testar RLS Supabase", "characterization tests PG function"
22
- - "supabase/tests/", "supabase/functions/_tests/"
23
- - "testar trigger Postgres", "testar function Postgres"
24
- - ".env.local Deno tests", "SUPABASE_URL SUPABASE_ANON_KEY tests"
25
- - "characterization PG legado", "refatorar function Postgres com tests"
26
-
27
- **Use APENAS para:**
28
-
29
- - Validar schema, columns, tipos, FKs via pgTAP (`has_table`, `has_column`, `col_type_is`)
30
- - Validar RLS policies retornando `permission denied` quando esperado (`throws_ok 42501`)
31
- - Validar PG functions retornando outputs esperados (`results_eq`)
32
- - Validar constraints, triggers, defaults (`throws_ok 23502 not_null_violation`, `throws_ok 23505 unique_violation`)
33
- - Testar Edge Functions HTTP handlers (status, response body, side effects)
34
- - Capturar comportamento atual de PG function legada como **characterization oracle** (Feathers cap 13)
35
- - Integrar com workflow CI `database-tests.yml` (Phase 151 CI-05) e `functions-tests.yml` (CI-06)
36
-
37
- **NÃO use para:**
38
-
39
- - Substituir integration tests no app layer (frontend ↔ Supabase via client SDK) — pgTAP cobre só DB; Deno tests cobrem só Edge Function isolado
40
- - Performance tests (latência, throughput) — pgTAP é assertion-based, não benchmark; use `pgbench` ou Vegeta para load testing
41
- - Tests em produção sem `rollback` — sempre `begin; ... rollback;` (cross-ref Anti-pattern 1)
42
- - Substituir auditoria via `auditor-consistencia-isolamento` (v1.22) — tests validam comportamento esperado; auditor detecta anti-patterns estáticos no SQL
43
- - Substituir mutation testing (`ai-mutation-tester`) — pgTAP é characterization; mutation testing valida que tests realmente detectam regressão
44
-
45
- ## Princípio canônico
46
-
47
- Três princípios canônicos:
48
-
49
- 1. **Testing shift-left.** Cada PR valida schema + RLS + PG functions + Edge Functions ANTES do merge. Gate canônico = required status check `database-tests / build` e `functions-tests / build` enforced via branch protection (cross-ref skill `supabase-ci-cd-github-actions` Phase 151 patterns 5 e 6).
50
-
51
- 2. **Tests são schema mutations transacionais.** Todo teste pgTAP roda dentro de `begin; ... rollback;` — assertions executam, fixtures são inseridas, RLS é exercitada, mas DB volta ao estado original. Nunca polui DB compartilhado; tests podem rodar em paralelo sem race.
52
-
53
- 3. **pgTAP é o oracle imutável para refactor de PG legado.** Quando PG function complexa (> 100 linhas, sem tests) precisa ser refatorada, pgTAP captura comportamento atual como characterization (Feathers cap 13). Tests viram oracle congelado — qualquer mudança que altere comportamento previamente capturado falha o build. Sem characterization, refactor de PG é "edit and pray" (cross-ref skill `legacy-characterization-tests` v1.16).
54
-
55
- ### Distinção canônica vs outros runners
56
-
57
- | | pgTAP (`supabase test db`) | Deno test (`deno test`) | Vitest/Jest (app layer) |
58
- |---|---|---|---|
59
- | Escopo | Schema, RLS, PG functions, triggers | Edge Functions HTTP handlers | Frontend + cliente Supabase |
60
- | Linguagem | SQL (TAP syntax) | TypeScript (Deno runtime) | TypeScript (Node runtime) |
61
- | Isolamento | `begin; ... rollback;` (atomic) | Function call por test | Mocked client OR real (E2E) |
62
- | Onde roda | Postgres local (via `supabase start`) | Deno runtime + Postgres local | Node + browser environment |
63
- | CI workflow | `database-tests.yml` (Phase 151 CI-05) | `functions-tests.yml` (CI-06) | Custom `vitest.yml` ou `jest.yml` |
64
- | Trigger canônico | PR (gate) | PR (gate) | PR + post-merge (canary) |
65
-
66
- Os três runners são **complementares** — pgTAP valida DB, Deno valida Edge Function HTTP layer, Vitest/Jest valida cliente. Nenhum substitui os outros.
67
-
68
- ## Pattern 1: pgTAP extension setup + sintaxe canônica (TEST-01)
69
-
70
- ### Habilitar pgTAP
71
-
72
- Em migration dedicada (uma única vez por projeto):
73
-
74
- ```sql
75
- -- supabase/migrations/YYYYMMDDHHmmss_enable_pgtap.sql
76
- create extension if not exists pgtap with schema extensions;
77
- ```
78
-
79
- **Por que `with schema extensions`:** convenção Supabase canônica — todas extensions ficam em schema dedicado (`extensions`), separadas de `public`. Permite namespace isolation + GRANT EXECUTE granular.
80
-
81
- ### Diretório canônico `supabase/tests/`
82
-
83
- Supabase CLI busca automaticamente arquivos `*.sql` em `supabase/tests/` quando executa `supabase test db`:
84
-
85
- ```text
86
- supabase/
87
- ├── migrations/
88
- │ ├── 20260101000000_initial_schema.sql
89
- │ └── 20260102000000_enable_pgtap.sql
90
- ├── tests/
91
- │ ├── employees_test.sql # ← descoberto automaticamente
92
- │ ├── rls_organizations_test.sql # ← descoberto automaticamente
93
- │ └── triggers_audit_test.sql # ← descoberto automaticamente
94
- └── functions/
95
- ```
96
-
97
- Naming convention: `<entity>_test.sql` ou `<concern>_test.sql` — nome descritivo + sufixo `_test.sql`. CLI roda em ordem alfabética.
98
-
99
- ### Estrutura canônica de um teste pgTAP
100
-
101
- ```sql
102
- -- supabase/tests/employees_test.sql
103
- begin; -- inicia transação
104
- select plan(4); -- declara 4 testes (CRÍTICO — sem plan, falha silencioso)
105
-
106
- -- Test 1: tabela existe
107
- select has_table('public', 'employees', 'employees table should exist');
108
-
109
- -- Test 2: coluna existe com tipo correto
110
- select has_column('public', 'employees', 'name', 'name column should exist');
111
- select col_type_is('public', 'employees', 'name', 'text', 'name should be text');
112
-
113
- -- Test 3: comportamento de seed
114
- select results_eq(
115
- 'select count(*) from public.employees',
116
- 'values (3::bigint)',
117
- 'employees count should be 3 after seed'
118
- );
119
-
120
- -- Test 4: constraint NOT NULL
121
- select throws_ok(
122
- 'insert into public.employees (name) values (null)',
123
- '23502', -- SQLSTATE not_null_violation
124
- 'null in name should violate not-null constraint'
125
- );
126
-
127
- select * from finish(); -- finaliza plano (gera summary TAP)
128
- rollback; -- desfaz qualquer side effect
129
- ```
130
-
131
- **Crítico:** `plan(N)` declara o número exato de assertions executadas. Se você roda 5 mas declarou 4, pgTAP reporta `# Looks like you planned 4 tests but ran 5.` — teste falha. Se rodar menos do que planejou, idem.
132
-
133
- ### Funções pgTAP canônicas
134
-
135
- #### Schema/structure assertions
136
-
137
- ```sql
138
- -- tabela existe
139
- select has_table('public', 'orders', 'orders table exists');
140
-
141
- -- tabela NÃO existe
142
- select hasnt_table('public', 'old_table', 'old_table was dropped');
143
-
144
- -- coluna existe
145
- select has_column('public', 'orders', 'total', 'total column exists');
146
-
147
- -- tipo da coluna
148
- select col_type_is('public', 'orders', 'total', 'numeric(10,2)', 'total is numeric(10,2)');
149
-
150
- -- NOT NULL
151
- select col_not_null('public', 'orders', 'customer_id', 'customer_id is NOT NULL');
152
-
153
- -- DEFAULT
154
- select col_has_default('public', 'orders', 'created_at', 'created_at has default');
155
-
156
- -- PRIMARY KEY
157
- select has_pk('public', 'orders', 'orders has primary key');
158
-
159
- -- FOREIGN KEY
160
- select has_fk('public', 'orders', 'orders.customer_id references customers');
161
-
162
- -- INDEX
163
- select has_index('public', 'orders', 'orders_customer_id_idx', 'index exists');
164
- ```
165
-
166
- #### Function/trigger assertions
167
-
168
- ```sql
169
- -- function existe
170
- select has_function('public', 'calculate_total', array['uuid'], 'function exists');
171
-
172
- -- return type
173
- select function_returns('public', 'calculate_total', 'numeric', 'returns numeric');
174
-
175
- -- trigger existe
176
- select has_trigger('public', 'orders', 'set_updated_at', 'trigger exists');
177
-
178
- -- enum value
179
- select has_enum('public', 'order_status', array['pending', 'paid', 'shipped'], 'enum values');
180
- ```
181
-
182
- #### Behavioral assertions
183
-
184
- ```sql
185
- -- equality (simple)
186
- select is(1 + 1, 2, '1+1 equals 2');
187
-
188
- -- inequality
189
- select isnt(now(), '2020-01-01'::timestamp, 'now is not 2020');
190
-
191
- -- boolean check
192
- select ok(exists(select 1 from public.users where id = '...'), 'user exists');
193
-
194
- -- equality (query result)
195
- select results_eq(
196
- 'select id, name from public.users where active = true order by id limit 2',
197
- $$values ('uuid-1'::uuid, 'Alice'), ('uuid-2'::uuid, 'Bob')$$,
198
- 'active users are Alice and Bob'
199
- );
200
-
201
- -- inequality (query result)
202
- select results_ne(
203
- 'select count(*) from public.deleted_users',
204
- 'values (0::bigint)',
205
- 'deleted_users is not empty'
206
- );
207
-
208
- -- subset (query result is subset)
209
- select set_eq(
210
- 'select email from public.users',
211
- $$values ('a@x.com'), ('b@x.com'), ('c@x.com')$$,
212
- 'all users have correct emails'
213
- );
214
- ```
215
-
216
- #### Error/exception assertions
217
-
218
- ```sql
219
- -- erro específico
220
- select throws_ok(
221
- 'insert into public.employees (name) values (null)',
222
- '23502', -- SQLSTATE not_null_violation
223
- 'null name should fail not-null constraint'
224
- );
225
-
226
- -- error matching pattern
227
- select throws_like(
228
- 'select * from public.private_table',
229
- '%permission denied%',
230
- 'cross-org SELECT raises permission denied'
231
- );
232
-
233
- -- NÃO lança erro
234
- select lives_ok(
235
- $$insert into public.employees (name, email) values ('Alice', 'alice@x.com')$$,
236
- 'valid insert should succeed'
237
- );
238
- ```
239
-
240
- #### Custom test helpers — fixtures
241
-
242
- ```sql
243
- -- helper para criar user JWT context
244
- create or replace function tests.set_jwt_claim(claim_name text, claim_value text)
245
- returns void
246
- language sql
247
- as $$
248
- select set_config('request.jwt.claim.' || claim_name, claim_value, true);
249
- $$;
250
-
251
- -- uso em teste
252
- select tests.set_jwt_claim('sub', 'uuid-of-user-1');
253
- select tests.set_jwt_claim('user_role', 'admin');
254
-
255
- -- agora policies que consultam auth.uid() retornam 'uuid-of-user-1'
256
- select results_eq(
257
- 'select count(*) from public.posts',
258
- 'values (10::bigint)', -- esperando 10 posts visíveis para este user
259
- 'user-1 admin sees all posts'
260
- );
261
- ```
262
-
263
- ### SQLSTATE codes canônicos para `throws_ok`
264
-
265
- | Code | Nome | Caso |
266
- |------|------|------|
267
- | `23502` | `not_null_violation` | INSERT/UPDATE com null em column NOT NULL |
268
- | `23503` | `foreign_key_violation` | Referência inexistente em FK |
269
- | `23505` | `unique_violation` | Duplicate em UNIQUE/PK |
270
- | `23514` | `check_violation` | Falha em CHECK constraint |
271
- | `42501` | `insufficient_privilege` | RLS bloqueando OR GRANT ausente |
272
- | `22001` | `string_data_right_truncation` | String muito longa para column |
273
- | `22008` | `datetime_field_overflow` | Date inválida |
274
- | `P0001` | `raise_exception` | RAISE EXCEPTION explícito em PG function |
275
-
276
- ## Pattern 2: `supabase test db` runner + integração CI (TEST-02)
277
-
278
- ### Execução local
279
-
280
- ```bash
281
- # Garantir Postgres local rodando (sobe via Docker)
282
- supabase start
283
-
284
- # Rodar todos os tests em supabase/tests/
285
- supabase test db
286
-
287
- # Output (TAP format):
288
- # 1..4
289
- # ok 1 - employees table should exist
290
- # ok 2 - name column should exist
291
- # ok 3 - name should be text
292
- # ok 4 - employees count should be 3 after seed
293
- # # Pass: 4
294
- # # Fail: 0
295
- ```
296
-
297
- ### Output TAP — anatomia
298
-
299
- ```text
300
- 1..4 ← plan (4 tests esperados)
301
- ok 1 - test description ← test passou
302
- not ok 2 - test description ← test falhou
303
- # Failed test 2: 'test description' ← detalhe da falha
304
- # got: 'actual_value'
305
- # expected: 'expected_value'
306
- ok 3 - test description
307
- ok 4 - test description
308
- # Looks like you failed 1 test of 4. ← summary
309
- ```
310
-
311
- Exit code:
312
- - **0** — todos tests passaram
313
- - **1** — algum teste falhou (CI fails → required check fails → merge bloqueado)
314
- - **3** — erro fatal (DB não disponível, syntax error em arquivo de test)
315
-
316
- ### Rodar test específico
317
-
318
- ```bash
319
- # Apenas um arquivo
320
- supabase test db supabase/tests/employees_test.sql
321
-
322
- # Útil para debug rápido durante TDD
323
- ```
324
-
325
- ### Integração CI (cross-ref Phase 151 CI-05 `database-tests.yml`)
326
-
327
- Workflow canônico em `.github/workflows/database-tests.yml`:
328
-
329
- ```yaml
330
- name: database-tests
331
- on:
332
- pull_request:
333
- jobs:
334
- build:
335
- runs-on: ubuntu-latest
336
- steps:
337
- - uses: actions/checkout@v4
338
- - uses: supabase/setup-cli@v1
339
- with:
340
- version: latest
341
- - run: supabase db start
342
- - run: supabase test db
343
- ```
344
-
345
- **Crítico:**
346
- - `supabase db start` (não `supabase start`) — sobe apenas Postgres, mais rápido que stack completa
347
- - `supabase test db` consome `supabase/tests/*.sql` em ordem alfabética
348
- - Falha em qualquer teste = exit code 1 = workflow falha = required check falha = merge bloqueado
349
-
350
- Required check `database-tests / build` deve ser obrigatório em branch protection rule para `main` (cross-ref Phase 151 lista de required checks).
351
-
352
- ### Caveat — fixtures via `supabase/seed.sql`
353
-
354
- `supabase db start` aplica todas migrations + executa `supabase/seed.sql` (se existir). Use seed para popular dados de fixture compartilhados:
355
-
356
- ```sql
357
- -- supabase/seed.sql (rodado uma vez em db start)
358
- insert into public.employees (id, name, email) values
359
- ('00000000-0000-0000-0000-000000000001', 'Alice', 'alice@x.com'),
360
- ('00000000-0000-0000-0000-000000000002', 'Bob', 'bob@x.com'),
361
- ('00000000-0000-0000-0000-000000000003', 'Carol', 'carol@x.com');
362
- ```
363
-
364
- Testes referenciam UUIDs determinísticos:
365
-
366
- ```sql
367
- select results_eq(
368
- 'select name from public.employees where id = ''00000000-0000-0000-0000-000000000001'' ',
369
- $$values ('Alice')$$,
370
- 'Alice exists'
371
- );
372
- ```
373
-
374
- **Caveat:** seed roda APENAS em `db start` (não a cada test). Tests que precisam de fixture específica devem inserir dentro do `begin; ... rollback;` para isolar.
375
-
376
- ### Caveat — paralelismo
377
-
378
- `supabase test db` roda arquivos **sequencialmente** (não em paralelo). Cada arquivo tem `begin; ... rollback;` próprio — isolamento OK. Mas tempo total cresce linearmente com N arquivos.
379
-
380
- Mitigação para suítes grandes (> 50 arquivos):
381
- - Particionar em jobs paralelos no GitHub Actions (matrix strategy)
382
- - Cada job roda subset de `supabase/tests/<group>/`
383
-
384
- ## Pattern 3: Deno Edge Function tests (TEST-03)
385
-
386
- ### Estrutura canônica
387
-
388
- ```text
389
- supabase/
390
- └── functions/
391
- ├── hello/
392
- │ └── index.ts # Edge Function code
393
- ├── create-invite/
394
- │ └── index.ts
395
- └── _tests/ # ← convenção (_ prefix evita conflito com fn name)
396
- ├── hello_test.ts
397
- └── create-invite_test.ts
398
- ```
399
-
400
- Naming convention: `<fn-name>_test.ts` em `supabase/functions/_tests/`.
401
-
402
- ### Anatomy de um teste Deno
403
-
404
- ```typescript
405
- // supabase/functions/_tests/hello_test.ts
406
- import { assertEquals } from "https://deno.land/std@0.221.0/assert/mod.ts";
407
-
408
- Deno.test("hello function returns 200", async () => {
409
- const response = await fetch("http://127.0.0.1:54321/functions/v1/hello", {
410
- method: "POST",
411
- headers: {
412
- "Authorization": `Bearer ${Deno.env.get("SUPABASE_ANON_KEY")}`,
413
- "Content-Type": "application/json",
414
- },
415
- body: JSON.stringify({ name: "World" }),
416
- });
417
-
418
- assertEquals(response.status, 200);
419
- const data = await response.json();
420
- assertEquals(data.message, "Hello World!");
421
- });
422
-
423
- Deno.test("hello function rejects empty body", async () => {
424
- const response = await fetch("http://127.0.0.1:54321/functions/v1/hello", {
425
- method: "POST",
426
- headers: {
427
- "Authorization": `Bearer ${Deno.env.get("SUPABASE_ANON_KEY")}`,
428
- "Content-Type": "application/json",
429
- },
430
- body: JSON.stringify({}),
431
- });
432
-
433
- assertEquals(response.status, 400);
434
- });
435
- ```
436
-
437
- ### Execução local
438
-
439
- ```bash
440
- # Sobe stack completa (Postgres + Auth + Edge Functions runtime + Storage)
441
- supabase start
442
-
443
- # Rodar tests
444
- deno test --allow-all supabase/functions/_tests/ --env-file .env.local
445
- ```
446
-
447
- `--allow-all` permite acesso file system + network + env vars (Deno é sandboxed por default).
448
-
449
- `--env-file .env.local` carrega env vars de arquivo local. Sem essa flag, `Deno.env.get(...)` retorna `undefined`.
450
-
451
- ### Estrutura `.env.local` (gitignored)
452
-
453
- ```bash
454
- # supabase/functions/.env.local (gitignored — gerado dinamicamente)
455
- SUPABASE_URL=http://127.0.0.1:54321
456
- SUPABASE_ANON_KEY=eyJ...local-anon-key...
457
- SUPABASE_SERVICE_ROLE_KEY=eyJ...local-service-role-key...
458
- ```
459
-
460
- **Caveat:** `supabase start` imprime essas keys no output. Capture-as automaticamente:
461
-
462
- ```bash
463
- # Linux/macOS
464
- supabase status -o env > supabase/functions/.env.local
465
-
466
- # Windows PowerShell
467
- supabase status -o env | Out-File -Encoding utf8 supabase/functions/.env.local
468
- ```
469
-
470
- Atualizar `.env.local` a cada `supabase start` — keys mudam se containers forem recriados.
471
-
472
- ### Importar handler diretamente (test unitário)
473
-
474
- Alternativa ao test via HTTP — importar handler direto:
475
-
476
- ```typescript
477
- // supabase/functions/_tests/hello_unit_test.ts
478
- import { assertEquals } from "https://deno.land/std@0.221.0/assert/mod.ts";
479
-
480
- // Assumindo hello/index.ts exporta handler
481
- import { handler } from "../hello/index.ts";
482
-
483
- Deno.test("handler returns 200 for valid input", async () => {
484
- const req = new Request("http://localhost/hello", {
485
- method: "POST",
486
- headers: { "Content-Type": "application/json" },
487
- body: JSON.stringify({ name: "World" }),
488
- });
489
-
490
- const res = await handler(req);
491
- assertEquals(res.status, 200);
492
-
493
- const data = await res.json();
494
- assertEquals(data.message, "Hello World!");
495
- });
496
-
497
- Deno.test("handler returns 400 for invalid input", async () => {
498
- const req = new Request("http://localhost/hello", {
499
- method: "POST",
500
- headers: { "Content-Type": "application/json" },
501
- body: JSON.stringify({}),
502
- });
503
-
504
- const res = await handler(req);
505
- assertEquals(res.status, 400);
506
- });
507
- ```
508
-
509
- **Vantagens vs HTTP test:**
510
- - Mais rápido (sem network roundtrip)
511
- - Testa lógica isolada do handler
512
- - Não requer `supabase start` (apenas Deno runtime)
513
-
514
- **Desvantagens:**
515
- - Não exercita auth middleware (JWT verification)
516
- - Não exercita rate limiting / CORS / Deno.serve setup
517
- - Mais frágil a mudanças no boilerplate da Edge Function
518
-
519
- **Canônico:** combinar AMBOS — unit tests do handler (rápidos) + smoke test via HTTP (validação E2E mínima).
520
-
521
- ### Integração CI (cross-ref Phase 151 CI-06 `functions-tests.yml`)
522
-
523
- Workflow canônico em `.github/workflows/functions-tests.yml`:
524
-
525
- ```yaml
526
- name: functions-tests
527
- on:
528
- pull_request:
529
- jobs:
530
- build:
531
- runs-on: ubuntu-latest
532
- steps:
533
- - uses: actions/checkout@v4
534
- - uses: supabase/setup-cli@v1
535
- with:
536
- version: latest
537
- - uses: denoland/setup-deno@v2
538
- with:
539
- deno-version: latest
540
- - run: supabase start
541
- - name: Generate .env.local
542
- run: supabase status -o env > supabase/functions/.env.local
543
- - run: deno test --allow-all supabase/functions/_tests/ --env-file supabase/functions/.env.local
544
- ```
545
-
546
- Required check `functions-tests / build` deve ser obrigatório em branch protection rule para `main`.
547
-
548
- ### Caveat — fixtures DB para Edge Function tests
549
-
550
- Edge Function pode escrever em DB. Tests precisam:
551
-
552
- 1. Inserir fixtures via SQL antes do test
553
- 2. Limpar após o test (ou cada test recria estado)
554
-
555
- ```typescript
556
- import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
557
-
558
- const supabase = createClient(
559
- Deno.env.get("SUPABASE_URL")!,
560
- Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!, // bypass RLS para setup
561
- );
562
-
563
- Deno.test("create-invite writes to org_invites table", async () => {
564
- // Setup
565
- await supabase.from("organizations").insert({ id: "org-1", name: "Test" });
566
-
567
- // Act
568
- const { data } = await supabase.functions.invoke("create-invite", {
569
- body: { org_id: "org-1", email: "new@x.com", role: "member" },
570
- });
571
-
572
- // Assert
573
- assertEquals(data.success, true);
574
-
575
- const { data: invites } = await supabase
576
- .from("org_invites")
577
- .select("*")
578
- .eq("org_id", "org-1");
579
- assertEquals(invites?.length, 1);
580
-
581
- // Teardown
582
- await supabase.from("org_invites").delete().eq("org_id", "org-1");
583
- await supabase.from("organizations").delete().eq("id", "org-1");
584
- });
585
- ```
586
-
587
- **Caveat:** ao contrário de pgTAP (transactional rollback automático), Deno tests precisam de cleanup MANUAL. Considere padrão `beforeEach/afterEach`:
588
-
589
- ```typescript
590
- async function cleanup() {
591
- await supabase.from("org_invites").delete().neq("id", "00000000-0000-0000-0000-000000000000");
592
- await supabase.from("organizations").delete().neq("id", "00000000-0000-0000-0000-000000000000");
593
- }
594
-
595
- Deno.test({
596
- name: "create-invite test",
597
- async fn() {
598
- await cleanup(); // estado limpo antes
599
- // ... test body ...
600
- await cleanup(); // teardown
601
- },
602
- });
603
- ```
604
-
605
- Ou usar `Deno.test.beforeEach` (Deno 1.40+):
606
-
607
- ```typescript
608
- Deno.test.beforeEach(cleanup);
609
- Deno.test.afterEach(cleanup);
610
-
611
- Deno.test("create-invite writes to org_invites", async () => {
612
- // ...
613
- });
614
- ```
615
-
616
- ## Pattern 4: Cross-ref legacy-characterizer — pgTAP como mecanismo de characterization (TEST-04)
617
-
618
- ### Princípio canônico (Feathers cap 13)
619
-
620
- **Legacy code = código sem testes** (definição Feathers, não estética). PG function/trigger/policy com > 100 linhas e sem tests = legacy, mesmo se escrita ontem.
621
-
622
- Antes de refatorar PG function legada, **characterize first** — capture comportamento atual como pgTAP tests. Tests viram oracle imutável. Refactor preserva oracle. Bug fix vem em PR separado depois.
623
-
624
- ### Workflow canônico de characterization de PG function
625
-
626
- ```text
627
- 1. Identificar PG function alvo do refactor
628
- Exemplo: public.calculate_invoice_total(uuid) — 200 linhas, sem tests
629
-
630
- 2. Inventariar inputs/outputs
631
- Inputs:
632
- - parâmetro: invoice_id uuid
633
- - reads: invoices, line_items, discounts, taxes (4 tabelas)
634
- - reads globais: current_setting('app.tax_rate')
635
- Outputs:
636
- - return: numeric(10,2)
637
- - side effects: insert em audit_log + update em invoices.calculated_at
638
-
639
- 3. Para cada grupo de equivalência (5+ inputs):
640
- a. Construir input (fixture)
641
- b. Executar function REAL — sem mocks ainda
642
- c. Capturar output completo + side effects
643
- d. REVISAR linha por linha — marcar bugs conhecidos como comments
644
- e. Salvar como pgTAP assertion
645
-
646
- 4. Escrever pgTAP test file
647
- - plan(N) declara N grupos × M assertions cada
648
- - results_eq para return value
649
- - results_eq para side effects (linhas em audit_log)
650
- - throws_ok para edge cases que esperam erro
651
-
652
- 5. supabase test db — TODOS verdes = baseline estabelecido
653
-
654
- 6. Refactor pode começar
655
- ```
656
-
657
- ### Exemplo canônico
658
-
659
- ```sql
660
- -- supabase/tests/calculate_invoice_total_characterization_test.sql
661
- begin;
662
- select plan(10);
663
-
664
- -- ===== GRUPO 1: invoice típica com 1 line item =====
665
- -- Setup
666
- insert into public.invoices (id, customer_id, status) values
667
- ('inv-001', 'cust-001', 'pending');
668
- insert into public.line_items (invoice_id, sku, qty, unit_price) values
669
- ('inv-001', 'SKU-1', 2, 50.00);
670
-
671
- -- Test 1: return value
672
- select results_eq(
673
- $$select public.calculate_invoice_total('inv-001'::uuid)$$,
674
- $$values (100.00::numeric)$$,
675
- 'GROUP 1: typical invoice 1 line item — total 100.00'
676
- );
677
-
678
- -- Test 2: side effect — audit_log row created
679
- select results_eq(
680
- $$select event_type, target_id from public.audit_log where target_id = 'inv-001'::uuid$$,
681
- $$values ('invoice_total_calculated'::text, 'inv-001'::uuid)$$,
682
- 'GROUP 1: audit_log row created'
683
- );
684
-
685
- -- ===== GRUPO 2: invoice com discount =====
686
- insert into public.invoices (id, customer_id, status) values
687
- ('inv-002', 'cust-001', 'pending');
688
- insert into public.line_items (invoice_id, sku, qty, unit_price) values
689
- ('inv-002', 'SKU-2', 1, 200.00);
690
- insert into public.discounts (invoice_id, percentage) values
691
- ('inv-002', 10.0); -- 10% discount
692
-
693
- select results_eq(
694
- $$select public.calculate_invoice_total('inv-002'::uuid)$$,
695
- $$values (180.00::numeric)$$,
696
- 'GROUP 2: invoice with 10% discount — total 180.00'
697
- );
698
-
699
- -- ===== GRUPO 3: invoice vazia (BUG #1 conhecido — retorna 0, deveria raise) =====
700
- insert into public.invoices (id, customer_id, status) values
701
- ('inv-003', 'cust-001', 'pending');
702
- -- NO line_items inseridos
703
-
704
- -- BUG #1: function retorna 0 para invoice vazia, deveria raise 'invalid_invoice'
705
- -- Preservar comportamento bugado durante refactor; fix em PR separado.
706
- select results_eq(
707
- $$select public.calculate_invoice_total('inv-003'::uuid)$$,
708
- $$values (0.00::numeric)$$,
709
- 'GROUP 3 [BUG #1]: empty invoice returns 0 (should raise — preserved as oracle)'
710
- );
711
-
712
- -- ===== GRUPO 4: invoice inexistente — espera erro =====
713
- select throws_ok(
714
- $$select public.calculate_invoice_total('00000000-0000-0000-0000-000000000000'::uuid)$$,
715
- 'P0001',
716
- 'GROUP 4: non-existent invoice raises P0001'
717
- );
718
-
719
- -- ===== GRUPO 5: invoice com NEG line items (edge case histórico) =====
720
- insert into public.invoices (id, customer_id, status) values
721
- ('inv-005', 'cust-001', 'pending');
722
- insert into public.line_items (invoice_id, sku, qty, unit_price) values
723
- ('inv-005', 'SKU-3', -1, 100.00); -- qty NEGATIVO (devolução)
724
-
725
- select results_eq(
726
- $$select public.calculate_invoice_total('inv-005'::uuid)$$,
727
- $$values (-100.00::numeric)$$,
728
- 'GROUP 5: negative qty produces negative total (refund flow)'
729
- );
730
-
731
- -- ... outros grupos (boundary valid, side-effect heavy, etc.) ...
732
-
733
- select * from finish();
734
- rollback;
735
- ```
736
-
737
- ### Bugs preservados como comments
738
-
739
- **Crítico:** characterization captura o que código FAZ, não o que DEVERIA fazer. Bugs conhecidos viram comments inline (`-- BUG #X: deveria Y, é Z`). Refactor preserva o oracle (incluindo bugs). Bug fix vem em PR separado **depois** do refactor, com seu próprio teste.
740
-
741
- Exemplo do test acima: GROUP 3 marca `BUG #1` — função retorna 0 para invoice vazia em vez de raise exception. Refactor manterá esse comportamento. PR de bug fix subsequente alterará a assertion para `throws_ok` e fixará a função.
742
-
743
- ### Behavioral coverage check (mutation testing — recomendado)
744
-
745
- pgTAP cobre o **que** o código faz; mutation testing valida que tests **detectam regressão**. Para PG functions críticas:
746
-
747
- 1. Rodar `supabase test db` — baseline verde
748
- 2. Aplicar mutation (manualmente — não há tool padrão para PG mutation; use search-replace deliberado):
749
- - Trocar `+` por `-` na cálculo
750
- - Trocar `>` por `>=` em CHECK
751
- - Comentar `RAISE EXCEPTION` em edge case
752
- 3. Rodar `supabase test db` — esperado vermelho. Se ficar verde, characterization tem ponto cego.
753
- 4. Adicionar test que cobre o ponto cego
754
- 5. Reverter mutation; suite volta a verde
755
-
756
- Cross-ref skill `ai-mutation-tester` (v1.20) — pattern análogo para JavaScript/TypeScript.
757
-
758
- ### Quando pgTAP characterization é mandatório
759
-
760
- Aplicar pgTAP characterization (cross-ref skill `pre-refactor-characterization` v1.18) ANTES de refactor de:
761
-
762
- - PG function > 100 linhas sem tests existentes
763
- - PG function consumida por > 3 callers (alto blast radius se regredir)
764
- - RLS policy complexa (> 5 OR conditions, ou referencia auth.uid() + claim + subquery)
765
- - Trigger BEFORE/AFTER que muta dados em outras tabelas
766
- - Function exposta via Edge Function ou PostgREST RPC (contrato externo)
767
-
768
- ## Anti-patterns
769
-
770
- ### Anti-pattern 1: Tests sem `rollback`
771
-
772
- **Errado:**
773
-
774
- ```sql
775
- -- supabase/tests/employees_test.sql
776
- select plan(2);
777
-
778
- select has_table('public', 'employees', 'employees exists');
779
-
780
- insert into public.employees (name) values ('Test User'); -- SEM begin/rollback!
781
-
782
- select results_eq(
783
- 'select count(*) from public.employees where name = ''Test User''',
784
- 'values (1::bigint)',
785
- 'inserted user found'
786
- );
787
-
788
- select * from finish();
789
- ```
790
-
791
- **Por quê:** `INSERT` sem `begin; ... rollback;` é **persistido** no DB local. Após rodar `supabase test db`:
792
-
793
- - Teste pode "passar" na primeira run mas falhar na segunda (`count` já não é 1, é 2)
794
- - DB local poluído com dados sintéticos — afeta outros tests
795
- - Se rodar em CI, novo container DB cada run = OK; mas localmente é problema
796
-
797
- **Certo:**
798
-
799
- ```sql
800
- -- supabase/tests/employees_test.sql
801
- begin; -- inicia transação
802
- select plan(2);
803
-
804
- select has_table('public', 'employees', 'employees exists');
805
-
806
- insert into public.employees (name) values ('Test User');
807
-
808
- select results_eq(
809
- 'select count(*) from public.employees where name = ''Test User''',
810
- 'values (1::bigint)',
811
- 'inserted user found'
812
- );
813
-
814
- select * from finish();
815
- rollback; -- desfaz INSERT
816
- ```
817
-
818
- `rollback` é **incondicional** — mesmo se `finish()` falhar, transação aborta no rollback. Estado DB inalterado.
819
-
820
- ### Anti-pattern 2: Esquecer `plan(N)` — testes silenciosos
821
-
822
- **Errado:**
823
-
824
- ```sql
825
- begin;
826
- -- SEM plan() declarado
827
-
828
- select has_table('public', 'employees', 'test 1');
829
- select has_column('public', 'employees', 'name', 'test 2');
830
- select col_type_is('public', 'employees', 'name', 'text', 'test 3');
831
-
832
- select * from finish();
833
- rollback;
834
- ```
835
-
836
- **Por quê:** sem `plan(N)`, pgTAP **não sabe** quantos tests esperar. Output ainda mostra `ok 1`, `ok 2`, `ok 3` mas:
837
-
838
- - TAP harness pode interpretar como "0 tests planned, 3 executed" → falha silenciosa em CI
839
- - `finish()` reporta `1..0` (planejados zero) — exit code pode ser 0 (sucesso) mesmo se tests **falharem** silenciosamente
840
- - Adicionar test novo não é notado (deveria mudar `plan(N)` para `plan(N+1)`, mas como não tem plan, esquecer é invisível)
841
-
842
- **Certo:**
843
-
844
- ```sql
845
- begin;
846
- select plan(3); -- DECLARA EXPLICITAMENTE 3 tests
847
-
848
- select has_table('public', 'employees', 'test 1');
849
- select has_column('public', 'employees', 'name', 'test 2');
850
- select col_type_is('public', 'employees', 'name', 'text', 'test 3');
851
-
852
- select * from finish();
853
- rollback;
854
- ```
855
-
856
- `plan(N)` é **obrigatório**. Se rodar 4 mas declarou 3, pgTAP reporta `# Looks like you planned 3 tests but ran 4.` — falha.
857
-
858
- Manter `plan(N)` atualizado é parte do contrato — se adiciona test, atualiza plan.
859
-
860
- ### Anti-pattern 3: Tests Deno sem `.env.local`
861
-
862
- **Errado:**
863
-
864
- ```bash
865
- # rodar tests sem env file
866
- deno test --allow-all supabase/functions/_tests/
867
- ```
868
-
869
- ```typescript
870
- // dentro do test
871
- const response = await fetch(`${Deno.env.get("SUPABASE_URL")}/functions/v1/hello`);
872
- // Deno.env.get("SUPABASE_URL") retorna undefined
873
- // fetch("undefined/functions/v1/hello") = URL inválida
874
- // Error: Invalid URL: 'undefined/functions/v1/hello'
875
- ```
876
-
877
- **Por quê:** sem `--env-file`, `Deno.env.get(...)` retorna `undefined`. `fetch` com URL inválida lança erro genérico. Mensagem confusa — dev pensa que tem bug no handler, na verdade é env config.
878
-
879
- **Certo:**
880
-
881
- ```bash
882
- # Gerar .env.local
883
- supabase status -o env > supabase/functions/.env.local
884
-
885
- # Rodar com env file
886
- deno test --allow-all supabase/functions/_tests/ --env-file supabase/functions/.env.local
887
- ```
888
-
889
- ```typescript
890
- // Defensive: assert env vars existem (fail-fast com mensagem clara)
891
- const SUPABASE_URL = Deno.env.get("SUPABASE_URL");
892
- const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY");
893
-
894
- if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
895
- throw new Error(
896
- "Missing SUPABASE_URL or SUPABASE_ANON_KEY. " +
897
- "Run: supabase status -o env > supabase/functions/.env.local"
898
- );
899
- }
900
- ```
901
-
902
- Em CI, geração de `.env.local` deve ser **step explícito** do workflow (cross-ref Phase 151 CI-06):
903
-
904
- ```yaml
905
- - name: Generate .env.local
906
- run: supabase status -o env > supabase/functions/.env.local
907
- - run: deno test --allow-all supabase/functions/_tests/ --env-file supabase/functions/.env.local
908
- ```
909
-
910
- ### Anti-pattern 4: Tratar pgTAP como "testes completos"
911
-
912
- **Errado:** time considera que pgTAP + Deno tests cobrem TODO o sistema → para CI: "se database-tests e functions-tests passam, deploy é seguro".
913
-
914
- **Por quê:** pgTAP cobre **schema e PG logic**; Deno tests cobrem **Edge Function HTTP handlers**. NENHUM dos dois cobre:
915
-
916
- - Cliente frontend interagindo com Supabase via `@supabase/supabase-js` (race conditions, retry, optimistic UI)
917
- - Comportamento Realtime (subscriptions, broadcast, presence)
918
- - Auth flows end-to-end (signup → email confirm → first login → setup wizard)
919
- - RLS visto do ponto de vista do cliente autenticado (JWT real, não simulado via `set_config`)
920
- - Performance / latência sob carga
921
- - Edge cases de browser (CORS, cookies, localStorage)
922
-
923
- **Certo:** treat pgTAP + Deno como **two layers of defense-in-depth**, não substituto de integration tests:
924
-
925
- ```text
926
- Pirâmide de testes Supabase (canônica):
927
-
928
- /\
929
- /E2E\ ← Playwright/Cypress: 5-10 critical user journeys
930
- /------\
931
- / INT. \ ← Vitest/Jest com Supabase real: auth flow, multi-tenant
932
- /----------\
933
- / DENO TESTS \ ← pattern 3: Edge Functions HTTP handlers
934
- /--------------\
935
- / pgTAP \ ← pattern 1: schema + RLS + PG functions
936
- /------------------\
937
- | Unit tests app | ← Vitest/Jest cliente: componentes React, helpers
938
- ```
939
-
940
- Cada camada cobre **incidentes diferentes**:
941
- - pgTAP: regressão em schema/policy/function
942
- - Deno: regressão em Edge Function logic
943
- - Integration: regressão em cliente ↔ Supabase wire
944
- - E2E: regressão em user journey
945
-
946
- Sem todas as camadas, gaps de cobertura existem. CI deve ter **separate required checks** por camada.
947
-
948
- ### Anti-pattern 5: Snapshot pgTAP sem revisão (characterization shortcut)
949
-
950
- **Errado:** copy-paste output bruto de query como `results_eq` expected sem inspecionar — "se runs verde, está OK".
951
-
952
- ```sql
953
- -- characterization sem revisão
954
- select results_eq(
955
- $$select public.complex_function('input-1')$$,
956
- $$values ('a', '2026-05-11T10:23:45.123456Z'::timestamptz, 'token_abc123xyz', 42, '00000000-1234-5678-9abc-def012345678'::uuid)$$,
957
- 'preserved as-is'
958
- );
959
- ```
960
-
961
- **Por quê:** output bruto pode incluir:
962
-
963
- - **PII** (emails, names) — vazam para git history
964
- - **Tokens/secrets** — vazam para git history (irreversível)
965
- - **Timestamps voláteis** — test será flaky em run subsequente
966
- - **UUIDs locais** — gerados por `gen_random_uuid()`, mudam a cada run
967
-
968
- CI fica "verde" porque snapshot bate consigo mesmo da última run, mas:
969
- - Adicionar fixture nova quebra (timestamp diferente)
970
- - PR review humano não detecta bugs (não inspecionou output)
971
- - Secrets podem ser commitados sem aviso
972
-
973
- **Certo:** revisão linha-por-linha + sanitização antes de salvar (cross-ref skill `legacy-characterization-tests` Pattern 6):
974
-
975
- ```sql
976
- -- characterization com sanitização determinística
977
- begin;
978
- -- 1. Fixar clock para determinismo
979
- select set_config('app.fake_now', '2026-05-11T10:00:00Z', true);
980
-
981
- -- 2. Fixar UUIDs determinísticos via fixture
982
- insert into public.things (id, name, created_at) values
983
- ('00000000-0000-0000-0000-000000000001', 'Test', '2026-05-11T10:00:00Z'::timestamptz);
984
-
985
- -- 3. Snapshot review:
986
- -- - Email 'alice@x.com' está no fixture (não real PII)
987
- -- - Timestamp '2026-05-11T10:00:00Z' está congelado (fake clock)
988
- -- - UUID '00000000...001' está determinístico (fixture)
989
- -- - Token 'token_test_abc' está sanitized (não real)
990
- select results_eq(
991
- $$select id, name, created_at from public.things where id = '00000000-0000-0000-0000-000000000001'::uuid$$,
992
- $$values ('00000000-0000-0000-0000-000000000001'::uuid, 'Test'::text, '2026-05-11T10:00:00Z'::timestamptz)$$,
993
- 'thing fixture matches characterization oracle (reviewed 2026-05-11)'
994
- );
995
-
996
- select * from finish();
997
- rollback;
998
- ```
999
-
1000
- **Crítico:** comentar quando snapshot foi revisado + por quem. Commit message: "characterize complex_function — reviewed 2026-05-11, bugs noted in comments".
1001
-
1002
- ## Cross-suite integration (v1.27)
1003
-
1004
- Esta skill é **complemento essencial** da skill Phase 151 `supabase-ci-cd-github-actions`:
1005
-
1006
- - Phase 151 estabelece workflows CI (`database-tests.yml` + `functions-tests.yml`) que executam `supabase test db` + `deno test --allow-all`
1007
- - Phase 152 (ESTA) detalha a **sintaxe canônica** dos tests que esses workflows consomem
1008
- - Forward-ref de Phase 151 é fechado por esta skill (Pattern 5 da Phase 151 referenciava "skill futura `supabase-pgtap-testing` Phase 152")
1009
-
1010
- Cross-refs com skills existentes v1.x:
1011
-
1012
- - **`legacy-characterization-tests` (v1.16)** — pgTAP é o mecanismo canônico para implementar characterization (cap 13 Feathers) em PG legado; Pattern 4 desta skill é especialização para Postgres
1013
- - **`pre-refactor-characterization` (v1.18)** — gate auto-trigger que bloqueia refactor sem characterization; pgTAP satisfaz a pré-condição para PG functions
1014
- - **`ai-mutation-tester` (v1.20)** — mutation testing complementar; valida que pgTAP detecta regressão
1015
- - **`supabase-database-functions`** — PG functions criadas seguindo essa skill são prime target de pgTAP (SECURITY INVOKER + SET search_path = '' são testáveis)
1016
- - **`supabase-rls-policies` (v1.23)** — RLS policies validadas via `throws_like '%permission denied%'` + `results_eq` com diferentes JWT claims
1017
- - **`supabase-edge-functions`** — Edge Functions cobertas por Deno tests (Pattern 3)
1018
- - **`supabase-postgres-roles` (v1.26)** — testes de roles validáveis via `has_role`, `set role test_role; ...; reset role`
1019
- - **`supabase-custom-claims-rbac` (v1.25)** — testes de auth hook validáveis via fixture JWT + `set_config('request.jwt.claim.user_role', 'admin', true)`
1020
-
1021
- Base para agent futuro v1.27:
1022
-
1023
- - **`supabase-cicd-pipeline-implementer` (Phase 154, futura)** — agent que materializa workflows + tests; consome esta skill para gerar test files iniciais junto com migrations
1024
-
1025
- Pattern de handoff cooperativo herdado v1.23-v1.26: **architect** projeta strategy → **test-writer** materializa pgTAP/Deno tests → **release-pipeline-auditor** (v1.10) audita coverage do pipeline. Nenhum agente descarta upstream — handoff cooperativo (princípio canônico v1.23).
1026
-
1027
- ### Casos de uso por agent
1028
-
1029
- | Agent | Como consome esta skill |
1030
- |-------|-------------------------|
1031
- | `supabase-migration-writer` | Gera migration + pgTAP test inicial (has_table, has_column, has_pk) |
1032
- | `supabase-rls-writer` (v1.23) | Gera RLS policies + pgTAP test (throws_like '%permission denied%' para cross-tenant) |
1033
- | `supabase-edge-fn-writer` | Gera Edge Function + Deno test (handler retorna 200/400 esperado) |
1034
- | `legacy-characterizer` | Gera pgTAP characterization tests para PG function legada (cap 13 Feathers) |
1035
- | `refactor-safety-auditor` | Verifica existência de pgTAP tests antes de permitir refactor PG |
1036
- | `supabase-cicd-pipeline-implementer` (Phase 154 futura) | Gera workflows `database-tests.yml` + `functions-tests.yml` + tests iniciais |
1037
-
1038
- ## Ver também
1039
-
1040
- - [supabase-ci-cd-github-actions](../supabase-ci-cd-github-actions/SKILL.md) (v1.27, Phase 151) — workflows CI que executam `supabase test db` + `deno test`
1041
- - [legacy-characterization-tests](../legacy-characterization-tests/SKILL.md) (v1.16) — cap 13 Feathers; pgTAP é mecanismo canônico para Postgres
1042
- - [pre-refactor-characterization](../pre-refactor-characterization/SKILL.md) (v1.18) — gate que pgTAP satisfaz para PG functions
1043
- - [supabase-database-functions](../supabase-database-functions/SKILL.md) — PG functions testáveis via pgTAP
1044
- - [supabase-rls-policies](../supabase-rls-policies/SKILL.md) (v1.23) — RLS testável via `throws_like` + `set_config jwt claim`
1045
- - [supabase-rls-defense-in-depth](../supabase-rls-defense-in-depth/SKILL.md) (v1.23) — Camadas testáveis em pgTAP
1046
- - [supabase-edge-functions](../supabase-edge-functions/SKILL.md) — Edge Functions cobertas por Deno tests
1047
- - [supabase-postgres-roles](../supabase-postgres-roles/SKILL.md) (v1.26) — `has_role`, `set role` testáveis
1048
- - [supabase-custom-claims-rbac](../supabase-custom-claims-rbac/SKILL.md) (v1.25) — fixture JWT via `set_config('request.jwt.claim.*')`
1049
- - [supabase-migrations](../supabase-migrations/SKILL.md) (v1.23) — migration cria tabela; pgTAP test valida
1050
- - [ai-mutation-tester](../../agents/ai-mutation-tester.md) (v1.20) — mutation testing complementar
1051
- - [glossário compartilhado](../_shared-supabase/glossary.md) — termos pgTAP, TAP, plan(N), throws_ok, results_eq, supabase test db, deno test --allow-all
1052
- - Doc oficial pgTAP: [pgTAP Documentation](https://pgtap.org/documentation.html)
1053
- - Doc oficial Supabase: [Testing with pgTAP](https://supabase.com/docs/guides/database/extensions/pgtap), [Testing Edge Functions](https://supabase.com/docs/guides/functions/unit-test)
1
+ ---
2
+ name: supabase-pgtap-testing
3
+ description: Use ao escrever testes de database/Edge Function em Supabase — pgTAP extension (TAP…
4
+ ---
5
+
6
+ # Supabase — pgTAP & Deno Testing
7
+
8
+ ## Quando usar
9
+
10
+ Esta skill cobre **testing-shift-left para Supabase** — testar schema, RLS, PG functions e Edge Functions ANTES de deploy via CI. Dois runners canônicos:
11
+
12
+ 1. **pgTAP** (`supabase test db`) — database tests via TAP (Test Anything Protocol) Postgres extension
13
+ 2. **Deno test** (`deno test --allow-all`) — Edge Functions tests via Deno runtime nativo
14
+
15
+ Trigger phrases:
16
+
17
+ - "testes Supabase", "test Supabase", "testing Supabase"
18
+ - "pgTAP", "supabase test db", "TAP Postgres extension"
19
+ - "plan ok is throws_ok results_eq pgTAP", "has_table has_column col_type_is"
20
+ - "Deno test Edge Function", "deno test --allow-all"
21
+ - "testar RLS Supabase", "characterization tests PG function"
22
+ - "supabase/tests/", "supabase/functions/_tests/"
23
+ - "testar trigger Postgres", "testar function Postgres"
24
+ - ".env.local Deno tests", "SUPABASE_URL SUPABASE_ANON_KEY tests"
25
+ - "characterization PG legado", "refatorar function Postgres com tests"
26
+
27
+ **Use APENAS para:**
28
+
29
+ - Validar schema, columns, tipos, FKs via pgTAP (`has_table`, `has_column`, `col_type_is`)
30
+ - Validar RLS policies retornando `permission denied` quando esperado (`throws_ok 42501`)
31
+ - Validar PG functions retornando outputs esperados (`results_eq`)
32
+ - Validar constraints, triggers, defaults (`throws_ok 23502 not_null_violation`, `throws_ok 23505 unique_violation`)
33
+ - Testar Edge Functions HTTP handlers (status, response body, side effects)
34
+ - Capturar comportamento atual de PG function legada como **characterization oracle** (Feathers cap 13)
35
+ - Integrar com workflow CI `database-tests.yml` (Phase 151 CI-05) e `functions-tests.yml` (CI-06)
36
+
37
+ **NÃO use para:**
38
+
39
+ - Substituir integration tests no app layer (frontend ↔ Supabase via client SDK) — pgTAP cobre só DB; Deno tests cobrem só Edge Function isolado
40
+ - Performance tests (latência, throughput) — pgTAP é assertion-based, não benchmark; use `pgbench` ou Vegeta para load testing
41
+ - Tests em produção sem `rollback` — sempre `begin; ... rollback;` (cross-ref Anti-pattern 1)
42
+ - Substituir auditoria via `auditor-consistencia-isolamento` (v1.22) — tests validam comportamento esperado; auditor detecta anti-patterns estáticos no SQL
43
+ - Substituir mutation testing (`ai-mutation-tester`) — pgTAP é characterization; mutation testing valida que tests realmente detectam regressão
44
+
45
+ ## Princípio canônico
46
+
47
+ Três princípios canônicos:
48
+
49
+ 1. **Testing shift-left.** Cada PR valida schema + RLS + PG functions + Edge Functions ANTES do merge. Gate canônico = required status check `database-tests / build` e `functions-tests / build` enforced via branch protection (cross-ref skill `supabase-ci-cd-github-actions` Phase 151 patterns 5 e 6).
50
+
51
+ 2. **Tests são schema mutations transacionais.** Todo teste pgTAP roda dentro de `begin; ... rollback;` — assertions executam, fixtures são inseridas, RLS é exercitada, mas DB volta ao estado original. Nunca polui DB compartilhado; tests podem rodar em paralelo sem race.
52
+
53
+ 3. **pgTAP é o oracle imutável para refactor de PG legado.** Quando PG function complexa (> 100 linhas, sem tests) precisa ser refatorada, pgTAP captura comportamento atual como characterization (Feathers cap 13). Tests viram oracle congelado — qualquer mudança que altere comportamento previamente capturado falha o build. Sem characterization, refactor de PG é "edit and pray" (cross-ref skill `legacy-characterization-tests` v1.16).
54
+
55
+ ### Distinção canônica vs outros runners
56
+
57
+ | | pgTAP (`supabase test db`) | Deno test (`deno test`) | Vitest/Jest (app layer) |
58
+ |---|---|---|---|
59
+ | Escopo | Schema, RLS, PG functions, triggers | Edge Functions HTTP handlers | Frontend + cliente Supabase |
60
+ | Linguagem | SQL (TAP syntax) | TypeScript (Deno runtime) | TypeScript (Node runtime) |
61
+ | Isolamento | `begin; ... rollback;` (atomic) | Function call por test | Mocked client OR real (E2E) |
62
+ | Onde roda | Postgres local (via `supabase start`) | Deno runtime + Postgres local | Node + browser environment |
63
+ | CI workflow | `database-tests.yml` (Phase 151 CI-05) | `functions-tests.yml` (CI-06) | Custom `vitest.yml` ou `jest.yml` |
64
+ | Trigger canônico | PR (gate) | PR (gate) | PR + post-merge (canary) |
65
+
66
+ Os três runners são **complementares** — pgTAP valida DB, Deno valida Edge Function HTTP layer, Vitest/Jest valida cliente. Nenhum substitui os outros.
67
+
68
+ ## Pattern 1: pgTAP extension setup + sintaxe canônica (TEST-01)
69
+
70
+ ### Habilitar pgTAP
71
+
72
+ Em migration dedicada (uma única vez por projeto):
73
+
74
+ ```sql
75
+ -- supabase/migrations/YYYYMMDDHHmmss_enable_pgtap.sql
76
+ create extension if not exists pgtap with schema extensions;
77
+ ```
78
+
79
+ **Por que `with schema extensions`:** convenção Supabase canônica — todas extensions ficam em schema dedicado (`extensions`), separadas de `public`. Permite namespace isolation + GRANT EXECUTE granular.
80
+
81
+ ### Diretório canônico `supabase/tests/`
82
+
83
+ Supabase CLI busca automaticamente arquivos `*.sql` em `supabase/tests/` quando executa `supabase test db`:
84
+
85
+ ```text
86
+ supabase/
87
+ ├── migrations/
88
+ │ ├── 20260101000000_initial_schema.sql
89
+ │ └── 20260102000000_enable_pgtap.sql
90
+ ├── tests/
91
+ │ ├── employees_test.sql # ← descoberto automaticamente
92
+ │ ├── rls_organizations_test.sql # ← descoberto automaticamente
93
+ │ └── triggers_audit_test.sql # ← descoberto automaticamente
94
+ └── functions/
95
+ ```
96
+
97
+ Naming convention: `<entity>_test.sql` ou `<concern>_test.sql` — nome descritivo + sufixo `_test.sql`. CLI roda em ordem alfabética.
98
+
99
+ ### Estrutura canônica de um teste pgTAP
100
+
101
+ ```sql
102
+ -- supabase/tests/employees_test.sql
103
+ begin; -- inicia transação
104
+ select plan(4); -- declara 4 testes (CRÍTICO — sem plan, falha silencioso)
105
+
106
+ -- Test 1: tabela existe
107
+ select has_table('public', 'employees', 'employees table should exist');
108
+
109
+ -- Test 2: coluna existe com tipo correto
110
+ select has_column('public', 'employees', 'name', 'name column should exist');
111
+ select col_type_is('public', 'employees', 'name', 'text', 'name should be text');
112
+
113
+ -- Test 3: comportamento de seed
114
+ select results_eq(
115
+ 'select count(*) from public.employees',
116
+ 'values (3::bigint)',
117
+ 'employees count should be 3 after seed'
118
+ );
119
+
120
+ -- Test 4: constraint NOT NULL
121
+ select throws_ok(
122
+ 'insert into public.employees (name) values (null)',
123
+ '23502', -- SQLSTATE not_null_violation
124
+ 'null in name should violate not-null constraint'
125
+ );
126
+
127
+ select * from finish(); -- finaliza plano (gera summary TAP)
128
+ rollback; -- desfaz qualquer side effect
129
+ ```
130
+
131
+ **Crítico:** `plan(N)` declara o número exato de assertions executadas. Se você roda 5 mas declarou 4, pgTAP reporta `# Looks like you planned 4 tests but ran 5.` — teste falha. Se rodar menos do que planejou, idem.
132
+
133
+ ### Funções pgTAP canônicas
134
+
135
+ #### Schema/structure assertions
136
+
137
+ ```sql
138
+ -- tabela existe
139
+ select has_table('public', 'orders', 'orders table exists');
140
+
141
+ -- tabela NÃO existe
142
+ select hasnt_table('public', 'old_table', 'old_table was dropped');
143
+
144
+ -- coluna existe
145
+ select has_column('public', 'orders', 'total', 'total column exists');
146
+
147
+ -- tipo da coluna
148
+ select col_type_is('public', 'orders', 'total', 'numeric(10,2)', 'total is numeric(10,2)');
149
+
150
+ -- NOT NULL
151
+ select col_not_null('public', 'orders', 'customer_id', 'customer_id is NOT NULL');
152
+
153
+ -- DEFAULT
154
+ select col_has_default('public', 'orders', 'created_at', 'created_at has default');
155
+
156
+ -- PRIMARY KEY
157
+ select has_pk('public', 'orders', 'orders has primary key');
158
+
159
+ -- FOREIGN KEY
160
+ select has_fk('public', 'orders', 'orders.customer_id references customers');
161
+
162
+ -- INDEX
163
+ select has_index('public', 'orders', 'orders_customer_id_idx', 'index exists');
164
+ ```
165
+
166
+ #### Function/trigger assertions
167
+
168
+ ```sql
169
+ -- function existe
170
+ select has_function('public', 'calculate_total', array['uuid'], 'function exists');
171
+
172
+ -- return type
173
+ select function_returns('public', 'calculate_total', 'numeric', 'returns numeric');
174
+
175
+ -- trigger existe
176
+ select has_trigger('public', 'orders', 'set_updated_at', 'trigger exists');
177
+
178
+ -- enum value
179
+ select has_enum('public', 'order_status', array['pending', 'paid', 'shipped'], 'enum values');
180
+ ```
181
+
182
+ #### Behavioral assertions
183
+
184
+ ```sql
185
+ -- equality (simple)
186
+ select is(1 + 1, 2, '1+1 equals 2');
187
+
188
+ -- inequality
189
+ select isnt(now(), '2020-01-01'::timestamp, 'now is not 2020');
190
+
191
+ -- boolean check
192
+ select ok(exists(select 1 from public.users where id = '...'), 'user exists');
193
+
194
+ -- equality (query result)
195
+ select results_eq(
196
+ 'select id, name from public.users where active = true order by id limit 2',
197
+ $$values ('uuid-1'::uuid, 'Alice'), ('uuid-2'::uuid, 'Bob')$$,
198
+ 'active users are Alice and Bob'
199
+ );
200
+
201
+ -- inequality (query result)
202
+ select results_ne(
203
+ 'select count(*) from public.deleted_users',
204
+ 'values (0::bigint)',
205
+ 'deleted_users is not empty'
206
+ );
207
+
208
+ -- subset (query result is subset)
209
+ select set_eq(
210
+ 'select email from public.users',
211
+ $$values ('a@x.com'), ('b@x.com'), ('c@x.com')$$,
212
+ 'all users have correct emails'
213
+ );
214
+ ```
215
+
216
+ #### Error/exception assertions
217
+
218
+ ```sql
219
+ -- erro específico
220
+ select throws_ok(
221
+ 'insert into public.employees (name) values (null)',
222
+ '23502', -- SQLSTATE not_null_violation
223
+ 'null name should fail not-null constraint'
224
+ );
225
+
226
+ -- error matching pattern
227
+ select throws_like(
228
+ 'select * from public.private_table',
229
+ '%permission denied%',
230
+ 'cross-org SELECT raises permission denied'
231
+ );
232
+
233
+ -- NÃO lança erro
234
+ select lives_ok(
235
+ $$insert into public.employees (name, email) values ('Alice', 'alice@x.com')$$,
236
+ 'valid insert should succeed'
237
+ );
238
+ ```
239
+
240
+ #### Custom test helpers — fixtures
241
+
242
+ ```sql
243
+ -- helper para criar user JWT context
244
+ create or replace function tests.set_jwt_claim(claim_name text, claim_value text)
245
+ returns void
246
+ language sql
247
+ as $$
248
+ select set_config('request.jwt.claim.' || claim_name, claim_value, true);
249
+ $$;
250
+
251
+ -- uso em teste
252
+ select tests.set_jwt_claim('sub', 'uuid-of-user-1');
253
+ select tests.set_jwt_claim('user_role', 'admin');
254
+
255
+ -- agora policies que consultam auth.uid() retornam 'uuid-of-user-1'
256
+ select results_eq(
257
+ 'select count(*) from public.posts',
258
+ 'values (10::bigint)', -- esperando 10 posts visíveis para este user
259
+ 'user-1 admin sees all posts'
260
+ );
261
+ ```
262
+
263
+ ### SQLSTATE codes canônicos para `throws_ok`
264
+
265
+ | Code | Nome | Caso |
266
+ |------|------|------|
267
+ | `23502` | `not_null_violation` | INSERT/UPDATE com null em column NOT NULL |
268
+ | `23503` | `foreign_key_violation` | Referência inexistente em FK |
269
+ | `23505` | `unique_violation` | Duplicate em UNIQUE/PK |
270
+ | `23514` | `check_violation` | Falha em CHECK constraint |
271
+ | `42501` | `insufficient_privilege` | RLS bloqueando OR GRANT ausente |
272
+ | `22001` | `string_data_right_truncation` | String muito longa para column |
273
+ | `22008` | `datetime_field_overflow` | Date inválida |
274
+ | `P0001` | `raise_exception` | RAISE EXCEPTION explícito em PG function |
275
+
276
+ ## Pattern 2: `supabase test db` runner + integração CI (TEST-02)
277
+
278
+ ### Execução local
279
+
280
+ ```bash
281
+ # Garantir Postgres local rodando (sobe via Docker)
282
+ supabase start
283
+
284
+ # Rodar todos os tests em supabase/tests/
285
+ supabase test db
286
+
287
+ # Output (TAP format):
288
+ # 1..4
289
+ # ok 1 - employees table should exist
290
+ # ok 2 - name column should exist
291
+ # ok 3 - name should be text
292
+ # ok 4 - employees count should be 3 after seed
293
+ # # Pass: 4
294
+ # # Fail: 0
295
+ ```
296
+
297
+ ### Output TAP — anatomia
298
+
299
+ ```text
300
+ 1..4 ← plan (4 tests esperados)
301
+ ok 1 - test description ← test passou
302
+ not ok 2 - test description ← test falhou
303
+ # Failed test 2: 'test description' ← detalhe da falha
304
+ # got: 'actual_value'
305
+ # expected: 'expected_value'
306
+ ok 3 - test description
307
+ ok 4 - test description
308
+ # Looks like you failed 1 test of 4. ← summary
309
+ ```
310
+
311
+ Exit code:
312
+ - **0** — todos tests passaram
313
+ - **1** — algum teste falhou (CI fails → required check fails → merge bloqueado)
314
+ - **3** — erro fatal (DB não disponível, syntax error em arquivo de test)
315
+
316
+ ### Rodar test específico
317
+
318
+ ```bash
319
+ # Apenas um arquivo
320
+ supabase test db supabase/tests/employees_test.sql
321
+
322
+ # Útil para debug rápido durante TDD
323
+ ```
324
+
325
+ ### Integração CI (cross-ref Phase 151 CI-05 `database-tests.yml`)
326
+
327
+ Workflow canônico em `.github/workflows/database-tests.yml`:
328
+
329
+ ```yaml
330
+ name: database-tests
331
+ on:
332
+ pull_request:
333
+ jobs:
334
+ build:
335
+ runs-on: ubuntu-latest
336
+ steps:
337
+ - uses: actions/checkout@v4
338
+ - uses: supabase/setup-cli@v1
339
+ with:
340
+ version: latest
341
+ - run: supabase db start
342
+ - run: supabase test db
343
+ ```
344
+
345
+ **Crítico:**
346
+ - `supabase db start` (não `supabase start`) — sobe apenas Postgres, mais rápido que stack completa
347
+ - `supabase test db` consome `supabase/tests/*.sql` em ordem alfabética
348
+ - Falha em qualquer teste = exit code 1 = workflow falha = required check falha = merge bloqueado
349
+
350
+ Required check `database-tests / build` deve ser obrigatório em branch protection rule para `main` (cross-ref Phase 151 lista de required checks).
351
+
352
+ ### Caveat — fixtures via `supabase/seed.sql`
353
+
354
+ `supabase db start` aplica todas migrations + executa `supabase/seed.sql` (se existir). Use seed para popular dados de fixture compartilhados:
355
+
356
+ ```sql
357
+ -- supabase/seed.sql (rodado uma vez em db start)
358
+ insert into public.employees (id, name, email) values
359
+ ('00000000-0000-0000-0000-000000000001', 'Alice', 'alice@x.com'),
360
+ ('00000000-0000-0000-0000-000000000002', 'Bob', 'bob@x.com'),
361
+ ('00000000-0000-0000-0000-000000000003', 'Carol', 'carol@x.com');
362
+ ```
363
+
364
+ Testes referenciam UUIDs determinísticos:
365
+
366
+ ```sql
367
+ select results_eq(
368
+ 'select name from public.employees where id = ''00000000-0000-0000-0000-000000000001'' ',
369
+ $$values ('Alice')$$,
370
+ 'Alice exists'
371
+ );
372
+ ```
373
+
374
+ **Caveat:** seed roda APENAS em `db start` (não a cada test). Tests que precisam de fixture específica devem inserir dentro do `begin; ... rollback;` para isolar.
375
+
376
+ ### Caveat — paralelismo
377
+
378
+ `supabase test db` roda arquivos **sequencialmente** (não em paralelo). Cada arquivo tem `begin; ... rollback;` próprio — isolamento OK. Mas tempo total cresce linearmente com N arquivos.
379
+
380
+ Mitigação para suítes grandes (> 50 arquivos):
381
+ - Particionar em jobs paralelos no GitHub Actions (matrix strategy)
382
+ - Cada job roda subset de `supabase/tests/<group>/`
383
+
384
+ ## Pattern 3: Deno Edge Function tests (TEST-03)
385
+
386
+ ### Estrutura canônica
387
+
388
+ ```text
389
+ supabase/
390
+ └── functions/
391
+ ├── hello/
392
+ │ └── index.ts # Edge Function code
393
+ ├── create-invite/
394
+ │ └── index.ts
395
+ └── _tests/ # ← convenção (_ prefix evita conflito com fn name)
396
+ ├── hello_test.ts
397
+ └── create-invite_test.ts
398
+ ```
399
+
400
+ Naming convention: `<fn-name>_test.ts` em `supabase/functions/_tests/`.
401
+
402
+ ### Anatomy de um teste Deno
403
+
404
+ ```typescript
405
+ // supabase/functions/_tests/hello_test.ts
406
+ import { assertEquals } from "https://deno.land/std@0.221.0/assert/mod.ts";
407
+
408
+ Deno.test("hello function returns 200", async () => {
409
+ const response = await fetch("http://127.0.0.1:54321/functions/v1/hello", {
410
+ method: "POST",
411
+ headers: {
412
+ "Authorization": `Bearer ${Deno.env.get("SUPABASE_ANON_KEY")}`,
413
+ "Content-Type": "application/json",
414
+ },
415
+ body: JSON.stringify({ name: "World" }),
416
+ });
417
+
418
+ assertEquals(response.status, 200);
419
+ const data = await response.json();
420
+ assertEquals(data.message, "Hello World!");
421
+ });
422
+
423
+ Deno.test("hello function rejects empty body", async () => {
424
+ const response = await fetch("http://127.0.0.1:54321/functions/v1/hello", {
425
+ method: "POST",
426
+ headers: {
427
+ "Authorization": `Bearer ${Deno.env.get("SUPABASE_ANON_KEY")}`,
428
+ "Content-Type": "application/json",
429
+ },
430
+ body: JSON.stringify({}),
431
+ });
432
+
433
+ assertEquals(response.status, 400);
434
+ });
435
+ ```
436
+
437
+ ### Execução local
438
+
439
+ ```bash
440
+ # Sobe stack completa (Postgres + Auth + Edge Functions runtime + Storage)
441
+ supabase start
442
+
443
+ # Rodar tests
444
+ deno test --allow-all supabase/functions/_tests/ --env-file .env.local
445
+ ```
446
+
447
+ `--allow-all` permite acesso file system + network + env vars (Deno é sandboxed por default).
448
+
449
+ `--env-file .env.local` carrega env vars de arquivo local. Sem essa flag, `Deno.env.get(...)` retorna `undefined`.
450
+
451
+ ### Estrutura `.env.local` (gitignored)
452
+
453
+ ```bash
454
+ # supabase/functions/.env.local (gitignored — gerado dinamicamente)
455
+ SUPABASE_URL=http://127.0.0.1:54321
456
+ SUPABASE_ANON_KEY=eyJ...local-anon-key...
457
+ SUPABASE_SERVICE_ROLE_KEY=eyJ...local-service-role-key...
458
+ ```
459
+
460
+ **Caveat:** `supabase start` imprime essas keys no output. Capture-as automaticamente:
461
+
462
+ ```bash
463
+ # Linux/macOS
464
+ supabase status -o env > supabase/functions/.env.local
465
+
466
+ # Windows PowerShell
467
+ supabase status -o env | Out-File -Encoding utf8 supabase/functions/.env.local
468
+ ```
469
+
470
+ Atualizar `.env.local` a cada `supabase start` — keys mudam se containers forem recriados.
471
+
472
+ ### Importar handler diretamente (test unitário)
473
+
474
+ Alternativa ao test via HTTP — importar handler direto:
475
+
476
+ ```typescript
477
+ // supabase/functions/_tests/hello_unit_test.ts
478
+ import { assertEquals } from "https://deno.land/std@0.221.0/assert/mod.ts";
479
+
480
+ // Assumindo hello/index.ts exporta handler
481
+ import { handler } from "../hello/index.ts";
482
+
483
+ Deno.test("handler returns 200 for valid input", async () => {
484
+ const req = new Request("http://localhost/hello", {
485
+ method: "POST",
486
+ headers: { "Content-Type": "application/json" },
487
+ body: JSON.stringify({ name: "World" }),
488
+ });
489
+
490
+ const res = await handler(req);
491
+ assertEquals(res.status, 200);
492
+
493
+ const data = await res.json();
494
+ assertEquals(data.message, "Hello World!");
495
+ });
496
+
497
+ Deno.test("handler returns 400 for invalid input", async () => {
498
+ const req = new Request("http://localhost/hello", {
499
+ method: "POST",
500
+ headers: { "Content-Type": "application/json" },
501
+ body: JSON.stringify({}),
502
+ });
503
+
504
+ const res = await handler(req);
505
+ assertEquals(res.status, 400);
506
+ });
507
+ ```
508
+
509
+ **Vantagens vs HTTP test:**
510
+ - Mais rápido (sem network roundtrip)
511
+ - Testa lógica isolada do handler
512
+ - Não requer `supabase start` (apenas Deno runtime)
513
+
514
+ **Desvantagens:**
515
+ - Não exercita auth middleware (JWT verification)
516
+ - Não exercita rate limiting / CORS / Deno.serve setup
517
+ - Mais frágil a mudanças no boilerplate da Edge Function
518
+
519
+ **Canônico:** combinar AMBOS — unit tests do handler (rápidos) + smoke test via HTTP (validação E2E mínima).
520
+
521
+ ### Integração CI (cross-ref Phase 151 CI-06 `functions-tests.yml`)
522
+
523
+ Workflow canônico em `.github/workflows/functions-tests.yml`:
524
+
525
+ ```yaml
526
+ name: functions-tests
527
+ on:
528
+ pull_request:
529
+ jobs:
530
+ build:
531
+ runs-on: ubuntu-latest
532
+ steps:
533
+ - uses: actions/checkout@v4
534
+ - uses: supabase/setup-cli@v1
535
+ with:
536
+ version: latest
537
+ - uses: denoland/setup-deno@v2
538
+ with:
539
+ deno-version: latest
540
+ - run: supabase start
541
+ - name: Generate .env.local
542
+ run: supabase status -o env > supabase/functions/.env.local
543
+ - run: deno test --allow-all supabase/functions/_tests/ --env-file supabase/functions/.env.local
544
+ ```
545
+
546
+ Required check `functions-tests / build` deve ser obrigatório em branch protection rule para `main`.
547
+
548
+ ### Caveat — fixtures DB para Edge Function tests
549
+
550
+ Edge Function pode escrever em DB. Tests precisam:
551
+
552
+ 1. Inserir fixtures via SQL antes do test
553
+ 2. Limpar após o test (ou cada test recria estado)
554
+
555
+ ```typescript
556
+ import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
557
+
558
+ const supabase = createClient(
559
+ Deno.env.get("SUPABASE_URL")!,
560
+ Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!, // bypass RLS para setup
561
+ );
562
+
563
+ Deno.test("create-invite writes to org_invites table", async () => {
564
+ // Setup
565
+ await supabase.from("organizations").insert({ id: "org-1", name: "Test" });
566
+
567
+ // Act
568
+ const { data } = await supabase.functions.invoke("create-invite", {
569
+ body: { org_id: "org-1", email: "new@x.com", role: "member" },
570
+ });
571
+
572
+ // Assert
573
+ assertEquals(data.success, true);
574
+
575
+ const { data: invites } = await supabase
576
+ .from("org_invites")
577
+ .select("*")
578
+ .eq("org_id", "org-1");
579
+ assertEquals(invites?.length, 1);
580
+
581
+ // Teardown
582
+ await supabase.from("org_invites").delete().eq("org_id", "org-1");
583
+ await supabase.from("organizations").delete().eq("id", "org-1");
584
+ });
585
+ ```
586
+
587
+ **Caveat:** ao contrário de pgTAP (transactional rollback automático), Deno tests precisam de cleanup MANUAL. Considere padrão `beforeEach/afterEach`:
588
+
589
+ ```typescript
590
+ async function cleanup() {
591
+ await supabase.from("org_invites").delete().neq("id", "00000000-0000-0000-0000-000000000000");
592
+ await supabase.from("organizations").delete().neq("id", "00000000-0000-0000-0000-000000000000");
593
+ }
594
+
595
+ Deno.test({
596
+ name: "create-invite test",
597
+ async fn() {
598
+ await cleanup(); // estado limpo antes
599
+ // ... test body ...
600
+ await cleanup(); // teardown
601
+ },
602
+ });
603
+ ```
604
+
605
+ Ou usar `Deno.test.beforeEach` (Deno 1.40+):
606
+
607
+ ```typescript
608
+ Deno.test.beforeEach(cleanup);
609
+ Deno.test.afterEach(cleanup);
610
+
611
+ Deno.test("create-invite writes to org_invites", async () => {
612
+ // ...
613
+ });
614
+ ```
615
+
616
+ ## Pattern 4: Cross-ref legacy-characterizer — pgTAP como mecanismo de characterization (TEST-04)
617
+
618
+ ### Princípio canônico (Feathers cap 13)
619
+
620
+ **Legacy code = código sem testes** (definição Feathers, não estética). PG function/trigger/policy com > 100 linhas e sem tests = legacy, mesmo se escrita ontem.
621
+
622
+ Antes de refatorar PG function legada, **characterize first** — capture comportamento atual como pgTAP tests. Tests viram oracle imutável. Refactor preserva oracle. Bug fix vem em PR separado depois.
623
+
624
+ ### Workflow canônico de characterization de PG function
625
+
626
+ ```text
627
+ 1. Identificar PG function alvo do refactor
628
+ Exemplo: public.calculate_invoice_total(uuid) — 200 linhas, sem tests
629
+
630
+ 2. Inventariar inputs/outputs
631
+ Inputs:
632
+ - parâmetro: invoice_id uuid
633
+ - reads: invoices, line_items, discounts, taxes (4 tabelas)
634
+ - reads globais: current_setting('app.tax_rate')
635
+ Outputs:
636
+ - return: numeric(10,2)
637
+ - side effects: insert em audit_log + update em invoices.calculated_at
638
+
639
+ 3. Para cada grupo de equivalência (5+ inputs):
640
+ a. Construir input (fixture)
641
+ b. Executar function REAL — sem mocks ainda
642
+ c. Capturar output completo + side effects
643
+ d. REVISAR linha por linha — marcar bugs conhecidos como comments
644
+ e. Salvar como pgTAP assertion
645
+
646
+ 4. Escrever pgTAP test file
647
+ - plan(N) declara N grupos × M assertions cada
648
+ - results_eq para return value
649
+ - results_eq para side effects (linhas em audit_log)
650
+ - throws_ok para edge cases que esperam erro
651
+
652
+ 5. supabase test db — TODOS verdes = baseline estabelecido
653
+
654
+ 6. Refactor pode começar
655
+ ```
656
+
657
+ ### Exemplo canônico
658
+
659
+ ```sql
660
+ -- supabase/tests/calculate_invoice_total_characterization_test.sql
661
+ begin;
662
+ select plan(10);
663
+
664
+ -- ===== GRUPO 1: invoice típica com 1 line item =====
665
+ -- Setup
666
+ insert into public.invoices (id, customer_id, status) values
667
+ ('inv-001', 'cust-001', 'pending');
668
+ insert into public.line_items (invoice_id, sku, qty, unit_price) values
669
+ ('inv-001', 'SKU-1', 2, 50.00);
670
+
671
+ -- Test 1: return value
672
+ select results_eq(
673
+ $$select public.calculate_invoice_total('inv-001'::uuid)$$,
674
+ $$values (100.00::numeric)$$,
675
+ 'GROUP 1: typical invoice 1 line item — total 100.00'
676
+ );
677
+
678
+ -- Test 2: side effect — audit_log row created
679
+ select results_eq(
680
+ $$select event_type, target_id from public.audit_log where target_id = 'inv-001'::uuid$$,
681
+ $$values ('invoice_total_calculated'::text, 'inv-001'::uuid)$$,
682
+ 'GROUP 1: audit_log row created'
683
+ );
684
+
685
+ -- ===== GRUPO 2: invoice com discount =====
686
+ insert into public.invoices (id, customer_id, status) values
687
+ ('inv-002', 'cust-001', 'pending');
688
+ insert into public.line_items (invoice_id, sku, qty, unit_price) values
689
+ ('inv-002', 'SKU-2', 1, 200.00);
690
+ insert into public.discounts (invoice_id, percentage) values
691
+ ('inv-002', 10.0); -- 10% discount
692
+
693
+ select results_eq(
694
+ $$select public.calculate_invoice_total('inv-002'::uuid)$$,
695
+ $$values (180.00::numeric)$$,
696
+ 'GROUP 2: invoice with 10% discount — total 180.00'
697
+ );
698
+
699
+ -- ===== GRUPO 3: invoice vazia (BUG #1 conhecido — retorna 0, deveria raise) =====
700
+ insert into public.invoices (id, customer_id, status) values
701
+ ('inv-003', 'cust-001', 'pending');
702
+ -- NO line_items inseridos
703
+
704
+ -- BUG #1: function retorna 0 para invoice vazia, deveria raise 'invalid_invoice'
705
+ -- Preservar comportamento bugado durante refactor; fix em PR separado.
706
+ select results_eq(
707
+ $$select public.calculate_invoice_total('inv-003'::uuid)$$,
708
+ $$values (0.00::numeric)$$,
709
+ 'GROUP 3 [BUG #1]: empty invoice returns 0 (should raise — preserved as oracle)'
710
+ );
711
+
712
+ -- ===== GRUPO 4: invoice inexistente — espera erro =====
713
+ select throws_ok(
714
+ $$select public.calculate_invoice_total('00000000-0000-0000-0000-000000000000'::uuid)$$,
715
+ 'P0001',
716
+ 'GROUP 4: non-existent invoice raises P0001'
717
+ );
718
+
719
+ -- ===== GRUPO 5: invoice com NEG line items (edge case histórico) =====
720
+ insert into public.invoices (id, customer_id, status) values
721
+ ('inv-005', 'cust-001', 'pending');
722
+ insert into public.line_items (invoice_id, sku, qty, unit_price) values
723
+ ('inv-005', 'SKU-3', -1, 100.00); -- qty NEGATIVO (devolução)
724
+
725
+ select results_eq(
726
+ $$select public.calculate_invoice_total('inv-005'::uuid)$$,
727
+ $$values (-100.00::numeric)$$,
728
+ 'GROUP 5: negative qty produces negative total (refund flow)'
729
+ );
730
+
731
+ -- ... outros grupos (boundary valid, side-effect heavy, etc.) ...
732
+
733
+ select * from finish();
734
+ rollback;
735
+ ```
736
+
737
+ ### Bugs preservados como comments
738
+
739
+ **Crítico:** characterization captura o que código FAZ, não o que DEVERIA fazer. Bugs conhecidos viram comments inline (`-- BUG #X: deveria Y, é Z`). Refactor preserva o oracle (incluindo bugs). Bug fix vem em PR separado **depois** do refactor, com seu próprio teste.
740
+
741
+ Exemplo do test acima: GROUP 3 marca `BUG #1` — função retorna 0 para invoice vazia em vez de raise exception. Refactor manterá esse comportamento. PR de bug fix subsequente alterará a assertion para `throws_ok` e fixará a função.
742
+
743
+ ### Behavioral coverage check (mutation testing — recomendado)
744
+
745
+ pgTAP cobre o **que** o código faz; mutation testing valida que tests **detectam regressão**. Para PG functions críticas:
746
+
747
+ 1. Rodar `supabase test db` — baseline verde
748
+ 2. Aplicar mutation (manualmente — não há tool padrão para PG mutation; use search-replace deliberado):
749
+ - Trocar `+` por `-` na cálculo
750
+ - Trocar `>` por `>=` em CHECK
751
+ - Comentar `RAISE EXCEPTION` em edge case
752
+ 3. Rodar `supabase test db` — esperado vermelho. Se ficar verde, characterization tem ponto cego.
753
+ 4. Adicionar test que cobre o ponto cego
754
+ 5. Reverter mutation; suite volta a verde
755
+
756
+ Cross-ref skill `ai-mutation-tester` (v1.20) — pattern análogo para JavaScript/TypeScript.
757
+
758
+ ### Quando pgTAP characterization é mandatório
759
+
760
+ Aplicar pgTAP characterization (cross-ref skill `pre-refactor-characterization` v1.18) ANTES de refactor de:
761
+
762
+ - PG function > 100 linhas sem tests existentes
763
+ - PG function consumida por > 3 callers (alto blast radius se regredir)
764
+ - RLS policy complexa (> 5 OR conditions, ou referencia auth.uid() + claim + subquery)
765
+ - Trigger BEFORE/AFTER que muta dados em outras tabelas
766
+ - Function exposta via Edge Function ou PostgREST RPC (contrato externo)
767
+
768
+ ## Anti-patterns
769
+
770
+ ### Anti-pattern 1: Tests sem `rollback`
771
+
772
+ **Errado:**
773
+
774
+ ```sql
775
+ -- supabase/tests/employees_test.sql
776
+ select plan(2);
777
+
778
+ select has_table('public', 'employees', 'employees exists');
779
+
780
+ insert into public.employees (name) values ('Test User'); -- SEM begin/rollback!
781
+
782
+ select results_eq(
783
+ 'select count(*) from public.employees where name = ''Test User''',
784
+ 'values (1::bigint)',
785
+ 'inserted user found'
786
+ );
787
+
788
+ select * from finish();
789
+ ```
790
+
791
+ **Por quê:** `INSERT` sem `begin; ... rollback;` é **persistido** no DB local. Após rodar `supabase test db`:
792
+
793
+ - Teste pode "passar" na primeira run mas falhar na segunda (`count` já não é 1, é 2)
794
+ - DB local poluído com dados sintéticos — afeta outros tests
795
+ - Se rodar em CI, novo container DB cada run = OK; mas localmente é problema
796
+
797
+ **Certo:**
798
+
799
+ ```sql
800
+ -- supabase/tests/employees_test.sql
801
+ begin; -- inicia transação
802
+ select plan(2);
803
+
804
+ select has_table('public', 'employees', 'employees exists');
805
+
806
+ insert into public.employees (name) values ('Test User');
807
+
808
+ select results_eq(
809
+ 'select count(*) from public.employees where name = ''Test User''',
810
+ 'values (1::bigint)',
811
+ 'inserted user found'
812
+ );
813
+
814
+ select * from finish();
815
+ rollback; -- desfaz INSERT
816
+ ```
817
+
818
+ `rollback` é **incondicional** — mesmo se `finish()` falhar, transação aborta no rollback. Estado DB inalterado.
819
+
820
+ ### Anti-pattern 2: Esquecer `plan(N)` — testes silenciosos
821
+
822
+ **Errado:**
823
+
824
+ ```sql
825
+ begin;
826
+ -- SEM plan() declarado
827
+
828
+ select has_table('public', 'employees', 'test 1');
829
+ select has_column('public', 'employees', 'name', 'test 2');
830
+ select col_type_is('public', 'employees', 'name', 'text', 'test 3');
831
+
832
+ select * from finish();
833
+ rollback;
834
+ ```
835
+
836
+ **Por quê:** sem `plan(N)`, pgTAP **não sabe** quantos tests esperar. Output ainda mostra `ok 1`, `ok 2`, `ok 3` mas:
837
+
838
+ - TAP harness pode interpretar como "0 tests planned, 3 executed" → falha silenciosa em CI
839
+ - `finish()` reporta `1..0` (planejados zero) — exit code pode ser 0 (sucesso) mesmo se tests **falharem** silenciosamente
840
+ - Adicionar test novo não é notado (deveria mudar `plan(N)` para `plan(N+1)`, mas como não tem plan, esquecer é invisível)
841
+
842
+ **Certo:**
843
+
844
+ ```sql
845
+ begin;
846
+ select plan(3); -- DECLARA EXPLICITAMENTE 3 tests
847
+
848
+ select has_table('public', 'employees', 'test 1');
849
+ select has_column('public', 'employees', 'name', 'test 2');
850
+ select col_type_is('public', 'employees', 'name', 'text', 'test 3');
851
+
852
+ select * from finish();
853
+ rollback;
854
+ ```
855
+
856
+ `plan(N)` é **obrigatório**. Se rodar 4 mas declarou 3, pgTAP reporta `# Looks like you planned 3 tests but ran 4.` — falha.
857
+
858
+ Manter `plan(N)` atualizado é parte do contrato — se adiciona test, atualiza plan.
859
+
860
+ ### Anti-pattern 3: Tests Deno sem `.env.local`
861
+
862
+ **Errado:**
863
+
864
+ ```bash
865
+ # rodar tests sem env file
866
+ deno test --allow-all supabase/functions/_tests/
867
+ ```
868
+
869
+ ```typescript
870
+ // dentro do test
871
+ const response = await fetch(`${Deno.env.get("SUPABASE_URL")}/functions/v1/hello`);
872
+ // Deno.env.get("SUPABASE_URL") retorna undefined
873
+ // fetch("undefined/functions/v1/hello") = URL inválida
874
+ // Error: Invalid URL: 'undefined/functions/v1/hello'
875
+ ```
876
+
877
+ **Por quê:** sem `--env-file`, `Deno.env.get(...)` retorna `undefined`. `fetch` com URL inválida lança erro genérico. Mensagem confusa — dev pensa que tem bug no handler, na verdade é env config.
878
+
879
+ **Certo:**
880
+
881
+ ```bash
882
+ # Gerar .env.local
883
+ supabase status -o env > supabase/functions/.env.local
884
+
885
+ # Rodar com env file
886
+ deno test --allow-all supabase/functions/_tests/ --env-file supabase/functions/.env.local
887
+ ```
888
+
889
+ ```typescript
890
+ // Defensive: assert env vars existem (fail-fast com mensagem clara)
891
+ const SUPABASE_URL = Deno.env.get("SUPABASE_URL");
892
+ const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY");
893
+
894
+ if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
895
+ throw new Error(
896
+ "Missing SUPABASE_URL or SUPABASE_ANON_KEY. " +
897
+ "Run: supabase status -o env > supabase/functions/.env.local"
898
+ );
899
+ }
900
+ ```
901
+
902
+ Em CI, geração de `.env.local` deve ser **step explícito** do workflow (cross-ref Phase 151 CI-06):
903
+
904
+ ```yaml
905
+ - name: Generate .env.local
906
+ run: supabase status -o env > supabase/functions/.env.local
907
+ - run: deno test --allow-all supabase/functions/_tests/ --env-file supabase/functions/.env.local
908
+ ```
909
+
910
+ ### Anti-pattern 4: Tratar pgTAP como "testes completos"
911
+
912
+ **Errado:** time considera que pgTAP + Deno tests cobrem TODO o sistema → para CI: "se database-tests e functions-tests passam, deploy é seguro".
913
+
914
+ **Por quê:** pgTAP cobre **schema e PG logic**; Deno tests cobrem **Edge Function HTTP handlers**. NENHUM dos dois cobre:
915
+
916
+ - Cliente frontend interagindo com Supabase via `@supabase/supabase-js` (race conditions, retry, optimistic UI)
917
+ - Comportamento Realtime (subscriptions, broadcast, presence)
918
+ - Auth flows end-to-end (signup → email confirm → first login → setup wizard)
919
+ - RLS visto do ponto de vista do cliente autenticado (JWT real, não simulado via `set_config`)
920
+ - Performance / latência sob carga
921
+ - Edge cases de browser (CORS, cookies, localStorage)
922
+
923
+ **Certo:** treat pgTAP + Deno como **two layers of defense-in-depth**, não substituto de integration tests:
924
+
925
+ ```text
926
+ Pirâmide de testes Supabase (canônica):
927
+
928
+ /\
929
+ /E2E\ ← Playwright/Cypress: 5-10 critical user journeys
930
+ /------\
931
+ / INT. \ ← Vitest/Jest com Supabase real: auth flow, multi-tenant
932
+ /----------\
933
+ / DENO TESTS \ ← pattern 3: Edge Functions HTTP handlers
934
+ /--------------\
935
+ / pgTAP \ ← pattern 1: schema + RLS + PG functions
936
+ /------------------\
937
+ | Unit tests app | ← Vitest/Jest cliente: componentes React, helpers
938
+ ```
939
+
940
+ Cada camada cobre **incidentes diferentes**:
941
+ - pgTAP: regressão em schema/policy/function
942
+ - Deno: regressão em Edge Function logic
943
+ - Integration: regressão em cliente ↔ Supabase wire
944
+ - E2E: regressão em user journey
945
+
946
+ Sem todas as camadas, gaps de cobertura existem. CI deve ter **separate required checks** por camada.
947
+
948
+ ### Anti-pattern 5: Snapshot pgTAP sem revisão (characterization shortcut)
949
+
950
+ **Errado:** copy-paste output bruto de query como `results_eq` expected sem inspecionar — "se runs verde, está OK".
951
+
952
+ ```sql
953
+ -- characterization sem revisão
954
+ select results_eq(
955
+ $$select public.complex_function('input-1')$$,
956
+ $$values ('a', '2026-05-11T10:23:45.123456Z'::timestamptz, 'token_abc123xyz', 42, '00000000-1234-5678-9abc-def012345678'::uuid)$$,
957
+ 'preserved as-is'
958
+ );
959
+ ```
960
+
961
+ **Por quê:** output bruto pode incluir:
962
+
963
+ - **PII** (emails, names) — vazam para git history
964
+ - **Tokens/secrets** — vazam para git history (irreversível)
965
+ - **Timestamps voláteis** — test será flaky em run subsequente
966
+ - **UUIDs locais** — gerados por `gen_random_uuid()`, mudam a cada run
967
+
968
+ CI fica "verde" porque snapshot bate consigo mesmo da última run, mas:
969
+ - Adicionar fixture nova quebra (timestamp diferente)
970
+ - PR review humano não detecta bugs (não inspecionou output)
971
+ - Secrets podem ser commitados sem aviso
972
+
973
+ **Certo:** revisão linha-por-linha + sanitização antes de salvar (cross-ref skill `legacy-characterization-tests` Pattern 6):
974
+
975
+ ```sql
976
+ -- characterization com sanitização determinística
977
+ begin;
978
+ -- 1. Fixar clock para determinismo
979
+ select set_config('app.fake_now', '2026-05-11T10:00:00Z', true);
980
+
981
+ -- 2. Fixar UUIDs determinísticos via fixture
982
+ insert into public.things (id, name, created_at) values
983
+ ('00000000-0000-0000-0000-000000000001', 'Test', '2026-05-11T10:00:00Z'::timestamptz);
984
+
985
+ -- 3. Snapshot review:
986
+ -- - Email 'alice@x.com' está no fixture (não real PII)
987
+ -- - Timestamp '2026-05-11T10:00:00Z' está congelado (fake clock)
988
+ -- - UUID '00000000...001' está determinístico (fixture)
989
+ -- - Token 'token_test_abc' está sanitized (não real)
990
+ select results_eq(
991
+ $$select id, name, created_at from public.things where id = '00000000-0000-0000-0000-000000000001'::uuid$$,
992
+ $$values ('00000000-0000-0000-0000-000000000001'::uuid, 'Test'::text, '2026-05-11T10:00:00Z'::timestamptz)$$,
993
+ 'thing fixture matches characterization oracle (reviewed 2026-05-11)'
994
+ );
995
+
996
+ select * from finish();
997
+ rollback;
998
+ ```
999
+
1000
+ **Crítico:** comentar quando snapshot foi revisado + por quem. Commit message: "characterize complex_function — reviewed 2026-05-11, bugs noted in comments".
1001
+
1002
+ ## Cross-suite integration (v1.27)
1003
+
1004
+ Esta skill é **complemento essencial** da skill Phase 151 `supabase-ci-cd-github-actions`:
1005
+
1006
+ - Phase 151 estabelece workflows CI (`database-tests.yml` + `functions-tests.yml`) que executam `supabase test db` + `deno test --allow-all`
1007
+ - Phase 152 (ESTA) detalha a **sintaxe canônica** dos tests que esses workflows consomem
1008
+ - Forward-ref de Phase 151 é fechado por esta skill (Pattern 5 da Phase 151 referenciava "skill futura `supabase-pgtap-testing` Phase 152")
1009
+
1010
+ Cross-refs com skills existentes v1.x:
1011
+
1012
+ - **`legacy-characterization-tests` (v1.16)** — pgTAP é o mecanismo canônico para implementar characterization (cap 13 Feathers) em PG legado; Pattern 4 desta skill é especialização para Postgres
1013
+ - **`pre-refactor-characterization` (v1.18)** — gate auto-trigger que bloqueia refactor sem characterization; pgTAP satisfaz a pré-condição para PG functions
1014
+ - **`ai-mutation-tester` (v1.20)** — mutation testing complementar; valida que pgTAP detecta regressão
1015
+ - **`supabase-database-functions`** — PG functions criadas seguindo essa skill são prime target de pgTAP (SECURITY INVOKER + SET search_path = '' são testáveis)
1016
+ - **`supabase-rls-policies` (v1.23)** — RLS policies validadas via `throws_like '%permission denied%'` + `results_eq` com diferentes JWT claims
1017
+ - **`supabase-edge-functions`** — Edge Functions cobertas por Deno tests (Pattern 3)
1018
+ - **`supabase-postgres-roles` (v1.26)** — testes de roles validáveis via `has_role`, `set role test_role; ...; reset role`
1019
+ - **`supabase-custom-claims-rbac` (v1.25)** — testes de auth hook validáveis via fixture JWT + `set_config('request.jwt.claim.user_role', 'admin', true)`
1020
+
1021
+ Base para agent futuro v1.27:
1022
+
1023
+ - **`supabase-cicd-pipeline-implementer` (Phase 154, futura)** — agent que materializa workflows + tests; consome esta skill para gerar test files iniciais junto com migrations
1024
+
1025
+ Pattern de handoff cooperativo herdado v1.23-v1.26: **architect** projeta strategy → **test-writer** materializa pgTAP/Deno tests → **release-pipeline-auditor** (v1.10) audita coverage do pipeline. Nenhum agente descarta upstream — handoff cooperativo (princípio canônico v1.23).
1026
+
1027
+ ### Casos de uso por agent
1028
+
1029
+ | Agent | Como consome esta skill |
1030
+ |-------|-------------------------|
1031
+ | `supabase-migration-writer` | Gera migration + pgTAP test inicial (has_table, has_column, has_pk) |
1032
+ | `supabase-rls-writer` (v1.23) | Gera RLS policies + pgTAP test (throws_like '%permission denied%' para cross-tenant) |
1033
+ | `supabase-edge-fn-writer` | Gera Edge Function + Deno test (handler retorna 200/400 esperado) |
1034
+ | `legacy-characterizer` | Gera pgTAP characterization tests para PG function legada (cap 13 Feathers) |
1035
+ | `refactor-safety-auditor` | Verifica existência de pgTAP tests antes de permitir refactor PG |
1036
+ | `supabase-cicd-pipeline-implementer` (Phase 154 futura) | Gera workflows `database-tests.yml` + `functions-tests.yml` + tests iniciais |
1037
+
1038
+ ## Ver também
1039
+
1040
+ - [supabase-ci-cd-github-actions](../supabase-ci-cd-github-actions/SKILL.md) (v1.27, Phase 151) — workflows CI que executam `supabase test db` + `deno test`
1041
+ - [legacy-characterization-tests](../legacy-characterization-tests/SKILL.md) (v1.16) — cap 13 Feathers; pgTAP é mecanismo canônico para Postgres
1042
+ - [pre-refactor-characterization](../pre-refactor-characterization/SKILL.md) (v1.18) — gate que pgTAP satisfaz para PG functions
1043
+ - [supabase-database-functions](../supabase-database-functions/SKILL.md) — PG functions testáveis via pgTAP
1044
+ - [supabase-rls-policies](../supabase-rls-policies/SKILL.md) (v1.23) — RLS testável via `throws_like` + `set_config jwt claim`
1045
+ - [supabase-rls-defense-in-depth](../supabase-rls-defense-in-depth/SKILL.md) (v1.23) — Camadas testáveis em pgTAP
1046
+ - [supabase-edge-functions](../supabase-edge-functions/SKILL.md) — Edge Functions cobertas por Deno tests
1047
+ - [supabase-postgres-roles](../supabase-postgres-roles/SKILL.md) (v1.26) — `has_role`, `set role` testáveis
1048
+ - [supabase-custom-claims-rbac](../supabase-custom-claims-rbac/SKILL.md) (v1.25) — fixture JWT via `set_config('request.jwt.claim.*')`
1049
+ - [supabase-migrations](../supabase-migrations/SKILL.md) (v1.23) — migration cria tabela; pgTAP test valida
1050
+ - [ai-mutation-tester](../../agents/ai-mutation-tester.md) (v1.20) — mutation testing complementar
1051
+ - [glossário compartilhado](../_shared-supabase/glossary.md) — termos pgTAP, TAP, plan(N), throws_ok, results_eq, supabase test db, deno test --allow-all
1052
+ - Doc oficial pgTAP: [pgTAP Documentation](https://pgtap.org/documentation.html)
1053
+ - Doc oficial Supabase: [Testing with pgTAP](https://supabase.com/docs/guides/database/extensions/pgtap), [Testing Edge Functions](https://supabase.com/docs/guides/functions/unit-test)