@codespar/cli 0.16.0 → 0.18.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 (486) hide show
  1. package/README.md +1 -0
  2. package/dist/commands/mandate-verify.js +98 -19
  3. package/dist/commands/mandate-verify.js.map +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/mandate-codec.js +31 -20
  7. package/dist/mandate-codec.js.map +1 -1
  8. package/package.json +1 -1
  9. package/templates/bills-agent/README.md +1 -1
  10. package/templates/bills-agent/agents/bills-agent/AGENTS.md +1 -1
  11. package/templates/bills-agent/agents/bills-agent/CLAUDE.md +1 -1
  12. package/templates/bills-agent/agents/bills-agent/README.md +6 -5
  13. package/templates/bills-agent/agents/bills-agent/src/modules/embedded-consent.ts +5 -4
  14. package/templates/bills-agent/packages/agent-core/README.md +14 -1
  15. package/templates/bills-agent/packages/agent-core/package.json +1 -1
  16. package/templates/bills-agent/packages/agent-core/src/agent.ts +39 -1
  17. package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  18. package/templates/bills-agent/packages/agent-core/src/api/client.ts +28 -5
  19. package/templates/bills-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  20. package/templates/bills-agent/packages/agent-core/src/api/rail.ts +62 -26
  21. package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  22. package/templates/bills-agent/packages/agent-core/src/approval.ts +14 -2
  23. package/templates/bills-agent/packages/agent-core/src/bundle.ts +35 -3
  24. package/templates/bills-agent/packages/agent-core/src/channels.ts +49 -3
  25. package/templates/bills-agent/packages/agent-core/src/check.ts +25 -2
  26. package/templates/bills-agent/packages/agent-core/src/engine.ts +235 -46
  27. package/templates/bills-agent/packages/agent-core/src/hash.ts +57 -1
  28. package/templates/bills-agent/packages/agent-core/src/index.ts +2 -0
  29. package/templates/bills-agent/packages/agent-core/src/quote.ts +50 -0
  30. package/templates/bills-agent/packages/agent-core/src/rail.ts +89 -4
  31. package/templates/bills-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  32. package/templates/bills-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  33. package/templates/bills-agent/packages/agent-core/src/revocation.ts +1 -1
  34. package/templates/bills-agent/packages/agent-core/src/state-machine.ts +10 -1
  35. package/templates/bills-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  36. package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  37. package/templates/bills-agent/packages/agent-core/src/tools.ts +17 -6
  38. package/templates/bills-agent/packages/agent-core/src/types.ts +54 -1
  39. package/templates/bills-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  40. package/templates/bills-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  41. package/templates/bills-agent/packages/agent-core/test/composition.test.ts +204 -0
  42. package/templates/bills-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  43. package/templates/bills-agent/packages/agent-core/test/helpers.ts +4 -1
  44. package/templates/bills-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  45. package/templates/bills-agent/packages/agent-core/test/quote.test.ts +198 -0
  46. package/templates/bills-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  47. package/templates/bills-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  48. package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +103 -3
  49. package/templates/bills-agent/packages/agent-runtime/README.md +12 -0
  50. package/templates/bills-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  51. package/templates/bills-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  52. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  53. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  54. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  55. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  56. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  57. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  58. package/templates/bills-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  59. package/templates/bills-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  60. package/templates/bills-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  61. package/templates/bills-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  62. package/templates/bills-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  63. package/templates/bills-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  64. package/templates/bills-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  65. package/templates/bills-agent/packages/agent-runtime/src/index.ts +1 -0
  66. package/templates/bills-agent/packages/agent-runtime/src/inspect.ts +5 -1
  67. package/templates/bills-agent/packages/agent-runtime/src/kit.ts +25 -2
  68. package/templates/bills-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  69. package/templates/bills-agent/packages/agent-runtime/src/setup.ts +13 -3
  70. package/templates/bills-agent/packages/agent-runtime/src/terminal.ts +127 -18
  71. package/templates/bills-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  72. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  73. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  74. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  75. package/templates/checkout-agent/LICENSE +21 -0
  76. package/templates/checkout-agent/README.md +28 -0
  77. package/templates/checkout-agent/_gitignore +10 -0
  78. package/templates/checkout-agent/agents/checkout-agent/.env.example +5 -0
  79. package/templates/checkout-agent/agents/checkout-agent/AGENTS.md +20 -0
  80. package/templates/checkout-agent/agents/checkout-agent/CLAUDE.md +20 -0
  81. package/templates/checkout-agent/agents/checkout-agent/README.md +94 -0
  82. package/templates/checkout-agent/agents/checkout-agent/SYSTEM_PROMPT.md +36 -0
  83. package/templates/checkout-agent/agents/checkout-agent/agent.yaml +30 -0
  84. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/README.md +28 -0
  85. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-beatriz.json +11 -0
  86. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina-retomada.json +10 -0
  87. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina.json +11 -0
  88. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/templates.json +49 -0
  89. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap-human.json +12 -0
  90. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.json +11 -0
  91. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +5 -0
  92. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.json +13 -0
  93. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.transcript.jsonl +4 -0
  94. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.json +26 -0
  95. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
  96. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.json +11 -0
  97. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.transcript.jsonl +5 -0
  98. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.json +12 -0
  99. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.transcript.jsonl +5 -0
  100. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.json +13 -0
  101. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.transcript.jsonl +5 -0
  102. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.json +15 -0
  103. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.transcript.jsonl +7 -0
  104. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.json +12 -0
  105. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.transcript.jsonl +5 -0
  106. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.json +13 -0
  107. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.transcript.jsonl +4 -0
  108. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.json +16 -0
  109. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.transcript.jsonl +9 -0
  110. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.json +15 -0
  111. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.transcript.jsonl +8 -0
  112. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/webhook-replay.json +11 -0
  113. package/templates/checkout-agent/agents/checkout-agent/evals/eval.yaml +11 -0
  114. package/templates/checkout-agent/agents/checkout-agent/guardrails.json +25 -0
  115. package/templates/checkout-agent/agents/checkout-agent/mandate.example.json +28 -0
  116. package/templates/checkout-agent/agents/checkout-agent/package.json +33 -0
  117. package/templates/checkout-agent/agents/checkout-agent/runbook.md +19 -0
  118. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.json +42 -0
  119. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +4 -0
  120. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.json +32 -0
  121. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.transcript.jsonl +4 -0
  122. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.json +61 -0
  123. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.transcript.jsonl +9 -0
  124. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.json +61 -0
  125. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.transcript.jsonl +9 -0
  126. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.json +36 -0
  127. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.transcript.jsonl +6 -0
  128. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.json +23 -0
  129. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.transcript.jsonl +4 -0
  130. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.json +72 -0
  131. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.transcript.jsonl +9 -0
  132. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.json +15 -0
  133. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.transcript.jsonl +6 -0
  134. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.json +23 -0
  135. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.transcript.jsonl +3 -0
  136. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.json +42 -0
  137. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.transcript.jsonl +12 -0
  138. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.json +14 -0
  139. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.transcript.jsonl +9 -0
  140. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.json +34 -0
  141. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.transcript.jsonl +7 -0
  142. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.json +33 -0
  143. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.transcript.jsonl +4 -0
  144. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.json +32 -0
  145. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.transcript.jsonl +4 -0
  146. package/templates/checkout-agent/agents/checkout-agent/src/catalog.ts +92 -0
  147. package/templates/checkout-agent/agents/checkout-agent/src/kit.ts +338 -0
  148. package/templates/checkout-agent/agents/checkout-agent/src/modules/bolepix-receivables.ts +179 -0
  149. package/templates/checkout-agent/agents/checkout-agent/src/modules/nfse-invoice.ts +374 -0
  150. package/templates/checkout-agent/agents/checkout-agent/src/modules/storefront-cart.ts +220 -0
  151. package/templates/checkout-agent/agents/checkout-agent/src/pricing.ts +297 -0
  152. package/templates/checkout-agent/agents/checkout-agent/test/adversarial.test.ts +117 -0
  153. package/templates/checkout-agent/agents/checkout-agent/test/check.test.ts +67 -0
  154. package/templates/checkout-agent/agents/checkout-agent/test/cli.test.ts +169 -0
  155. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/order-marina.transcript.jsonl +5 -0
  156. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/ordered-marina-awaiting-payer.transcript.jsonl +6 -0
  157. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/tap-falar-agora-marina.transcript.jsonl +2 -0
  158. package/templates/checkout-agent/agents/checkout-agent/test/nfse.test.ts +170 -0
  159. package/templates/checkout-agent/agents/checkout-agent/test/pricing.test.ts +138 -0
  160. package/templates/checkout-agent/agents/checkout-agent/test/scenarios.test.ts +113 -0
  161. package/templates/checkout-agent/agents/checkout-agent/test/whatsapp.test.ts +171 -0
  162. package/templates/checkout-agent/agents/checkout-agent/tools.json +68 -0
  163. package/templates/checkout-agent/agents/checkout-agent/tsconfig.json +10 -0
  164. package/templates/checkout-agent/package.json +38 -0
  165. package/templates/checkout-agent/packages/agent-core/README.md +34 -0
  166. package/templates/checkout-agent/packages/agent-core/package.json +24 -0
  167. package/templates/checkout-agent/packages/agent-core/src/agent.ts +163 -0
  168. package/templates/checkout-agent/packages/agent-core/src/api/charge-rail.ts +195 -0
  169. package/templates/checkout-agent/packages/agent-core/src/api/client.ts +76 -0
  170. package/templates/checkout-agent/packages/agent-core/src/api/mandate-status.ts +74 -0
  171. package/templates/checkout-agent/packages/agent-core/src/api/rail.ts +153 -0
  172. package/templates/checkout-agent/packages/agent-core/src/api/sandbox-payer.ts +43 -0
  173. package/templates/checkout-agent/packages/agent-core/src/approval.ts +135 -0
  174. package/templates/checkout-agent/packages/agent-core/src/bundle.ts +191 -0
  175. package/templates/checkout-agent/packages/agent-core/src/channels.ts +186 -0
  176. package/templates/checkout-agent/packages/agent-core/src/check.ts +276 -0
  177. package/templates/checkout-agent/packages/agent-core/src/clock.ts +42 -0
  178. package/templates/checkout-agent/packages/agent-core/src/engine.ts +1074 -0
  179. package/templates/checkout-agent/packages/agent-core/src/escalate.ts +85 -0
  180. package/templates/checkout-agent/packages/agent-core/src/events.ts +40 -0
  181. package/templates/checkout-agent/packages/agent-core/src/guardrails.ts +45 -0
  182. package/templates/checkout-agent/packages/agent-core/src/hash.ts +118 -0
  183. package/templates/checkout-agent/packages/agent-core/src/ids.ts +10 -0
  184. package/templates/checkout-agent/packages/agent-core/src/index.ts +34 -0
  185. package/templates/checkout-agent/packages/agent-core/src/mandate.ts +100 -0
  186. package/templates/checkout-agent/packages/agent-core/src/manifest.ts +76 -0
  187. package/templates/checkout-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
  188. package/templates/checkout-agent/packages/agent-core/src/providers/replay.ts +63 -0
  189. package/templates/checkout-agent/packages/agent-core/src/providers/types.ts +39 -0
  190. package/templates/checkout-agent/packages/agent-core/src/quote.ts +50 -0
  191. package/templates/checkout-agent/packages/agent-core/src/rail.ts +186 -0
  192. package/templates/checkout-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  193. package/templates/checkout-agent/packages/agent-core/src/receipt-verification.ts +577 -0
  194. package/templates/checkout-agent/packages/agent-core/src/revocation.ts +30 -0
  195. package/templates/checkout-agent/packages/agent-core/src/secrets.ts +30 -0
  196. package/templates/checkout-agent/packages/agent-core/src/state/store.ts +307 -0
  197. package/templates/checkout-agent/packages/agent-core/src/state-machine.ts +157 -0
  198. package/templates/checkout-agent/packages/agent-core/src/stubs/charge-rail.ts +195 -0
  199. package/templates/checkout-agent/packages/agent-core/src/stubs/mandate-status.ts +58 -0
  200. package/templates/checkout-agent/packages/agent-core/src/stubs/rail.ts +209 -0
  201. package/templates/checkout-agent/packages/agent-core/src/tools.ts +66 -0
  202. package/templates/checkout-agent/packages/agent-core/src/types.ts +187 -0
  203. package/templates/checkout-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  204. package/templates/checkout-agent/packages/agent-core/test/approval.test.ts +117 -0
  205. package/templates/checkout-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  206. package/templates/checkout-agent/packages/agent-core/test/bundle.test.ts +85 -0
  207. package/templates/checkout-agent/packages/agent-core/test/clock.test.ts +25 -0
  208. package/templates/checkout-agent/packages/agent-core/test/composition.test.ts +204 -0
  209. package/templates/checkout-agent/packages/agent-core/test/engine.test.ts +596 -0
  210. package/templates/checkout-agent/packages/agent-core/test/escalate.test.ts +42 -0
  211. package/templates/checkout-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  212. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt-keys.json +18 -0
  213. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt.json +24 -0
  214. package/templates/checkout-agent/packages/agent-core/test/helpers.ts +142 -0
  215. package/templates/checkout-agent/packages/agent-core/test/mandate-status.test.ts +229 -0
  216. package/templates/checkout-agent/packages/agent-core/test/manifest.test.ts +45 -0
  217. package/templates/checkout-agent/packages/agent-core/test/quote.test.ts +198 -0
  218. package/templates/checkout-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  219. package/templates/checkout-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  220. package/templates/checkout-agent/packages/agent-core/test/receipt-verification.test.ts +316 -0
  221. package/templates/checkout-agent/packages/agent-core/test/receivables.test.ts +416 -0
  222. package/templates/checkout-agent/packages/agent-core/test/secrets.test.ts +28 -0
  223. package/templates/checkout-agent/packages/agent-core/test/state-machine.test.ts +67 -0
  224. package/templates/checkout-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
  225. package/templates/checkout-agent/packages/agent-core/tsconfig.json +10 -0
  226. package/templates/checkout-agent/packages/agent-runtime/README.md +80 -0
  227. package/templates/checkout-agent/packages/agent-runtime/bin.mjs +32 -0
  228. package/templates/checkout-agent/packages/agent-runtime/package.json +23 -0
  229. package/templates/checkout-agent/packages/agent-runtime/src/adversarial.ts +199 -0
  230. package/templates/checkout-agent/packages/agent-runtime/src/agent.ts +68 -0
  231. package/templates/checkout-agent/packages/agent-runtime/src/channels/contact.ts +13 -0
  232. package/templates/checkout-agent/packages/agent-runtime/src/channels/index.ts +72 -0
  233. package/templates/checkout-agent/packages/agent-runtime/src/channels/rules.ts +120 -0
  234. package/templates/checkout-agent/packages/agent-runtime/src/channels/types.ts +173 -0
  235. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +459 -0
  236. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +216 -0
  237. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/evidence.ts +109 -0
  238. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +350 -0
  239. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +180 -0
  240. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/present.ts +47 -0
  241. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +120 -0
  242. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +127 -0
  243. package/templates/checkout-agent/packages/agent-runtime/src/cli.ts +98 -0
  244. package/templates/checkout-agent/packages/agent-runtime/src/commands/check.ts +15 -0
  245. package/templates/checkout-agent/packages/agent-runtime/src/commands/decide.ts +63 -0
  246. package/templates/checkout-agent/packages/agent-runtime/src/commands/eval.ts +58 -0
  247. package/templates/checkout-agent/packages/agent-runtime/src/commands/inspect.ts +100 -0
  248. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +367 -0
  249. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll.ts +145 -0
  250. package/templates/checkout-agent/packages/agent-runtime/src/commands/reconcile.ts +42 -0
  251. package/templates/checkout-agent/packages/agent-runtime/src/commands/rerun.ts +86 -0
  252. package/templates/checkout-agent/packages/agent-runtime/src/commands/resume.ts +49 -0
  253. package/templates/checkout-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +134 -0
  254. package/templates/checkout-agent/packages/agent-runtime/src/commands/start.ts +248 -0
  255. package/templates/checkout-agent/packages/agent-runtime/src/commands/verify.ts +330 -0
  256. package/templates/checkout-agent/packages/agent-runtime/src/commands/webhook.ts +40 -0
  257. package/templates/checkout-agent/packages/agent-runtime/src/default-kit.ts +83 -0
  258. package/templates/checkout-agent/packages/agent-runtime/src/index.ts +13 -0
  259. package/templates/checkout-agent/packages/agent-runtime/src/inspect.ts +801 -0
  260. package/templates/checkout-agent/packages/agent-runtime/src/kit.ts +229 -0
  261. package/templates/checkout-agent/packages/agent-runtime/src/poll.ts +105 -0
  262. package/templates/checkout-agent/packages/agent-runtime/src/scenarios.ts +274 -0
  263. package/templates/checkout-agent/packages/agent-runtime/src/setup.ts +239 -0
  264. package/templates/checkout-agent/packages/agent-runtime/src/terminal.ts +263 -0
  265. package/templates/checkout-agent/packages/agent-runtime/src/webhook.ts +131 -0
  266. package/templates/checkout-agent/packages/agent-runtime/test/inspect.test.ts +336 -0
  267. package/templates/checkout-agent/packages/agent-runtime/test/verify-cli.test.ts +329 -0
  268. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +277 -0
  269. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-evidence.test.ts +94 -0
  270. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-rules.test.ts +174 -0
  271. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-session.test.ts +167 -0
  272. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  273. package/templates/checkout-agent/packages/agent-runtime/tsconfig.json +9 -0
  274. package/templates/checkout-agent/scripts/whatsapp-emulator.mjs +84 -0
  275. package/templates/checkout-agent/tsconfig.base.json +16 -0
  276. package/templates/checkout-agent/vitest.config.ts +12 -0
  277. package/templates/collections-agent/README.md +6 -1
  278. package/templates/collections-agent/agents/collections-agent/README.md +12 -8
  279. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/README.md +8 -0
  280. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/acordo-1042-retomada.json +10 -0
  281. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/templates.json +27 -1
  282. package/templates/collections-agent/agents/collections-agent/src/kit.ts +2 -0
  283. package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts +12 -2
  284. package/templates/collections-agent/agents/collections-agent/test/ambiguous-charge.test.ts +55 -0
  285. package/templates/collections-agent/agents/collections-agent/test/fixtures/tap-emitir-nova-1042.transcript.jsonl +2 -0
  286. package/templates/collections-agent/agents/collections-agent/test/whatsapp-poll.test.ts +12 -6
  287. package/templates/collections-agent/agents/collections-agent/test/whatsapp-status-replies.test.ts +148 -0
  288. package/templates/collections-agent/agents/collections-agent/test/whatsapp.test.ts +5 -0
  289. package/templates/collections-agent/package.json +1 -0
  290. package/templates/collections-agent/packages/agent-core/README.md +14 -1
  291. package/templates/collections-agent/packages/agent-core/package.json +1 -1
  292. package/templates/collections-agent/packages/agent-core/src/agent.ts +39 -1
  293. package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  294. package/templates/collections-agent/packages/agent-core/src/api/client.ts +28 -5
  295. package/templates/collections-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  296. package/templates/collections-agent/packages/agent-core/src/api/rail.ts +62 -26
  297. package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  298. package/templates/collections-agent/packages/agent-core/src/approval.ts +14 -2
  299. package/templates/collections-agent/packages/agent-core/src/bundle.ts +35 -3
  300. package/templates/collections-agent/packages/agent-core/src/channels.ts +49 -3
  301. package/templates/collections-agent/packages/agent-core/src/check.ts +25 -2
  302. package/templates/collections-agent/packages/agent-core/src/engine.ts +235 -46
  303. package/templates/collections-agent/packages/agent-core/src/hash.ts +57 -1
  304. package/templates/collections-agent/packages/agent-core/src/index.ts +2 -0
  305. package/templates/collections-agent/packages/agent-core/src/quote.ts +50 -0
  306. package/templates/collections-agent/packages/agent-core/src/rail.ts +89 -4
  307. package/templates/collections-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  308. package/templates/collections-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  309. package/templates/collections-agent/packages/agent-core/src/revocation.ts +1 -1
  310. package/templates/collections-agent/packages/agent-core/src/state-machine.ts +10 -1
  311. package/templates/collections-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  312. package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  313. package/templates/collections-agent/packages/agent-core/src/tools.ts +17 -6
  314. package/templates/collections-agent/packages/agent-core/src/types.ts +54 -1
  315. package/templates/collections-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  316. package/templates/collections-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  317. package/templates/collections-agent/packages/agent-core/test/composition.test.ts +204 -0
  318. package/templates/collections-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  319. package/templates/collections-agent/packages/agent-core/test/helpers.ts +4 -1
  320. package/templates/collections-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  321. package/templates/collections-agent/packages/agent-core/test/quote.test.ts +198 -0
  322. package/templates/collections-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  323. package/templates/collections-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  324. package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +103 -3
  325. package/templates/collections-agent/packages/agent-runtime/README.md +12 -0
  326. package/templates/collections-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  327. package/templates/collections-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  328. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  329. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  330. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  331. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  332. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  333. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  334. package/templates/collections-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  335. package/templates/collections-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  336. package/templates/collections-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  337. package/templates/collections-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  338. package/templates/collections-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  339. package/templates/collections-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  340. package/templates/collections-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  341. package/templates/collections-agent/packages/agent-runtime/src/index.ts +1 -0
  342. package/templates/collections-agent/packages/agent-runtime/src/inspect.ts +5 -1
  343. package/templates/collections-agent/packages/agent-runtime/src/kit.ts +25 -2
  344. package/templates/collections-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  345. package/templates/collections-agent/packages/agent-runtime/src/setup.ts +13 -3
  346. package/templates/collections-agent/packages/agent-runtime/src/terminal.ts +127 -18
  347. package/templates/collections-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  348. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  349. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  350. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  351. package/templates/collections-agent/scripts/whatsapp-emulator.mjs +84 -0
  352. package/templates/hello-agent/README.md +1 -1
  353. package/templates/hello-agent/packages/agent-core/README.md +14 -1
  354. package/templates/hello-agent/packages/agent-core/package.json +1 -1
  355. package/templates/hello-agent/packages/agent-core/src/agent.ts +39 -1
  356. package/templates/hello-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  357. package/templates/hello-agent/packages/agent-core/src/api/client.ts +28 -5
  358. package/templates/hello-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  359. package/templates/hello-agent/packages/agent-core/src/api/rail.ts +62 -26
  360. package/templates/hello-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  361. package/templates/hello-agent/packages/agent-core/src/approval.ts +14 -2
  362. package/templates/hello-agent/packages/agent-core/src/bundle.ts +35 -3
  363. package/templates/hello-agent/packages/agent-core/src/channels.ts +49 -3
  364. package/templates/hello-agent/packages/agent-core/src/check.ts +25 -2
  365. package/templates/hello-agent/packages/agent-core/src/engine.ts +235 -46
  366. package/templates/hello-agent/packages/agent-core/src/hash.ts +57 -1
  367. package/templates/hello-agent/packages/agent-core/src/index.ts +2 -0
  368. package/templates/hello-agent/packages/agent-core/src/quote.ts +50 -0
  369. package/templates/hello-agent/packages/agent-core/src/rail.ts +89 -4
  370. package/templates/hello-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  371. package/templates/hello-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  372. package/templates/hello-agent/packages/agent-core/src/revocation.ts +1 -1
  373. package/templates/hello-agent/packages/agent-core/src/state-machine.ts +10 -1
  374. package/templates/hello-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  375. package/templates/hello-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  376. package/templates/hello-agent/packages/agent-core/src/tools.ts +17 -6
  377. package/templates/hello-agent/packages/agent-core/src/types.ts +54 -1
  378. package/templates/hello-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  379. package/templates/hello-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  380. package/templates/hello-agent/packages/agent-core/test/composition.test.ts +204 -0
  381. package/templates/hello-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  382. package/templates/hello-agent/packages/agent-core/test/helpers.ts +4 -1
  383. package/templates/hello-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  384. package/templates/hello-agent/packages/agent-core/test/quote.test.ts +198 -0
  385. package/templates/hello-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  386. package/templates/hello-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  387. package/templates/hello-agent/packages/agent-core/test/receivables.test.ts +103 -3
  388. package/templates/hello-agent/packages/agent-runtime/README.md +12 -0
  389. package/templates/hello-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  390. package/templates/hello-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  391. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  392. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  393. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  394. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  395. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  396. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  397. package/templates/hello-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  398. package/templates/hello-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  399. package/templates/hello-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  400. package/templates/hello-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  401. package/templates/hello-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  402. package/templates/hello-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  403. package/templates/hello-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  404. package/templates/hello-agent/packages/agent-runtime/src/index.ts +1 -0
  405. package/templates/hello-agent/packages/agent-runtime/src/inspect.ts +5 -1
  406. package/templates/hello-agent/packages/agent-runtime/src/kit.ts +25 -2
  407. package/templates/hello-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  408. package/templates/hello-agent/packages/agent-runtime/src/setup.ts +13 -3
  409. package/templates/hello-agent/packages/agent-runtime/src/terminal.ts +127 -18
  410. package/templates/hello-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  411. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  412. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  413. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  414. package/templates/kits.lock.json +29 -6
  415. package/templates/supplier-payments-agent/README.md +1 -1
  416. package/templates/supplier-payments-agent/agents/supplier-payments-agent/AGENTS.md +1 -1
  417. package/templates/supplier-payments-agent/agents/supplier-payments-agent/CLAUDE.md +1 -1
  418. package/templates/supplier-payments-agent/agents/supplier-payments-agent/README.md +11 -9
  419. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/kit.ts +1 -1
  420. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/modules/batch-payout.ts +67 -20
  421. package/templates/supplier-payments-agent/agents/supplier-payments-agent/test/batch.test.ts +504 -5
  422. package/templates/supplier-payments-agent/packages/agent-core/README.md +14 -1
  423. package/templates/supplier-payments-agent/packages/agent-core/package.json +1 -1
  424. package/templates/supplier-payments-agent/packages/agent-core/src/agent.ts +39 -1
  425. package/templates/supplier-payments-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  426. package/templates/supplier-payments-agent/packages/agent-core/src/api/client.ts +28 -5
  427. package/templates/supplier-payments-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  428. package/templates/supplier-payments-agent/packages/agent-core/src/api/rail.ts +62 -26
  429. package/templates/supplier-payments-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  430. package/templates/supplier-payments-agent/packages/agent-core/src/approval.ts +14 -2
  431. package/templates/supplier-payments-agent/packages/agent-core/src/bundle.ts +35 -3
  432. package/templates/supplier-payments-agent/packages/agent-core/src/channels.ts +49 -3
  433. package/templates/supplier-payments-agent/packages/agent-core/src/check.ts +25 -2
  434. package/templates/supplier-payments-agent/packages/agent-core/src/engine.ts +235 -46
  435. package/templates/supplier-payments-agent/packages/agent-core/src/hash.ts +57 -1
  436. package/templates/supplier-payments-agent/packages/agent-core/src/index.ts +2 -0
  437. package/templates/supplier-payments-agent/packages/agent-core/src/quote.ts +50 -0
  438. package/templates/supplier-payments-agent/packages/agent-core/src/rail.ts +89 -4
  439. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  440. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  441. package/templates/supplier-payments-agent/packages/agent-core/src/revocation.ts +1 -1
  442. package/templates/supplier-payments-agent/packages/agent-core/src/state-machine.ts +10 -1
  443. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  444. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  445. package/templates/supplier-payments-agent/packages/agent-core/src/tools.ts +17 -6
  446. package/templates/supplier-payments-agent/packages/agent-core/src/types.ts +54 -1
  447. package/templates/supplier-payments-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  448. package/templates/supplier-payments-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  449. package/templates/supplier-payments-agent/packages/agent-core/test/composition.test.ts +204 -0
  450. package/templates/supplier-payments-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  451. package/templates/supplier-payments-agent/packages/agent-core/test/helpers.ts +4 -1
  452. package/templates/supplier-payments-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  453. package/templates/supplier-payments-agent/packages/agent-core/test/quote.test.ts +198 -0
  454. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  455. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  456. package/templates/supplier-payments-agent/packages/agent-core/test/receivables.test.ts +103 -3
  457. package/templates/supplier-payments-agent/packages/agent-runtime/README.md +12 -0
  458. package/templates/supplier-payments-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  459. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  460. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  461. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  462. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  463. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  464. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  465. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  466. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  467. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  468. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  469. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  470. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  471. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  472. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  473. package/templates/supplier-payments-agent/packages/agent-runtime/src/index.ts +1 -0
  474. package/templates/supplier-payments-agent/packages/agent-runtime/src/inspect.ts +5 -1
  475. package/templates/supplier-payments-agent/packages/agent-runtime/src/kit.ts +25 -2
  476. package/templates/supplier-payments-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  477. package/templates/supplier-payments-agent/packages/agent-runtime/src/setup.ts +13 -3
  478. package/templates/supplier-payments-agent/packages/agent-runtime/src/terminal.ts +127 -18
  479. package/templates/supplier-payments-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  480. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  481. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  482. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  483. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  484. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  485. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  486. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
@@ -0,0 +1,182 @@
1
+ /**
2
+ * §46's three product gaps, as the channel now answers them, with no network:
3
+ * a status webhook is read (a `failed` of an outcome's message is that
4
+ * outcome NOT told), a tapped quick reply becomes a turn only as the intent a
5
+ * declared template gave it, and the registry carries a fallback for an
6
+ * outcome the kit has no copy for. The same behaviour against the emulator is
7
+ * in the repository's `test/whatsapp-emulator.integration.test.ts`, and the
8
+ * `check` rule that requires the fallback, asked of the shipped agents, in
9
+ * `test/whatsapp-fallback-check.test.ts`: both read this repository's other
10
+ * agents and scripts, which a template made from one agent does not carry.
11
+ */
12
+ import { describe, expect, it } from "vitest";
13
+ import type { WhatsAppTemplate } from "@codespar/agent-core";
14
+ import { WhatsAppChannel } from "../src/channels/whatsapp/index.js";
15
+ import { buildSendRequest, parseInbound, type CloudApiConfig } from "../src/channels/whatsapp/cloud-api.js";
16
+ import type { ChannelBackend, ChannelLogLine, InboundMessage, OutboundBody, SentMessage, StatusUpdate } from "../src/channels/types.js";
17
+
18
+ const CONTACT = "+5511987654321";
19
+ const NOW = new Date("2026-09-23T17:00:00Z");
20
+
21
+ /** A backend the test drives: it queues what the person does and hands the channel the statuses the test reports. */
22
+ class Scripted implements ChannelBackend {
23
+ readonly name = "scripted";
24
+ readonly live = false;
25
+ readonly delivered: OutboundBody[] = [];
26
+ private listener: ((s: StatusUpdate) => void) | undefined;
27
+ constructor(private readonly inbox: InboundMessage[] = []) {}
28
+ async open(): Promise<void> {}
29
+ async next(): Promise<InboundMessage | undefined> {
30
+ return this.inbox.shift();
31
+ }
32
+ async deliver(_to: string, body: OutboundBody): Promise<SentMessage> {
33
+ this.delivered.push(body);
34
+ return { id: `wamid.test.${this.delivered.length}`, state: "sent" };
35
+ }
36
+ onStatus(listener: (s: StatusUpdate) => void): void {
37
+ this.listener = listener;
38
+ }
39
+ report(status: StatusUpdate): void {
40
+ this.listener?.(status);
41
+ }
42
+ async close(): Promise<void> {}
43
+ }
44
+
45
+ const TEMPLATES: WhatsAppTemplate[] = [
46
+ { name: "acordo_quitado", language: "pt_BR", description: "paid", body: "Oi! {{1}} quitado." },
47
+ {
48
+ name: "acordo_cobranca_vencida",
49
+ language: "pt_BR",
50
+ description: "expired",
51
+ body: "Oi! A cobranca do {{1}} venceu.",
52
+ buttons: [
53
+ { id: "emitir_nova", title: "Emitir nova", intent: "quero que voce emita uma nova cobranca para o meu acordo" },
54
+ { id: "agora_nao", title: "Agora nao", intent: "agora nao quero uma nova cobranca" },
55
+ ],
56
+ },
57
+ { name: "atendimento_atualizacao", language: "pt_BR", description: "fallback", body: "Oi! Temos uma atualizacao.", fallback: true },
58
+ ];
59
+
60
+ function channel(inbox: InboundMessage[] = []) {
61
+ const backend = new Scripted(inbox);
62
+ const said: string[] = [];
63
+ const failed: ChannelLogLine[] = [];
64
+ const ch = new WhatsAppChannel({ backend, conversation: { contact: CONTACT, subject: "acordo-1042" }, now: () => NOW, templates: TEMPLATES, say: (l) => said.push(l), onDeliveryFailed: (l) => failed.push(l) });
65
+ return { ch, backend, said, failed };
66
+ }
67
+
68
+ const tap = (id: string, extra: Partial<InboundMessage["reply"]> = {}): InboundMessage => ({ id: `wamid.in.${id}`, from: CONTACT, text: "Emitir nova", timestamp: 1790190000, reply: { type: "button_reply", id, title: "Emitir nova", ...extra } });
69
+
70
+ describe("1. a status webhook is read, and a failed outcome is an outcome not told", () => {
71
+ it("parses statuses, with Meta's errors, out of the same delivery that carries messages", () => {
72
+ const parsed = parseInbound({
73
+ entry: [{ changes: [{ value: { statuses: [{ id: "wamid.a", status: "failed", timestamp: "1790190000", errors: [{ code: 131026, title: "Message undeliverable", error_data: { details: "not on whatsapp" } }] }, { id: "wamid.b", status: "read", timestamp: "1790190001" }, { id: "wamid.c", status: "deleted" }] } }] }],
74
+ });
75
+ expect(parsed.statuses).toEqual([
76
+ { message_id: "wamid.a", status: "failed", timestamp: 1790190000, errors: [{ code: 131026, title: "Message undeliverable", details: "not on whatsapp" }] },
77
+ { message_id: "wamid.b", status: "read", timestamp: 1790190001, errors: [] },
78
+ ]);
79
+ });
80
+
81
+ it("a failed status of a message that told an outcome is recorded, reaches the operator and the outcome's owner, and names the outcome", async () => {
82
+ const { ch, backend, said, failed } = channel();
83
+ await ch.open();
84
+ (ch as unknown as { session: { observeInbound(t: number): void } }).session.observeInbound(Math.floor(NOW.getTime() / 1000));
85
+ const sent = await ch.send({ kind: "text", text: "Recebemos, acordo quitado.", about: { execution_id: "exe_1", state: "settled" } });
86
+ backend.report({ message_id: sent.id, status: "failed", timestamp: 1790190100, errors: [{ code: 131026, details: "not on whatsapp" }] });
87
+ expect(ch.deliveryOf(sent.id)).toBe("failed");
88
+ const status = ch.log().find((l) => l.direction === "status")!;
89
+ expect(status).toMatchObject({ kind: "status", message_id: sent.id, state: "failed", about: { execution_id: "exe_1", state: "settled" }, errors: [{ code: 131026, details: "not on whatsapp" }] });
90
+ expect(failed).toEqual([status]);
91
+ expect(said.join("\n")).toContain("a pessoa NAO foi avisada");
92
+ });
93
+
94
+ it("a read is recorded and nothing more: not consent, not an acknowledgement, not a reply", async () => {
95
+ const { ch, backend, said, failed } = channel();
96
+ await ch.open();
97
+ (ch as unknown as { session: { observeInbound(t: number): void } }).session.observeInbound(Math.floor(NOW.getTime() / 1000));
98
+ const sent = await ch.send({ kind: "text", text: "Recebemos.", about: { execution_id: "exe_1", state: "settled" } });
99
+ backend.report({ message_id: sent.id, status: "read", timestamp: 1790190100, errors: [] });
100
+ expect(ch.log().filter((l) => l.direction === "status").map((l) => l.state)).toEqual(["read"]);
101
+ expect(failed).toEqual([]);
102
+ expect(said).toEqual([]);
103
+ // A read is not a turn either.
104
+ expect(await ch.next()).toBeUndefined();
105
+ });
106
+
107
+ it("a failure the provider reports BEFORE the send answered (the emulator posts statuses synchronously) is still tied to its outcome", async () => {
108
+ class Eager extends Scripted {
109
+ override async deliver(to: string, body: OutboundBody): Promise<SentMessage> {
110
+ const sent = await super.deliver(to, body);
111
+ this.report({ message_id: sent.id, status: "failed", timestamp: 1, errors: [{ code: 131026 }] });
112
+ return sent;
113
+ }
114
+ }
115
+ const backend = new Eager();
116
+ const failed: ChannelLogLine[] = [];
117
+ const ch = new WhatsAppChannel({ backend, conversation: { contact: CONTACT }, now: () => NOW, templates: TEMPLATES, onDeliveryFailed: (l) => failed.push(l) });
118
+ await ch.open();
119
+ await ch.send({ kind: "template", template: "acordo_quitado", language: "pt_BR", variables: ["acordo-1042"], about: { execution_id: "exe_2", state: "settled" } });
120
+ await new Promise((r) => setTimeout(r, 0));
121
+ expect(failed.map((l) => l.about)).toEqual([{ execution_id: "exe_2", state: "settled" }]);
122
+ });
123
+
124
+ it("a status about a message an EARLIER run sent is tied back to the outcome that run recorded", async () => {
125
+ const prior: ChannelLogLine = { at: NOW.toISOString(), direction: "out", contact: "+55*****4321", kind: "template", message_id: "wamid.old", state: "sent", about: { execution_id: "exe_old", state: "settled" } };
126
+ const backend = new Scripted();
127
+ const failed: ChannelLogLine[] = [];
128
+ const ch = new WhatsAppChannel({ backend, conversation: { contact: CONTACT }, now: () => NOW, templates: TEMPLATES, priorLines: [prior], onDeliveryFailed: (l) => failed.push(l) });
129
+ await ch.open();
130
+ backend.report({ message_id: "wamid.old", status: "failed", timestamp: 1, errors: [{ code: 131026 }] });
131
+ expect(failed[0]?.about).toEqual({ execution_id: "exe_old", state: "settled" });
132
+ });
133
+ });
134
+
135
+ describe("2. a tapped quick reply is a turn, as the intent a declared template gave it", () => {
136
+ it("parses a button_reply, a list_reply and a template's quick-reply button, id and context kept", () => {
137
+ const msg = (m: Record<string, unknown>) => ({ entry: [{ changes: [{ value: { messages: [{ from: "5511987654321", timestamp: "1", ...m }] } }] }] });
138
+ expect(parseInbound(msg({ id: "a", type: "interactive", interactive: { type: "button_reply", button_reply: { id: "emitir_nova", title: "Emitir nova" } }, context: { id: "wamid.offer" } })).messages[0]?.reply).toEqual({ type: "button_reply", id: "emitir_nova", title: "Emitir nova", context_id: "wamid.offer" });
139
+ expect(parseInbound(msg({ id: "b", type: "interactive", interactive: { type: "list_reply", list_reply: { id: "3x", title: "3x", description: "tres" } } })).messages[0]?.reply).toEqual({ type: "list_reply", id: "3x", title: "3x" });
140
+ expect(parseInbound(msg({ id: "c", type: "button", button: { payload: "emitir_nova", text: "Emitir nova" } })).messages[0]?.reply).toEqual({ type: "button", id: "emitir_nova", title: "Emitir nova" });
141
+ // What this channel cannot read is still named, not turned into an empty message.
142
+ expect(parseInbound(msg({ id: "d", type: "image" })).ignored).toEqual([{ id: "d", type: "image" }]);
143
+ expect(parseInbound(msg({ id: "e", type: "interactive", interactive: { type: "nfm_reply" } })).ignored).toEqual([{ id: "e", type: "interactive:nfm_reply" }]);
144
+ });
145
+
146
+ it("a declared id is a turn whose text is the declared intent, never the title", async () => {
147
+ const { ch } = channel([tap("emitir_nova")]);
148
+ const turn = await ch.next();
149
+ expect(turn?.text).toBe("quero que voce emita uma nova cobranca para o meu acordo");
150
+ const line = ch.log().at(-1)!;
151
+ expect(line).toMatchObject({ direction: "in", kind: "reply", reply: { id: "emitir_nova", title: "Emitir nova" }, text: "quero que voce emita uma nova cobranca para o meu acordo" });
152
+ });
153
+
154
+ it("an id no template declares is not a turn: recorded, told to the operator, and the next real turn is what comes back", async () => {
155
+ const typed: InboundMessage = { id: "wamid.in.text", from: CONTACT, text: "oi", timestamp: 1790190001 };
156
+ const { ch, said } = channel([tap("pagar_tudo", { title: "Pagar tudo" }), typed]);
157
+ const turn = await ch.next();
158
+ expect(turn?.text).toBe("oi");
159
+ expect(ch.log()[0]).toMatchObject({ kind: "reply", refused: { rule: "reply_not_offered" }, reply: { id: "pagar_tudo" } });
160
+ expect(said.join("\n")).toContain("reply_not_offered");
161
+ });
162
+
163
+ it("a tap that names a message this conversation did not send with that reply is not a turn either", async () => {
164
+ const { ch } = channel([tap("emitir_nova", { context_id: "wamid.somebody_else" })]);
165
+ expect(await ch.next()).toBeUndefined();
166
+ expect(ch.log()[0]?.refused?.rule).toBe("reply_not_offered");
167
+ });
168
+
169
+ it("a template goes out with the quick replies its declaration offers, and the log says which", async () => {
170
+ const { ch, backend } = channel();
171
+ await ch.send({ kind: "template", template: "acordo_cobranca_vencida", language: "pt_BR", variables: ["acordo-1042"] });
172
+ expect((backend.delivered[0] as { buttons?: unknown }).buttons).toEqual([{ id: "emitir_nova", title: "Emitir nova" }, { id: "agora_nao", title: "Agora nao" }]);
173
+ expect(ch.log()[0]?.offered).toEqual(["emitir_nova", "agora_nao"]);
174
+ const config: CloudApiConfig = { baseUrl: "http://sim", phoneNumberId: "1", accessToken: "t", verifyToken: "v", appSecret: "s", apiVersion: "v22.0", webhookPort: 0 };
175
+ const request = buildSendRequest(config, CONTACT, backend.delivered[0]!) as { body: string };
176
+ expect(JSON.parse(request.body).template.components).toEqual([
177
+ { type: "body", parameters: [{ type: "text", text: "acordo-1042" }] },
178
+ { type: "button", sub_type: "quick_reply", index: "0", parameters: [{ type: "payload", payload: "emitir_nova" }] },
179
+ { type: "button", sub_type: "quick_reply", index: "1", parameters: [{ type: "payload", payload: "agora_nao" }] },
180
+ ]);
181
+ });
182
+ });
@@ -1,204 +0,0 @@
1
- /**
2
- * What the emulator does, measured rather than assumed — and what it does NOT
3
- * do, which is the more useful half.
4
- *
5
- * These run against `dyvit-wa-sim` (https://github.com/fabianocruz/whatsapp-simulator,
6
- * MIT) at the version `scripts/whatsapp-emulator.mjs` pins. They SKIP when it is
7
- * not listening, so `npm test` is green on a machine that never started it; the
8
- * CI starts it, and `npm run whatsapp:gate` fails loudly rather than skipping,
9
- * so nothing important hides behind a skip.
10
- *
11
- * Two kinds of assertion live here and they are not the same thing.
12
- *
13
- * What the emulator gets RIGHT, which our adapter depends on: the Cloud API
14
- * response shape, the signed webhook, the controllable clock. If one of these
15
- * breaks, our channel breaks.
16
- *
17
- * What the emulator does NOT do, which our channel has to cover itself. Those
18
- * assertions are written to FAIL the day the emulator closes the gap — which
19
- * is what we want, because the pin means it can only change when we move it,
20
- * and the failure is the notification. They are a spec for the repo's owner,
21
- * not a complaint: `docs/OPEN_QUESTIONS.md` §46 carries the same list in
22
- * prose with the payloads.
23
- */
24
- import { describe, expect, it } from "vitest";
25
- import { EmulatorDriver } from "../src/channels/whatsapp/emulator.js";
26
- import { buildSendRequest, type CloudApiConfig } from "../src/channels/whatsapp/cloud-api.js";
27
-
28
- const URL_BASE = process.env["WHATSAPP_SIM_URL"] ?? "http://127.0.0.1:4290";
29
-
30
- /**
31
- * Probed at MODULE level, not in a `beforeAll`: `describe.skipIf` is read when
32
- * the file is collected, which is before any hook has run, so a flag set in a
33
- * hook would skip every case on a machine where the emulator is up.
34
- */
35
- const up = await (async () => {
36
- try {
37
- return (await fetch(`${URL_BASE}/health`, { signal: AbortSignal.timeout(2000) })).ok;
38
- } catch {
39
- return false;
40
- }
41
- })();
42
- if (!up) process.stderr.write(`[whatsapp] no emulator at ${URL_BASE}; its integration cases are skipped. Start it with \`npm run whatsapp:emulator\`.\n`);
43
-
44
- /** A fresh phone-number id per case, because the emulator keys a conversation on it. */
45
- const pnid = () => `9${String(Math.floor(Math.random() * 1e11)).padStart(11, "0")}`;
46
- const TO = "5511987654321";
47
-
48
- const config = (phoneNumberId: string): CloudApiConfig => ({
49
- baseUrl: URL_BASE,
50
- phoneNumberId,
51
- accessToken: "emulator",
52
- verifyToken: "emulator",
53
- appSecret: "dev",
54
- apiVersion: "v22.0",
55
- webhookPort: 0,
56
- });
57
-
58
- async function send(phoneNumberId: string, body: Parameters<typeof buildSendRequest>[2]) {
59
- const request = buildSendRequest(config(phoneNumberId), `+${TO}`, body);
60
- if ("unsupported" in request) return { status: 0, unsupported: request.unsupported, body: null as unknown };
61
- const response = await fetch(request.url, { method: request.method, headers: request.headers, body: request.body });
62
- return { status: response.status, unsupported: null, body: (await response.json()) as Record<string, unknown> };
63
- }
64
-
65
- async function raw(path: string, init?: RequestInit) {
66
- const response = await fetch(`${URL_BASE}${path}`, init);
67
- return { status: response.status, body: (await response.json().catch(() => null)) as Record<string, unknown> | null };
68
- }
69
-
70
- describe.skipIf(!up)("what our adapter depends on, and the emulator provides", () => {
71
- it("answers our text send with the Cloud API's own response shape", async () => {
72
- const id = pnid();
73
- const result = await send(id, { kind: "text", text: "Oi, Joana!" });
74
- expect(result.status).toBe(200);
75
- expect(result.body).toMatchObject({ messaging_product: "whatsapp" });
76
- const messages = (result.body as { messages: Array<{ id: string; message_status: string }> }).messages;
77
- expect(messages[0]!.id).toMatch(/^wamid\./);
78
- expect(messages[0]!.message_status).toBe("accepted");
79
- });
80
-
81
- it("takes the Pix copy-and-paste as a plain text message: the string that pays goes through unchanged", async () => {
82
- const brcode = "00020126580014br.gov.bcb.pix0136stub-chg_abc5204000053039865802BR5909CODESPAR6009SAO PAULO62070503***6304STUB";
83
- const result = await send(pnid(), { kind: "instrument", instrument: "pix_copy_paste", value: brcode });
84
- expect(result.status).toBe(200);
85
- });
86
-
87
- it("takes a template with its body parameters", async () => {
88
- const result = await send(pnid(), { kind: "template", template: "cobranca_lembrete", language: "pt_BR", variables: ["Joana"] });
89
- expect(result.status).toBe(200);
90
- });
91
-
92
- it("has a conversation clock we can pin and move, which is the only way a 24h window ever closes in a replay", async () => {
93
- const driver = new EmulatorDriver(URL_BASE);
94
- const pinned = (await driver.pin(new Date("2026-09-23T17:00:00.000Z"))) as { now: string };
95
- expect(pinned.now).toBe("2026-09-23T17:00:00.000Z");
96
- const moved = (await driver.advanceHours(26)) as { now: string };
97
- expect(Date.parse(moved.now) - Date.parse(pinned.now)).toBeGreaterThanOrEqual(26 * 3600 * 1000);
98
- });
99
-
100
- it("delivers an inbound message as a webhook our own parser reads", async () => {
101
- const id = pnid();
102
- const driver = new EmulatorDriver(URL_BASE);
103
- await driver.inbound({ phoneNumberId: id, from: `+${TO}`, text: "oi, recebi a mensagem" });
104
- const { body } = await raw("/_sim/webhooks");
105
- const deliveries = (body as { deliveries: Array<{ body: unknown }> }).deliveries;
106
- const mine = deliveries.map((d) => d.body).filter((b) => JSON.stringify(b).includes(id));
107
- expect(mine.length).toBeGreaterThan(0);
108
- });
109
- });
110
-
111
- /**
112
- * The gaps. Each one is the exact payload that failed, so the assertion doubles
113
- * as the report. They are written against the PINNED sha: a green run here
114
- * means the gap is still open, and a red one means it was closed and our own
115
- * cover for it can be reconsidered.
116
- */
117
- describe.skipIf(!up)("what the emulator does not do, measured", () => {
118
- it("GAP: accepts a free-form message outside the 24h window that Meta refuses with 131047 — and it already knows", async () => {
119
- const id = pnid();
120
- const driver = new EmulatorDriver(URL_BASE);
121
- await driver.pin(new Date("2026-09-23T17:00:00.000Z"));
122
- await driver.inbound({ phoneNumberId: id, from: `+${TO}`, text: "oi" });
123
- expect((await send(id, { kind: "text", text: "dentro da janela" })).status).toBe(200);
124
-
125
- await driver.advanceHours(26);
126
- const outside = await send(id, { kind: "text", text: "Recebemos, acordo quitado." });
127
- // The real Cloud API answers 400 with error 131047 and sends nothing.
128
- expect(outside.status).toBe(200);
129
-
130
- // And this is what makes it a one-line fix rather than a feature: the
131
- // pricing engine has ALREADY decided the message is invalid there.
132
- const state = (await raw(`/_sim/state?key=${id}:${TO}`)).body as {
133
- messages: Array<{ id: string }>;
134
- priced: { byMessageId: Record<string, { reasonCode: string }> };
135
- };
136
- const last = state.messages[state.messages.length - 1]!;
137
- expect(state.priced.byMessageId[last.id]!.reasonCode).toBe("INVALID_NON_TEMPLATE_OUTSIDE_CSW");
138
- });
139
-
140
- it("GAP: has no way to redeliver or reorder a webhook, so duplicate and out-of-order delivery cannot be driven", async () => {
141
- for (const path of ["/_sim/replay", "/_sim/webhooks/replay", "/_sim/redeliver"]) {
142
- const { status } = await raw(path, { method: "POST", headers: { "content-type": "application/json" }, body: "{}" });
143
- expect(status, path).toBe(404);
144
- }
145
- // `GET /_sim/webhooks` is a read-only log; nothing re-dispatches from it.
146
- const { status, body } = await raw("/_sim/webhooks");
147
- expect(status).toBe(200);
148
- expect(Array.isArray((body as { deliveries: unknown[] }).deliveries)).toBe(true);
149
- });
150
-
151
- it("GAP: an inbound interactive reply degrades to an empty text message, so a debtor cannot tap a button", async () => {
152
- const id = pnid();
153
- const { status, body } = await raw("/_sim/inbound", {
154
- method: "POST",
155
- headers: { "content-type": "application/json" },
156
- body: JSON.stringify({
157
- phone_number_id: id,
158
- from: `+${TO}`,
159
- type: "interactive",
160
- interactive: { type: "button_reply", button_reply: { id: "avista", title: "À vista" } },
161
- }),
162
- });
163
- expect(status).toBe(200);
164
- const message = (body as { message: { contentType: string; bodyPreview: string } }).message;
165
- // The interactive payload is dropped: outbound buttons work, the reply to one does not exist.
166
- expect(message.contentType).toBe("text");
167
- expect(message.bodyPreview).toBe("");
168
- });
169
-
170
- it("GAP: only `sent` and `delivered` are ever emitted, so a channel cannot observe a read receipt or a failure", async () => {
171
- const id = pnid();
172
- await send(id, { kind: "text", text: "uma mensagem" });
173
- const { body } = await raw("/_sim/webhooks");
174
- const statuses = (body as { deliveries: Array<{ body: unknown }> }).deliveries
175
- .map((d) => d.body as { entry?: Array<{ id?: string; changes?: Array<{ value?: { statuses?: Array<{ status: string }> } }> }> })
176
- .filter((b) => b.entry?.[0]?.id === id)
177
- .flatMap((b) => b.entry?.[0]?.changes?.[0]?.value?.statuses ?? [])
178
- .map((s) => s.status);
179
- expect(statuses).toEqual(["sent", "delivered"]);
180
- });
181
-
182
- it("GAP: a `+` on one side and none on the other splits one person into two conversations", async () => {
183
- const id = pnid();
184
- const driver = new EmulatorDriver(URL_BASE);
185
- // Our driver strips the `+` precisely to avoid this; done by hand here, it splits.
186
- await raw("/_sim/inbound", {
187
- method: "POST",
188
- headers: { "content-type": "application/json" },
189
- body: JSON.stringify({ phone_number_id: id, from: `+${TO}`, text: "oi" }),
190
- });
191
- await send(id, { kind: "text", text: "resposta" });
192
- const miss = await raw(`/_sim/state?key=${id}:not-a-conversation`);
193
- const keys = (miss.body as { keys: string[] }).keys;
194
- expect(keys).toContain(`${id}:+${TO}`);
195
- expect(keys).toContain(`${id}:${TO}`);
196
-
197
- // With our driver, which strips it, there is one conversation and the window opens.
198
- const same = pnid();
199
- await driver.inbound({ phoneNumberId: same, from: `+${TO}`, text: "oi" });
200
- await send(same, { kind: "text", text: "resposta" });
201
- const keysAfter = ((await raw(`/_sim/state?key=${same}:not-a-conversation`)).body as { keys: string[] }).keys;
202
- expect(keysAfter.filter((k) => k.startsWith(`${same}:`))).toEqual([`${same}:${TO}`]);
203
- });
204
- });
@@ -1,204 +0,0 @@
1
- /**
2
- * What the emulator does, measured rather than assumed — and what it does NOT
3
- * do, which is the more useful half.
4
- *
5
- * These run against `dyvit-wa-sim` (https://github.com/fabianocruz/whatsapp-simulator,
6
- * MIT) at the version `scripts/whatsapp-emulator.mjs` pins. They SKIP when it is
7
- * not listening, so `npm test` is green on a machine that never started it; the
8
- * CI starts it, and `npm run whatsapp:gate` fails loudly rather than skipping,
9
- * so nothing important hides behind a skip.
10
- *
11
- * Two kinds of assertion live here and they are not the same thing.
12
- *
13
- * What the emulator gets RIGHT, which our adapter depends on: the Cloud API
14
- * response shape, the signed webhook, the controllable clock. If one of these
15
- * breaks, our channel breaks.
16
- *
17
- * What the emulator does NOT do, which our channel has to cover itself. Those
18
- * assertions are written to FAIL the day the emulator closes the gap — which
19
- * is what we want, because the pin means it can only change when we move it,
20
- * and the failure is the notification. They are a spec for the repo's owner,
21
- * not a complaint: `docs/OPEN_QUESTIONS.md` §46 carries the same list in
22
- * prose with the payloads.
23
- */
24
- import { describe, expect, it } from "vitest";
25
- import { EmulatorDriver } from "../src/channels/whatsapp/emulator.js";
26
- import { buildSendRequest, type CloudApiConfig } from "../src/channels/whatsapp/cloud-api.js";
27
-
28
- const URL_BASE = process.env["WHATSAPP_SIM_URL"] ?? "http://127.0.0.1:4290";
29
-
30
- /**
31
- * Probed at MODULE level, not in a `beforeAll`: `describe.skipIf` is read when
32
- * the file is collected, which is before any hook has run, so a flag set in a
33
- * hook would skip every case on a machine where the emulator is up.
34
- */
35
- const up = await (async () => {
36
- try {
37
- return (await fetch(`${URL_BASE}/health`, { signal: AbortSignal.timeout(2000) })).ok;
38
- } catch {
39
- return false;
40
- }
41
- })();
42
- if (!up) process.stderr.write(`[whatsapp] no emulator at ${URL_BASE}; its integration cases are skipped. Start it with \`npm run whatsapp:emulator\`.\n`);
43
-
44
- /** A fresh phone-number id per case, because the emulator keys a conversation on it. */
45
- const pnid = () => `9${String(Math.floor(Math.random() * 1e11)).padStart(11, "0")}`;
46
- const TO = "5511987654321";
47
-
48
- const config = (phoneNumberId: string): CloudApiConfig => ({
49
- baseUrl: URL_BASE,
50
- phoneNumberId,
51
- accessToken: "emulator",
52
- verifyToken: "emulator",
53
- appSecret: "dev",
54
- apiVersion: "v22.0",
55
- webhookPort: 0,
56
- });
57
-
58
- async function send(phoneNumberId: string, body: Parameters<typeof buildSendRequest>[2]) {
59
- const request = buildSendRequest(config(phoneNumberId), `+${TO}`, body);
60
- if ("unsupported" in request) return { status: 0, unsupported: request.unsupported, body: null as unknown };
61
- const response = await fetch(request.url, { method: request.method, headers: request.headers, body: request.body });
62
- return { status: response.status, unsupported: null, body: (await response.json()) as Record<string, unknown> };
63
- }
64
-
65
- async function raw(path: string, init?: RequestInit) {
66
- const response = await fetch(`${URL_BASE}${path}`, init);
67
- return { status: response.status, body: (await response.json().catch(() => null)) as Record<string, unknown> | null };
68
- }
69
-
70
- describe.skipIf(!up)("what our adapter depends on, and the emulator provides", () => {
71
- it("answers our text send with the Cloud API's own response shape", async () => {
72
- const id = pnid();
73
- const result = await send(id, { kind: "text", text: "Oi, Joana!" });
74
- expect(result.status).toBe(200);
75
- expect(result.body).toMatchObject({ messaging_product: "whatsapp" });
76
- const messages = (result.body as { messages: Array<{ id: string; message_status: string }> }).messages;
77
- expect(messages[0]!.id).toMatch(/^wamid\./);
78
- expect(messages[0]!.message_status).toBe("accepted");
79
- });
80
-
81
- it("takes the Pix copy-and-paste as a plain text message: the string that pays goes through unchanged", async () => {
82
- const brcode = "00020126580014br.gov.bcb.pix0136stub-chg_abc5204000053039865802BR5909CODESPAR6009SAO PAULO62070503***6304STUB";
83
- const result = await send(pnid(), { kind: "instrument", instrument: "pix_copy_paste", value: brcode });
84
- expect(result.status).toBe(200);
85
- });
86
-
87
- it("takes a template with its body parameters", async () => {
88
- const result = await send(pnid(), { kind: "template", template: "cobranca_lembrete", language: "pt_BR", variables: ["Joana"] });
89
- expect(result.status).toBe(200);
90
- });
91
-
92
- it("has a conversation clock we can pin and move, which is the only way a 24h window ever closes in a replay", async () => {
93
- const driver = new EmulatorDriver(URL_BASE);
94
- const pinned = (await driver.pin(new Date("2026-09-23T17:00:00.000Z"))) as { now: string };
95
- expect(pinned.now).toBe("2026-09-23T17:00:00.000Z");
96
- const moved = (await driver.advanceHours(26)) as { now: string };
97
- expect(Date.parse(moved.now) - Date.parse(pinned.now)).toBeGreaterThanOrEqual(26 * 3600 * 1000);
98
- });
99
-
100
- it("delivers an inbound message as a webhook our own parser reads", async () => {
101
- const id = pnid();
102
- const driver = new EmulatorDriver(URL_BASE);
103
- await driver.inbound({ phoneNumberId: id, from: `+${TO}`, text: "oi, recebi a mensagem" });
104
- const { body } = await raw("/_sim/webhooks");
105
- const deliveries = (body as { deliveries: Array<{ body: unknown }> }).deliveries;
106
- const mine = deliveries.map((d) => d.body).filter((b) => JSON.stringify(b).includes(id));
107
- expect(mine.length).toBeGreaterThan(0);
108
- });
109
- });
110
-
111
- /**
112
- * The gaps. Each one is the exact payload that failed, so the assertion doubles
113
- * as the report. They are written against the PINNED sha: a green run here
114
- * means the gap is still open, and a red one means it was closed and our own
115
- * cover for it can be reconsidered.
116
- */
117
- describe.skipIf(!up)("what the emulator does not do, measured", () => {
118
- it("GAP: accepts a free-form message outside the 24h window that Meta refuses with 131047 — and it already knows", async () => {
119
- const id = pnid();
120
- const driver = new EmulatorDriver(URL_BASE);
121
- await driver.pin(new Date("2026-09-23T17:00:00.000Z"));
122
- await driver.inbound({ phoneNumberId: id, from: `+${TO}`, text: "oi" });
123
- expect((await send(id, { kind: "text", text: "dentro da janela" })).status).toBe(200);
124
-
125
- await driver.advanceHours(26);
126
- const outside = await send(id, { kind: "text", text: "Recebemos, acordo quitado." });
127
- // The real Cloud API answers 400 with error 131047 and sends nothing.
128
- expect(outside.status).toBe(200);
129
-
130
- // And this is what makes it a one-line fix rather than a feature: the
131
- // pricing engine has ALREADY decided the message is invalid there.
132
- const state = (await raw(`/_sim/state?key=${id}:${TO}`)).body as {
133
- messages: Array<{ id: string }>;
134
- priced: { byMessageId: Record<string, { reasonCode: string }> };
135
- };
136
- const last = state.messages[state.messages.length - 1]!;
137
- expect(state.priced.byMessageId[last.id]!.reasonCode).toBe("INVALID_NON_TEMPLATE_OUTSIDE_CSW");
138
- });
139
-
140
- it("GAP: has no way to redeliver or reorder a webhook, so duplicate and out-of-order delivery cannot be driven", async () => {
141
- for (const path of ["/_sim/replay", "/_sim/webhooks/replay", "/_sim/redeliver"]) {
142
- const { status } = await raw(path, { method: "POST", headers: { "content-type": "application/json" }, body: "{}" });
143
- expect(status, path).toBe(404);
144
- }
145
- // `GET /_sim/webhooks` is a read-only log; nothing re-dispatches from it.
146
- const { status, body } = await raw("/_sim/webhooks");
147
- expect(status).toBe(200);
148
- expect(Array.isArray((body as { deliveries: unknown[] }).deliveries)).toBe(true);
149
- });
150
-
151
- it("GAP: an inbound interactive reply degrades to an empty text message, so a debtor cannot tap a button", async () => {
152
- const id = pnid();
153
- const { status, body } = await raw("/_sim/inbound", {
154
- method: "POST",
155
- headers: { "content-type": "application/json" },
156
- body: JSON.stringify({
157
- phone_number_id: id,
158
- from: `+${TO}`,
159
- type: "interactive",
160
- interactive: { type: "button_reply", button_reply: { id: "avista", title: "À vista" } },
161
- }),
162
- });
163
- expect(status).toBe(200);
164
- const message = (body as { message: { contentType: string; bodyPreview: string } }).message;
165
- // The interactive payload is dropped: outbound buttons work, the reply to one does not exist.
166
- expect(message.contentType).toBe("text");
167
- expect(message.bodyPreview).toBe("");
168
- });
169
-
170
- it("GAP: only `sent` and `delivered` are ever emitted, so a channel cannot observe a read receipt or a failure", async () => {
171
- const id = pnid();
172
- await send(id, { kind: "text", text: "uma mensagem" });
173
- const { body } = await raw("/_sim/webhooks");
174
- const statuses = (body as { deliveries: Array<{ body: unknown }> }).deliveries
175
- .map((d) => d.body as { entry?: Array<{ id?: string; changes?: Array<{ value?: { statuses?: Array<{ status: string }> } }> }> })
176
- .filter((b) => b.entry?.[0]?.id === id)
177
- .flatMap((b) => b.entry?.[0]?.changes?.[0]?.value?.statuses ?? [])
178
- .map((s) => s.status);
179
- expect(statuses).toEqual(["sent", "delivered"]);
180
- });
181
-
182
- it("GAP: a `+` on one side and none on the other splits one person into two conversations", async () => {
183
- const id = pnid();
184
- const driver = new EmulatorDriver(URL_BASE);
185
- // Our driver strips the `+` precisely to avoid this; done by hand here, it splits.
186
- await raw("/_sim/inbound", {
187
- method: "POST",
188
- headers: { "content-type": "application/json" },
189
- body: JSON.stringify({ phone_number_id: id, from: `+${TO}`, text: "oi" }),
190
- });
191
- await send(id, { kind: "text", text: "resposta" });
192
- const miss = await raw(`/_sim/state?key=${id}:not-a-conversation`);
193
- const keys = (miss.body as { keys: string[] }).keys;
194
- expect(keys).toContain(`${id}:+${TO}`);
195
- expect(keys).toContain(`${id}:${TO}`);
196
-
197
- // With our driver, which strips it, there is one conversation and the window opens.
198
- const same = pnid();
199
- await driver.inbound({ phoneNumberId: same, from: `+${TO}`, text: "oi" });
200
- await send(same, { kind: "text", text: "resposta" });
201
- const keysAfter = ((await raw(`/_sim/state?key=${same}:not-a-conversation`)).body as { keys: string[] }).keys;
202
- expect(keysAfter.filter((k) => k.startsWith(`${same}:`))).toEqual([`${same}:${TO}`]);
203
- });
204
- });