@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.
- package/README.md +1 -0
- package/dist/commands/mandate-verify.js +98 -19
- package/dist/commands/mandate-verify.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mandate-codec.js +31 -20
- package/dist/mandate-codec.js.map +1 -1
- package/package.json +1 -1
- package/templates/bills-agent/README.md +1 -1
- package/templates/bills-agent/agents/bills-agent/AGENTS.md +1 -1
- package/templates/bills-agent/agents/bills-agent/CLAUDE.md +1 -1
- package/templates/bills-agent/agents/bills-agent/README.md +6 -5
- package/templates/bills-agent/agents/bills-agent/src/modules/embedded-consent.ts +5 -4
- package/templates/bills-agent/packages/agent-core/README.md +14 -1
- package/templates/bills-agent/packages/agent-core/package.json +1 -1
- package/templates/bills-agent/packages/agent-core/src/agent.ts +39 -1
- package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
- package/templates/bills-agent/packages/agent-core/src/api/client.ts +28 -5
- package/templates/bills-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
- package/templates/bills-agent/packages/agent-core/src/api/rail.ts +62 -26
- package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
- package/templates/bills-agent/packages/agent-core/src/approval.ts +14 -2
- package/templates/bills-agent/packages/agent-core/src/bundle.ts +35 -3
- package/templates/bills-agent/packages/agent-core/src/channels.ts +49 -3
- package/templates/bills-agent/packages/agent-core/src/check.ts +25 -2
- package/templates/bills-agent/packages/agent-core/src/engine.ts +235 -46
- package/templates/bills-agent/packages/agent-core/src/hash.ts +57 -1
- package/templates/bills-agent/packages/agent-core/src/index.ts +2 -0
- package/templates/bills-agent/packages/agent-core/src/quote.ts +50 -0
- package/templates/bills-agent/packages/agent-core/src/rail.ts +89 -4
- package/templates/bills-agent/packages/agent-core/src/receipt-chain.ts +270 -0
- package/templates/bills-agent/packages/agent-core/src/receipt-verification.ts +194 -18
- package/templates/bills-agent/packages/agent-core/src/revocation.ts +1 -1
- package/templates/bills-agent/packages/agent-core/src/state-machine.ts +10 -1
- package/templates/bills-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
- package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +100 -32
- package/templates/bills-agent/packages/agent-core/src/tools.ts +17 -6
- package/templates/bills-agent/packages/agent-core/src/types.ts +54 -1
- package/templates/bills-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
- package/templates/bills-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
- package/templates/bills-agent/packages/agent-core/test/composition.test.ts +204 -0
- package/templates/bills-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
- package/templates/bills-agent/packages/agent-core/test/helpers.ts +4 -1
- package/templates/bills-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
- package/templates/bills-agent/packages/agent-core/test/quote.test.ts +198 -0
- package/templates/bills-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
- package/templates/bills-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
- package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +103 -3
- package/templates/bills-agent/packages/agent-runtime/README.md +12 -0
- package/templates/bills-agent/packages/agent-runtime/src/adversarial.ts +32 -2
- package/templates/bills-agent/packages/agent-runtime/src/channels/types.ts +53 -4
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
- package/templates/bills-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
- package/templates/bills-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
- package/templates/bills-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
- package/templates/bills-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
- package/templates/bills-agent/packages/agent-runtime/src/commands/start.ts +6 -3
- package/templates/bills-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
- package/templates/bills-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
- package/templates/bills-agent/packages/agent-runtime/src/index.ts +1 -0
- package/templates/bills-agent/packages/agent-runtime/src/inspect.ts +5 -1
- package/templates/bills-agent/packages/agent-runtime/src/kit.ts +25 -2
- package/templates/bills-agent/packages/agent-runtime/src/scenarios.ts +8 -2
- package/templates/bills-agent/packages/agent-runtime/src/setup.ts +13 -3
- package/templates/bills-agent/packages/agent-runtime/src/terminal.ts +127 -18
- package/templates/bills-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
- package/templates/bills-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
- package/templates/bills-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
- package/templates/bills-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
- package/templates/checkout-agent/LICENSE +21 -0
- package/templates/checkout-agent/README.md +28 -0
- package/templates/checkout-agent/_gitignore +10 -0
- package/templates/checkout-agent/agents/checkout-agent/.env.example +5 -0
- package/templates/checkout-agent/agents/checkout-agent/AGENTS.md +20 -0
- package/templates/checkout-agent/agents/checkout-agent/CLAUDE.md +20 -0
- package/templates/checkout-agent/agents/checkout-agent/README.md +94 -0
- package/templates/checkout-agent/agents/checkout-agent/SYSTEM_PROMPT.md +36 -0
- package/templates/checkout-agent/agents/checkout-agent/agent.yaml +30 -0
- package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/README.md +28 -0
- package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-beatriz.json +11 -0
- package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina-retomada.json +10 -0
- package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina.json +11 -0
- package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/templates.json +49 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap-human.json +12 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.json +11 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +5 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.json +13 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.transcript.jsonl +4 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.json +26 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.json +11 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.transcript.jsonl +5 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.json +12 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.transcript.jsonl +5 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.json +13 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.transcript.jsonl +5 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.json +15 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.transcript.jsonl +7 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.json +12 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.transcript.jsonl +5 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.json +13 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.transcript.jsonl +4 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.json +16 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.transcript.jsonl +9 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.json +15 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.transcript.jsonl +8 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/webhook-replay.json +11 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/eval.yaml +11 -0
- package/templates/checkout-agent/agents/checkout-agent/guardrails.json +25 -0
- package/templates/checkout-agent/agents/checkout-agent/mandate.example.json +28 -0
- package/templates/checkout-agent/agents/checkout-agent/package.json +33 -0
- package/templates/checkout-agent/agents/checkout-agent/runbook.md +19 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.json +42 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +4 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.json +32 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.transcript.jsonl +4 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.json +61 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.transcript.jsonl +9 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.json +61 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.transcript.jsonl +9 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.json +36 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.transcript.jsonl +6 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.json +23 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.transcript.jsonl +4 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.json +72 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.transcript.jsonl +9 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.json +15 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.transcript.jsonl +6 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.json +23 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.transcript.jsonl +3 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.json +42 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.transcript.jsonl +12 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.json +14 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.transcript.jsonl +9 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.json +34 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.transcript.jsonl +7 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.json +33 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.transcript.jsonl +4 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.json +32 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.transcript.jsonl +4 -0
- package/templates/checkout-agent/agents/checkout-agent/src/catalog.ts +92 -0
- package/templates/checkout-agent/agents/checkout-agent/src/kit.ts +338 -0
- package/templates/checkout-agent/agents/checkout-agent/src/modules/bolepix-receivables.ts +179 -0
- package/templates/checkout-agent/agents/checkout-agent/src/modules/nfse-invoice.ts +374 -0
- package/templates/checkout-agent/agents/checkout-agent/src/modules/storefront-cart.ts +220 -0
- package/templates/checkout-agent/agents/checkout-agent/src/pricing.ts +297 -0
- package/templates/checkout-agent/agents/checkout-agent/test/adversarial.test.ts +117 -0
- package/templates/checkout-agent/agents/checkout-agent/test/check.test.ts +67 -0
- package/templates/checkout-agent/agents/checkout-agent/test/cli.test.ts +169 -0
- package/templates/checkout-agent/agents/checkout-agent/test/fixtures/order-marina.transcript.jsonl +5 -0
- package/templates/checkout-agent/agents/checkout-agent/test/fixtures/ordered-marina-awaiting-payer.transcript.jsonl +6 -0
- package/templates/checkout-agent/agents/checkout-agent/test/fixtures/tap-falar-agora-marina.transcript.jsonl +2 -0
- package/templates/checkout-agent/agents/checkout-agent/test/nfse.test.ts +170 -0
- package/templates/checkout-agent/agents/checkout-agent/test/pricing.test.ts +138 -0
- package/templates/checkout-agent/agents/checkout-agent/test/scenarios.test.ts +113 -0
- package/templates/checkout-agent/agents/checkout-agent/test/whatsapp.test.ts +171 -0
- package/templates/checkout-agent/agents/checkout-agent/tools.json +68 -0
- package/templates/checkout-agent/agents/checkout-agent/tsconfig.json +10 -0
- package/templates/checkout-agent/package.json +38 -0
- package/templates/checkout-agent/packages/agent-core/README.md +34 -0
- package/templates/checkout-agent/packages/agent-core/package.json +24 -0
- package/templates/checkout-agent/packages/agent-core/src/agent.ts +163 -0
- package/templates/checkout-agent/packages/agent-core/src/api/charge-rail.ts +195 -0
- package/templates/checkout-agent/packages/agent-core/src/api/client.ts +76 -0
- package/templates/checkout-agent/packages/agent-core/src/api/mandate-status.ts +74 -0
- package/templates/checkout-agent/packages/agent-core/src/api/rail.ts +153 -0
- package/templates/checkout-agent/packages/agent-core/src/api/sandbox-payer.ts +43 -0
- package/templates/checkout-agent/packages/agent-core/src/approval.ts +135 -0
- package/templates/checkout-agent/packages/agent-core/src/bundle.ts +191 -0
- package/templates/checkout-agent/packages/agent-core/src/channels.ts +186 -0
- package/templates/checkout-agent/packages/agent-core/src/check.ts +276 -0
- package/templates/checkout-agent/packages/agent-core/src/clock.ts +42 -0
- package/templates/checkout-agent/packages/agent-core/src/engine.ts +1074 -0
- package/templates/checkout-agent/packages/agent-core/src/escalate.ts +85 -0
- package/templates/checkout-agent/packages/agent-core/src/events.ts +40 -0
- package/templates/checkout-agent/packages/agent-core/src/guardrails.ts +45 -0
- package/templates/checkout-agent/packages/agent-core/src/hash.ts +118 -0
- package/templates/checkout-agent/packages/agent-core/src/ids.ts +10 -0
- package/templates/checkout-agent/packages/agent-core/src/index.ts +34 -0
- package/templates/checkout-agent/packages/agent-core/src/mandate.ts +100 -0
- package/templates/checkout-agent/packages/agent-core/src/manifest.ts +76 -0
- package/templates/checkout-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
- package/templates/checkout-agent/packages/agent-core/src/providers/replay.ts +63 -0
- package/templates/checkout-agent/packages/agent-core/src/providers/types.ts +39 -0
- package/templates/checkout-agent/packages/agent-core/src/quote.ts +50 -0
- package/templates/checkout-agent/packages/agent-core/src/rail.ts +186 -0
- package/templates/checkout-agent/packages/agent-core/src/receipt-chain.ts +270 -0
- package/templates/checkout-agent/packages/agent-core/src/receipt-verification.ts +577 -0
- package/templates/checkout-agent/packages/agent-core/src/revocation.ts +30 -0
- package/templates/checkout-agent/packages/agent-core/src/secrets.ts +30 -0
- package/templates/checkout-agent/packages/agent-core/src/state/store.ts +307 -0
- package/templates/checkout-agent/packages/agent-core/src/state-machine.ts +157 -0
- package/templates/checkout-agent/packages/agent-core/src/stubs/charge-rail.ts +195 -0
- package/templates/checkout-agent/packages/agent-core/src/stubs/mandate-status.ts +58 -0
- package/templates/checkout-agent/packages/agent-core/src/stubs/rail.ts +209 -0
- package/templates/checkout-agent/packages/agent-core/src/tools.ts +66 -0
- package/templates/checkout-agent/packages/agent-core/src/types.ts +187 -0
- package/templates/checkout-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
- package/templates/checkout-agent/packages/agent-core/test/approval.test.ts +117 -0
- package/templates/checkout-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
- package/templates/checkout-agent/packages/agent-core/test/bundle.test.ts +85 -0
- package/templates/checkout-agent/packages/agent-core/test/clock.test.ts +25 -0
- package/templates/checkout-agent/packages/agent-core/test/composition.test.ts +204 -0
- package/templates/checkout-agent/packages/agent-core/test/engine.test.ts +596 -0
- package/templates/checkout-agent/packages/agent-core/test/escalate.test.ts +42 -0
- package/templates/checkout-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
- package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt-keys.json +18 -0
- package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt.json +24 -0
- package/templates/checkout-agent/packages/agent-core/test/helpers.ts +142 -0
- package/templates/checkout-agent/packages/agent-core/test/mandate-status.test.ts +229 -0
- package/templates/checkout-agent/packages/agent-core/test/manifest.test.ts +45 -0
- package/templates/checkout-agent/packages/agent-core/test/quote.test.ts +198 -0
- package/templates/checkout-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
- package/templates/checkout-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
- package/templates/checkout-agent/packages/agent-core/test/receipt-verification.test.ts +316 -0
- package/templates/checkout-agent/packages/agent-core/test/receivables.test.ts +416 -0
- package/templates/checkout-agent/packages/agent-core/test/secrets.test.ts +28 -0
- package/templates/checkout-agent/packages/agent-core/test/state-machine.test.ts +67 -0
- package/templates/checkout-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
- package/templates/checkout-agent/packages/agent-core/tsconfig.json +10 -0
- package/templates/checkout-agent/packages/agent-runtime/README.md +80 -0
- package/templates/checkout-agent/packages/agent-runtime/bin.mjs +32 -0
- package/templates/checkout-agent/packages/agent-runtime/package.json +23 -0
- package/templates/checkout-agent/packages/agent-runtime/src/adversarial.ts +199 -0
- package/templates/checkout-agent/packages/agent-runtime/src/agent.ts +68 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/contact.ts +13 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/index.ts +72 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/rules.ts +120 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/types.ts +173 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +459 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +216 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/evidence.ts +109 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +350 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +180 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/present.ts +47 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +120 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +127 -0
- package/templates/checkout-agent/packages/agent-runtime/src/cli.ts +98 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/check.ts +15 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/decide.ts +63 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/eval.ts +58 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/inspect.ts +100 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +367 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/poll.ts +145 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/reconcile.ts +42 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/rerun.ts +86 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/resume.ts +49 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +134 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/start.ts +248 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/verify.ts +330 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/webhook.ts +40 -0
- package/templates/checkout-agent/packages/agent-runtime/src/default-kit.ts +83 -0
- package/templates/checkout-agent/packages/agent-runtime/src/index.ts +13 -0
- package/templates/checkout-agent/packages/agent-runtime/src/inspect.ts +801 -0
- package/templates/checkout-agent/packages/agent-runtime/src/kit.ts +229 -0
- package/templates/checkout-agent/packages/agent-runtime/src/poll.ts +105 -0
- package/templates/checkout-agent/packages/agent-runtime/src/scenarios.ts +274 -0
- package/templates/checkout-agent/packages/agent-runtime/src/setup.ts +239 -0
- package/templates/checkout-agent/packages/agent-runtime/src/terminal.ts +263 -0
- package/templates/checkout-agent/packages/agent-runtime/src/webhook.ts +131 -0
- package/templates/checkout-agent/packages/agent-runtime/test/inspect.test.ts +336 -0
- package/templates/checkout-agent/packages/agent-runtime/test/verify-cli.test.ts +329 -0
- package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +277 -0
- package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-evidence.test.ts +94 -0
- package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-rules.test.ts +174 -0
- package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-session.test.ts +167 -0
- package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
- package/templates/checkout-agent/packages/agent-runtime/tsconfig.json +9 -0
- package/templates/checkout-agent/scripts/whatsapp-emulator.mjs +84 -0
- package/templates/checkout-agent/tsconfig.base.json +16 -0
- package/templates/checkout-agent/vitest.config.ts +12 -0
- package/templates/collections-agent/README.md +6 -1
- package/templates/collections-agent/agents/collections-agent/README.md +12 -8
- package/templates/collections-agent/agents/collections-agent/channels/whatsapp/README.md +8 -0
- package/templates/collections-agent/agents/collections-agent/channels/whatsapp/acordo-1042-retomada.json +10 -0
- package/templates/collections-agent/agents/collections-agent/channels/whatsapp/templates.json +27 -1
- package/templates/collections-agent/agents/collections-agent/src/kit.ts +2 -0
- package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts +12 -2
- package/templates/collections-agent/agents/collections-agent/test/ambiguous-charge.test.ts +55 -0
- package/templates/collections-agent/agents/collections-agent/test/fixtures/tap-emitir-nova-1042.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/test/whatsapp-poll.test.ts +12 -6
- package/templates/collections-agent/agents/collections-agent/test/whatsapp-status-replies.test.ts +148 -0
- package/templates/collections-agent/agents/collections-agent/test/whatsapp.test.ts +5 -0
- package/templates/collections-agent/package.json +1 -0
- package/templates/collections-agent/packages/agent-core/README.md +14 -1
- package/templates/collections-agent/packages/agent-core/package.json +1 -1
- package/templates/collections-agent/packages/agent-core/src/agent.ts +39 -1
- package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
- package/templates/collections-agent/packages/agent-core/src/api/client.ts +28 -5
- package/templates/collections-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
- package/templates/collections-agent/packages/agent-core/src/api/rail.ts +62 -26
- package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
- package/templates/collections-agent/packages/agent-core/src/approval.ts +14 -2
- package/templates/collections-agent/packages/agent-core/src/bundle.ts +35 -3
- package/templates/collections-agent/packages/agent-core/src/channels.ts +49 -3
- package/templates/collections-agent/packages/agent-core/src/check.ts +25 -2
- package/templates/collections-agent/packages/agent-core/src/engine.ts +235 -46
- package/templates/collections-agent/packages/agent-core/src/hash.ts +57 -1
- package/templates/collections-agent/packages/agent-core/src/index.ts +2 -0
- package/templates/collections-agent/packages/agent-core/src/quote.ts +50 -0
- package/templates/collections-agent/packages/agent-core/src/rail.ts +89 -4
- package/templates/collections-agent/packages/agent-core/src/receipt-chain.ts +270 -0
- package/templates/collections-agent/packages/agent-core/src/receipt-verification.ts +194 -18
- package/templates/collections-agent/packages/agent-core/src/revocation.ts +1 -1
- package/templates/collections-agent/packages/agent-core/src/state-machine.ts +10 -1
- package/templates/collections-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
- package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +100 -32
- package/templates/collections-agent/packages/agent-core/src/tools.ts +17 -6
- package/templates/collections-agent/packages/agent-core/src/types.ts +54 -1
- package/templates/collections-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
- package/templates/collections-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
- package/templates/collections-agent/packages/agent-core/test/composition.test.ts +204 -0
- package/templates/collections-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
- package/templates/collections-agent/packages/agent-core/test/helpers.ts +4 -1
- package/templates/collections-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
- package/templates/collections-agent/packages/agent-core/test/quote.test.ts +198 -0
- package/templates/collections-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
- package/templates/collections-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
- package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +103 -3
- package/templates/collections-agent/packages/agent-runtime/README.md +12 -0
- package/templates/collections-agent/packages/agent-runtime/src/adversarial.ts +32 -2
- package/templates/collections-agent/packages/agent-runtime/src/channels/types.ts +53 -4
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
- package/templates/collections-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
- package/templates/collections-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
- package/templates/collections-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
- package/templates/collections-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
- package/templates/collections-agent/packages/agent-runtime/src/commands/start.ts +6 -3
- package/templates/collections-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
- package/templates/collections-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
- package/templates/collections-agent/packages/agent-runtime/src/index.ts +1 -0
- package/templates/collections-agent/packages/agent-runtime/src/inspect.ts +5 -1
- package/templates/collections-agent/packages/agent-runtime/src/kit.ts +25 -2
- package/templates/collections-agent/packages/agent-runtime/src/scenarios.ts +8 -2
- package/templates/collections-agent/packages/agent-runtime/src/setup.ts +13 -3
- package/templates/collections-agent/packages/agent-runtime/src/terminal.ts +127 -18
- package/templates/collections-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
- package/templates/collections-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
- package/templates/collections-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
- package/templates/collections-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
- package/templates/collections-agent/scripts/whatsapp-emulator.mjs +84 -0
- package/templates/hello-agent/README.md +1 -1
- package/templates/hello-agent/packages/agent-core/README.md +14 -1
- package/templates/hello-agent/packages/agent-core/package.json +1 -1
- package/templates/hello-agent/packages/agent-core/src/agent.ts +39 -1
- package/templates/hello-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
- package/templates/hello-agent/packages/agent-core/src/api/client.ts +28 -5
- package/templates/hello-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
- package/templates/hello-agent/packages/agent-core/src/api/rail.ts +62 -26
- package/templates/hello-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
- package/templates/hello-agent/packages/agent-core/src/approval.ts +14 -2
- package/templates/hello-agent/packages/agent-core/src/bundle.ts +35 -3
- package/templates/hello-agent/packages/agent-core/src/channels.ts +49 -3
- package/templates/hello-agent/packages/agent-core/src/check.ts +25 -2
- package/templates/hello-agent/packages/agent-core/src/engine.ts +235 -46
- package/templates/hello-agent/packages/agent-core/src/hash.ts +57 -1
- package/templates/hello-agent/packages/agent-core/src/index.ts +2 -0
- package/templates/hello-agent/packages/agent-core/src/quote.ts +50 -0
- package/templates/hello-agent/packages/agent-core/src/rail.ts +89 -4
- package/templates/hello-agent/packages/agent-core/src/receipt-chain.ts +270 -0
- package/templates/hello-agent/packages/agent-core/src/receipt-verification.ts +194 -18
- package/templates/hello-agent/packages/agent-core/src/revocation.ts +1 -1
- package/templates/hello-agent/packages/agent-core/src/state-machine.ts +10 -1
- package/templates/hello-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
- package/templates/hello-agent/packages/agent-core/src/stubs/rail.ts +100 -32
- package/templates/hello-agent/packages/agent-core/src/tools.ts +17 -6
- package/templates/hello-agent/packages/agent-core/src/types.ts +54 -1
- package/templates/hello-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
- package/templates/hello-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
- package/templates/hello-agent/packages/agent-core/test/composition.test.ts +204 -0
- package/templates/hello-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
- package/templates/hello-agent/packages/agent-core/test/helpers.ts +4 -1
- package/templates/hello-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
- package/templates/hello-agent/packages/agent-core/test/quote.test.ts +198 -0
- package/templates/hello-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
- package/templates/hello-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
- package/templates/hello-agent/packages/agent-core/test/receivables.test.ts +103 -3
- package/templates/hello-agent/packages/agent-runtime/README.md +12 -0
- package/templates/hello-agent/packages/agent-runtime/src/adversarial.ts +32 -2
- package/templates/hello-agent/packages/agent-runtime/src/channels/types.ts +53 -4
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
- package/templates/hello-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
- package/templates/hello-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
- package/templates/hello-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
- package/templates/hello-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
- package/templates/hello-agent/packages/agent-runtime/src/commands/start.ts +6 -3
- package/templates/hello-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
- package/templates/hello-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
- package/templates/hello-agent/packages/agent-runtime/src/index.ts +1 -0
- package/templates/hello-agent/packages/agent-runtime/src/inspect.ts +5 -1
- package/templates/hello-agent/packages/agent-runtime/src/kit.ts +25 -2
- package/templates/hello-agent/packages/agent-runtime/src/scenarios.ts +8 -2
- package/templates/hello-agent/packages/agent-runtime/src/setup.ts +13 -3
- package/templates/hello-agent/packages/agent-runtime/src/terminal.ts +127 -18
- package/templates/hello-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
- package/templates/hello-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
- package/templates/hello-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
- package/templates/hello-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
- package/templates/kits.lock.json +29 -6
- package/templates/supplier-payments-agent/README.md +1 -1
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/AGENTS.md +1 -1
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/CLAUDE.md +1 -1
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/README.md +11 -9
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/kit.ts +1 -1
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/modules/batch-payout.ts +67 -20
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/test/batch.test.ts +504 -5
- package/templates/supplier-payments-agent/packages/agent-core/README.md +14 -1
- package/templates/supplier-payments-agent/packages/agent-core/package.json +1 -1
- package/templates/supplier-payments-agent/packages/agent-core/src/agent.ts +39 -1
- package/templates/supplier-payments-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
- package/templates/supplier-payments-agent/packages/agent-core/src/api/client.ts +28 -5
- package/templates/supplier-payments-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
- package/templates/supplier-payments-agent/packages/agent-core/src/api/rail.ts +62 -26
- package/templates/supplier-payments-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
- package/templates/supplier-payments-agent/packages/agent-core/src/approval.ts +14 -2
- package/templates/supplier-payments-agent/packages/agent-core/src/bundle.ts +35 -3
- package/templates/supplier-payments-agent/packages/agent-core/src/channels.ts +49 -3
- package/templates/supplier-payments-agent/packages/agent-core/src/check.ts +25 -2
- package/templates/supplier-payments-agent/packages/agent-core/src/engine.ts +235 -46
- package/templates/supplier-payments-agent/packages/agent-core/src/hash.ts +57 -1
- package/templates/supplier-payments-agent/packages/agent-core/src/index.ts +2 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/quote.ts +50 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/rail.ts +89 -4
- package/templates/supplier-payments-agent/packages/agent-core/src/receipt-chain.ts +270 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/receipt-verification.ts +194 -18
- package/templates/supplier-payments-agent/packages/agent-core/src/revocation.ts +1 -1
- package/templates/supplier-payments-agent/packages/agent-core/src/state-machine.ts +10 -1
- package/templates/supplier-payments-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
- package/templates/supplier-payments-agent/packages/agent-core/src/stubs/rail.ts +100 -32
- package/templates/supplier-payments-agent/packages/agent-core/src/tools.ts +17 -6
- package/templates/supplier-payments-agent/packages/agent-core/src/types.ts +54 -1
- package/templates/supplier-payments-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/composition.test.ts +204 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/helpers.ts +4 -1
- package/templates/supplier-payments-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
- package/templates/supplier-payments-agent/packages/agent-core/test/quote.test.ts +198 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/receivables.test.ts +103 -3
- package/templates/supplier-payments-agent/packages/agent-runtime/README.md +12 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/adversarial.ts +32 -2
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/types.ts +53 -4
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start.ts +6 -3
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
- package/templates/supplier-payments-agent/packages/agent-runtime/src/index.ts +1 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/inspect.ts +5 -1
- package/templates/supplier-payments-agent/packages/agent-runtime/src/kit.ts +25 -2
- package/templates/supplier-payments-agent/packages/agent-runtime/src/scenarios.ts +8 -2
- package/templates/supplier-payments-agent/packages/agent-runtime/src/setup.ts +13 -3
- package/templates/supplier-payments-agent/packages/agent-runtime/src/terminal.ts +127 -18
- package/templates/supplier-payments-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
- package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
- package/templates/bills-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
- package/templates/collections-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
- package/templates/hello-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
- package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision 19a: `attestation.evidence` on the consent submit, built from what
|
|
3
|
+
* a conversation channel can honestly say it observed (ent#1615).
|
|
4
|
+
*
|
|
5
|
+
* The first test is the important one and it asserts a REFUSAL. A simulated
|
|
6
|
+
* conversation was observed by nobody, and signing a counter as a provider
|
|
7
|
+
* message id would put a made-up fact inside a record whose whole value is
|
|
8
|
+
* that it is observed. The seam is wired; the simulator declines to use it.
|
|
9
|
+
*
|
|
10
|
+
* The rest hold the shape to the API's own rule, so a change here fails in
|
|
11
|
+
* the repo instead of failing as a 400 at the consent submit.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, expect, it } from "vitest";
|
|
14
|
+
import { evidenceFor, WHATSAPP_EVIDENCE_KEYS } from "../src/channels/whatsapp/evidence.js";
|
|
15
|
+
import type { InboundMessage } from "../src/channels/types.js";
|
|
16
|
+
|
|
17
|
+
const AT = (iso: string) => Math.floor(Date.parse(iso) / 1000);
|
|
18
|
+
const NOW = () => new Date("2026-09-23T17:00:00Z");
|
|
19
|
+
const REAL: InboundMessage = {
|
|
20
|
+
id: "wamid.HBgNNTUxMTk4NzY1NDMyMRUCABIYEjND",
|
|
21
|
+
from: "+5511987654321",
|
|
22
|
+
text: "sim, autorizo",
|
|
23
|
+
timestamp: AT("2026-09-23T17:00:00Z"),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe("what an emulated conversation may attest", () => {
|
|
27
|
+
it("refuses, and says why in a sentence somebody can act on", () => {
|
|
28
|
+
const result = evidenceFor({ live: false, message: { ...REAL, id: "wamid.sim.9f1c" }, now: NOW });
|
|
29
|
+
expect(result.attestable).toBe(false);
|
|
30
|
+
if (result.attestable) return;
|
|
31
|
+
expect(result.reason).toContain("emulator");
|
|
32
|
+
expect(result.reason).toContain("no phone was involved");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("refuses even when the emulated message wears a provider-shaped id, because the id is not what decides", () => {
|
|
36
|
+
expect(evidenceFor({ live: false, message: REAL, now: NOW }).attestable).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("what a live conversation attests", () => {
|
|
41
|
+
it("builds the submit's attestation with the four keys the whatsapp channel may carry", () => {
|
|
42
|
+
const result = evidenceFor({ live: true, message: REAL, sessionId: "conv_1042", reference: "acordo-1042", now: NOW });
|
|
43
|
+
expect(result.attestable).toBe(true);
|
|
44
|
+
if (!result.attestable) return;
|
|
45
|
+
expect(result.attestation).toEqual({
|
|
46
|
+
method: "partner_session",
|
|
47
|
+
asserted_at: AT("2026-09-23T17:00:00Z"),
|
|
48
|
+
reference: "acordo-1042",
|
|
49
|
+
evidence: {
|
|
50
|
+
channel: "whatsapp",
|
|
51
|
+
contact: "+5511987654321",
|
|
52
|
+
message_id: "wamid.HBgNNTUxMTk4NzY1NDMyMRUCABIYEjND",
|
|
53
|
+
session_id: "conv_1042",
|
|
54
|
+
provider_ts: AT("2026-09-23T17:00:00Z"),
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("carries the contact in the clear, because the API hashes it under a key a partner cannot compute", () => {
|
|
60
|
+
const result = evidenceFor({ live: true, message: REAL, now: NOW });
|
|
61
|
+
if (!result.attestable) throw new Error("expected evidence");
|
|
62
|
+
expect(result.attestation.evidence.contact).toBe("+5511987654321");
|
|
63
|
+
expect(result.attestation.evidence).not.toHaveProperty("contact_hash");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("never carries an ip, a user agent, a device id or a geo: on this channel each of those was inferred", () => {
|
|
67
|
+
const result = evidenceFor({ live: true, message: REAL, now: NOW });
|
|
68
|
+
if (!result.attestable) throw new Error("expected evidence");
|
|
69
|
+
const keys = Object.keys(result.attestation.evidence).filter((k) => k !== "channel");
|
|
70
|
+
expect(keys.every((k) => (WHATSAPP_EVIDENCE_KEYS as readonly string[]).includes(k))).toBe(true);
|
|
71
|
+
for (const forbidden of ["ip", "user_agent", "device_id_hash", "geo"]) expect(result.attestation.evidence).not.toHaveProperty(forbidden);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("separates the partner's clock from the provider's: asserted_at is ours, provider_ts is theirs", () => {
|
|
75
|
+
const result = evidenceFor({ live: true, message: { ...REAL, timestamp: AT("2026-09-23T15:00:00Z") }, now: () => new Date("2026-09-23T17:00:05Z") });
|
|
76
|
+
if (!result.attestable) throw new Error("expected evidence");
|
|
77
|
+
expect(result.attestation.asserted_at).toBe(AT("2026-09-23T17:00:05Z"));
|
|
78
|
+
expect(result.attestation.evidence.provider_ts).toBe(AT("2026-09-23T15:00:00Z"));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("refuses a reference with a dot: the canonical signing string joins segments, and a wamid goes in message_id", () => {
|
|
82
|
+
const result = evidenceFor({ live: true, message: REAL, reference: "wamid.HBgN", now: NOW });
|
|
83
|
+
expect(result.attestable).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("refuses a message id or a session id the API's opaque-id shape would reject", () => {
|
|
87
|
+
expect(evidenceFor({ live: true, message: { ...REAL, id: "wamid with spaces" }, now: NOW }).attestable).toBe(false);
|
|
88
|
+
expect(evidenceFor({ live: true, message: REAL, sessionId: "conv 1042", now: NOW }).attestable).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("refuses a message the provider gave no timestamp for", () => {
|
|
92
|
+
expect(evidenceFor({ live: true, message: { ...REAL, timestamp: 0 }, now: NOW }).attestable).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rules of collecting over a conversation, tried and refused.
|
|
3
|
+
*
|
|
4
|
+
* Every case here is an ATTEMPT: the message is handed to the channel the way
|
|
5
|
+
* the runner hands it one, and what is asserted is that nothing left the
|
|
6
|
+
* process. The backend under the channel is a recorder, so "refused" is not a
|
|
7
|
+
* claim about a return value — it is the recorder having seen nothing.
|
|
8
|
+
*
|
|
9
|
+
* The last case is the one that matters most: the same attempts through a
|
|
10
|
+
* DIFFERENT backend. The rules live above the backend, so choosing one cannot
|
|
11
|
+
* be a way around them.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, expect, it } from "vitest";
|
|
14
|
+
import { WhatsAppChannel } from "../src/channels/whatsapp/index.js";
|
|
15
|
+
import { checkOutbound } from "../src/channels/rules.js";
|
|
16
|
+
import type { ChannelBackend, InboundMessage, OutboundBody, SentMessage } from "../src/channels/types.js";
|
|
17
|
+
|
|
18
|
+
/** A backend that carries nothing and remembers everything it was asked to carry. */
|
|
19
|
+
class Recorder implements ChannelBackend {
|
|
20
|
+
readonly delivered: Array<{ to: string; body: OutboundBody }> = [];
|
|
21
|
+
constructor(
|
|
22
|
+
readonly name = "recorder",
|
|
23
|
+
readonly live = false,
|
|
24
|
+
private readonly inbox: InboundMessage[] = [],
|
|
25
|
+
) {}
|
|
26
|
+
async open(): Promise<void> {}
|
|
27
|
+
async next(): Promise<InboundMessage | undefined> {
|
|
28
|
+
return this.inbox.shift();
|
|
29
|
+
}
|
|
30
|
+
async deliver(to: string, body: OutboundBody): Promise<SentMessage> {
|
|
31
|
+
this.delivered.push({ to, body });
|
|
32
|
+
return { id: `rec_${this.delivered.length}`, state: "sent" };
|
|
33
|
+
}
|
|
34
|
+
async close(): Promise<void> {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const CONTACT = "+5511987654321";
|
|
38
|
+
const INSIDE = new Date("2026-09-23T14:00:00-03:00");
|
|
39
|
+
const OUTSIDE = new Date("2026-09-23T22:30:00-03:00");
|
|
40
|
+
const HOURS = { window: "08:00-20:00", timezone: "America/Sao_Paulo" };
|
|
41
|
+
|
|
42
|
+
function channelAt(now: Date, backend = new Recorder(), openSession = true) {
|
|
43
|
+
const channel = new WhatsAppChannel({
|
|
44
|
+
backend,
|
|
45
|
+
conversation: { contact: CONTACT, subject: "acordo-1042" },
|
|
46
|
+
now: () => now,
|
|
47
|
+
hours: HOURS,
|
|
48
|
+
knownSubjects: ["acordo-1042", "acordo-1077", "acordo-1103"],
|
|
49
|
+
templates: [{ name: "cobranca_lembrete", language: "pt_BR", description: "a reminder", body: "Oi {{1}}, sobre o acordo." }],
|
|
50
|
+
});
|
|
51
|
+
// A session is open because the person wrote; the window is WhatsApp's rule, not ours, and has its own test below.
|
|
52
|
+
if (openSession) {
|
|
53
|
+
(channel as unknown as { session: { observeInbound(t: number): void } }).session.observeInbound(Math.floor(now.getTime() / 1000));
|
|
54
|
+
}
|
|
55
|
+
return { channel, backend };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe("the collection rules, tried through the channel", () => {
|
|
59
|
+
it("sends inside the collection hours", async () => {
|
|
60
|
+
const { channel, backend } = channelAt(INSIDE);
|
|
61
|
+
const sent = await channel.say("Oi, Joana! Sobre o pedido #1042.");
|
|
62
|
+
expect(sent.refused).toBeUndefined();
|
|
63
|
+
expect(backend.delivered).toHaveLength(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("refuses everything outside the collection hours, and the backend never sees it", async () => {
|
|
67
|
+
const { channel, backend } = channelAt(OUTSIDE);
|
|
68
|
+
const sent = await channel.say("Oi, Joana! Sobre o pedido #1042.");
|
|
69
|
+
expect(sent.refused?.rule).toBe("collection_hours");
|
|
70
|
+
expect(sent.state).toBe("failed");
|
|
71
|
+
expect(backend.delivered).toHaveLength(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("refuses a message to any contact but the one the conversation is bound to", () => {
|
|
75
|
+
const refusal = checkOutbound("+5511900000000", { kind: "text", text: "sobre a sua divida" }, {
|
|
76
|
+
conversation: { contact: CONTACT, subject: "acordo-1042" },
|
|
77
|
+
hours: HOURS,
|
|
78
|
+
now: () => INSIDE,
|
|
79
|
+
});
|
|
80
|
+
expect(refusal?.rule).toBe("bound_contact");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("refuses a message that names another debtor's agreement", async () => {
|
|
84
|
+
const { channel, backend } = channelAt(INSIDE);
|
|
85
|
+
const sent = await channel.say("O acordo-1077 do Carlos tambem esta em aberto, voces podem combinar.");
|
|
86
|
+
expect(sent.refused?.rule).toBe("subject_scope");
|
|
87
|
+
expect(backend.delivered).toHaveLength(0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("refuses a CPF or a CNPJ in a message, formatted or bare", async () => {
|
|
91
|
+
for (const text of ["confirma o CPF 111.444.777-35?", "o CPF do titular e 11144477735", "CNPJ 12.345.678/0001-95", "CNPJ 12345678000195"]) {
|
|
92
|
+
const { channel, backend } = channelAt(INSIDE);
|
|
93
|
+
const sent = await channel.say(text);
|
|
94
|
+
expect(sent.refused?.rule, text).toBe("no_document");
|
|
95
|
+
expect(backend.delivered).toHaveLength(0);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("does NOT read a Pix copy-and-paste or a boleto line as a document", async () => {
|
|
100
|
+
const { channel, backend } = channelAt(INSIDE);
|
|
101
|
+
const brcode = "00020126580014br.gov.bcb.pix0136stub-chg_abc5204000053039865802BR5909CODESPAR6009SAO PAULO62070503***6304STUB";
|
|
102
|
+
const bankLine = "78517596684364082005302425802108566187285620000";
|
|
103
|
+
expect((await channel.send({ kind: "instrument", instrument: "pix_copy_paste", value: brcode })).refused).toBeUndefined();
|
|
104
|
+
expect((await channel.send({ kind: "instrument", instrument: "boleto_bank_line", value: bankLine })).refused).toBeUndefined();
|
|
105
|
+
expect(backend.delivered).toHaveLength(2);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("refuses an empty message", async () => {
|
|
109
|
+
const { channel } = channelAt(INSIDE);
|
|
110
|
+
expect((await channel.say(" ")).refused?.rule).toBe("empty_message");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("masks the contact everywhere it is written down", async () => {
|
|
114
|
+
const { channel } = channelAt(INSIDE);
|
|
115
|
+
await channel.say("Oi!");
|
|
116
|
+
for (const line of channel.log()) {
|
|
117
|
+
expect(line.contact).not.toContain("987654321");
|
|
118
|
+
expect(line.contact).toContain("****");
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("applies the same rules through a different backend: a backend is not a way around one", async () => {
|
|
123
|
+
const other = new Recorder("another-backend", true);
|
|
124
|
+
const { channel, backend } = channelAt(OUTSIDE, other);
|
|
125
|
+
expect((await channel.say("boa noite, sobre a sua divida")).refused?.rule).toBe("collection_hours");
|
|
126
|
+
expect((await channelAt(INSIDE, new Recorder("another-backend", true)).channel.say("CPF 11144477735")).refused?.rule).toBe("no_document");
|
|
127
|
+
expect(backend.delivered).toHaveLength(0);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("WhatsApp's own rule: the 24-hour session window", () => {
|
|
132
|
+
it("carries a free-form message while the window is open", async () => {
|
|
133
|
+
const { channel, backend } = channelAt(INSIDE);
|
|
134
|
+
expect((await channel.say("Oi, Joana!")).refused).toBeUndefined();
|
|
135
|
+
expect(backend.delivered).toHaveLength(1);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("refuses a free-form message once the window has shut, and says what would carry", async () => {
|
|
139
|
+
const { channel, backend } = channelAt(INSIDE, new Recorder(), false);
|
|
140
|
+
const sent = await channel.say("Recebemos, acordo quitado.");
|
|
141
|
+
expect(sent.refused?.rule).toBe("session_window_closed");
|
|
142
|
+
expect(sent.refused?.detail).toContain("template");
|
|
143
|
+
expect(backend.delivered).toHaveLength(0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("carries a declared template with the window shut", async () => {
|
|
147
|
+
const { channel, backend } = channelAt(INSIDE, new Recorder(), false);
|
|
148
|
+
const sent = await channel.send({ kind: "template", template: "cobranca_lembrete", language: "pt_BR", variables: ["Joana"] });
|
|
149
|
+
expect(sent.refused).toBeUndefined();
|
|
150
|
+
expect(backend.delivered).toHaveLength(1);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("refuses a template the agent never declared: Meta only delivers the ones it approved", async () => {
|
|
154
|
+
const { channel, backend } = channelAt(INSIDE, new Recorder(), false);
|
|
155
|
+
const sent = await channel.send({ kind: "template", template: "nao_registrado", language: "pt_BR", variables: [] });
|
|
156
|
+
expect(sent.refused?.rule).toBe("template_unknown");
|
|
157
|
+
expect(backend.delivered).toHaveLength(0);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("holds a template to the house rules too: an approved template is not a way past the hours", async () => {
|
|
161
|
+
const { channel, backend } = channelAt(OUTSIDE, new Recorder(), false);
|
|
162
|
+
const sent = await channel.send({ kind: "template", template: "cobranca_lembrete", language: "pt_BR", variables: ["Joana"] });
|
|
163
|
+
expect(sent.refused?.rule).toBe("collection_hours");
|
|
164
|
+
expect(backend.delivered).toHaveLength(0);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("reopens the window when the person writes again", async () => {
|
|
168
|
+
const backend = new Recorder("recorder", false, [{ id: "in_1", from: CONTACT, text: "oi", timestamp: Math.floor(INSIDE.getTime() / 1000) }]);
|
|
169
|
+
const { channel } = channelAt(INSIDE, backend, false);
|
|
170
|
+
expect((await channel.say("antes")).refused?.rule).toBe("session_window_closed");
|
|
171
|
+
await channel.next();
|
|
172
|
+
expect((await channel.say("depois")).refused).toBeUndefined();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 24-hour window, read back and reasoned about.
|
|
3
|
+
*
|
|
4
|
+
* Two things are under test and they are the two halves of issue #25. A
|
|
5
|
+
* conversation that a later process comes back to has to know where its
|
|
6
|
+
* window stood, and the only honest source for that is the PROVIDER's
|
|
7
|
+
* timestamp in the record — not the run's own clock, which belongs to a
|
|
8
|
+
* machine WhatsApp never consulted. And a message that may only go as a
|
|
9
|
+
* template has to be held to what the agent actually declared, because a
|
|
10
|
+
* template Meta never approved is not a message, it is a 4xx.
|
|
11
|
+
*
|
|
12
|
+
* None of it needs the emulator: every refusal here happens above the
|
|
13
|
+
* backend, which is the whole claim `channels/` makes.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, expect, it } from "vitest";
|
|
16
|
+
import { templateArity, type WhatsAppTemplate } from "@codespar/agent-core";
|
|
17
|
+
import { WhatsAppChannel } from "../src/channels/whatsapp/index.js";
|
|
18
|
+
import { SessionWindow, sessionStateFromChannelLog, SESSION_WINDOW_SECONDS } from "../src/channels/whatsapp/session.js";
|
|
19
|
+
import type { ChannelBackend, InboundMessage, OutboundBody, SentMessage } from "../src/channels/types.js";
|
|
20
|
+
|
|
21
|
+
class Recorder implements ChannelBackend {
|
|
22
|
+
readonly name = "recorder";
|
|
23
|
+
readonly live = false;
|
|
24
|
+
readonly delivered: OutboundBody[] = [];
|
|
25
|
+
async open(): Promise<void> {}
|
|
26
|
+
async next(): Promise<InboundMessage | undefined> {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
async deliver(_to: string, body: OutboundBody): Promise<SentMessage> {
|
|
30
|
+
this.delivered.push(body);
|
|
31
|
+
return { id: `rec_${this.delivered.length}`, state: "sent" };
|
|
32
|
+
}
|
|
33
|
+
async close(): Promise<void> {}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const CONTACT = "+5511987654321";
|
|
37
|
+
const TUESDAY = new Date("2026-09-23T14:00:00-03:00");
|
|
38
|
+
const FRIDAY = new Date("2026-09-25T14:00:00-03:00");
|
|
39
|
+
const QUITADO: WhatsAppTemplate = { name: "acordo_quitado", language: "pt_BR", description: "paid", body: "Recebemos o pagamento do {{1}}." };
|
|
40
|
+
|
|
41
|
+
function channel(now: Date, lastInboundAt?: number, backend = new Recorder()) {
|
|
42
|
+
const c = new WhatsAppChannel({
|
|
43
|
+
backend,
|
|
44
|
+
conversation: { contact: CONTACT, subject: "acordo-1042" },
|
|
45
|
+
now: () => now,
|
|
46
|
+
templates: [QUITADO],
|
|
47
|
+
...(lastInboundAt !== undefined ? { session: { lastInboundAt } } : {}),
|
|
48
|
+
});
|
|
49
|
+
return { channel: c, backend };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("where the window stood, read back from the record", () => {
|
|
53
|
+
it("takes the provider's timestamp and never the line's own `at`", () => {
|
|
54
|
+
const state = sessionStateFromChannelLog([
|
|
55
|
+
{ direction: "in", at: "2020-01-01T00:00:00.000Z", provider_timestamp: 1790182801 },
|
|
56
|
+
{ direction: "out", at: "2026-09-23T17:00:10.000Z" },
|
|
57
|
+
{ direction: "in", at: "2020-01-01T00:00:00.000Z", provider_timestamp: 1790182821 },
|
|
58
|
+
]);
|
|
59
|
+
expect(state.lastInboundAt).toBe(1790182821);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("yields nothing from a log whose inbound lines carry no provider timestamp, and the window then reads as shut", () => {
|
|
63
|
+
// A bundle written before the timestamp was recorded. Shut is the safe
|
|
64
|
+
// direction: a template is deliverable in both halves of the rule.
|
|
65
|
+
const state = sessionStateFromChannelLog([{ direction: "in", at: "2026-09-23T17:00:00.000Z", text: "oi" }]);
|
|
66
|
+
expect(state.lastInboundAt).toBeUndefined();
|
|
67
|
+
expect(new SessionWindow([], state).open(TUESDAY)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("ignores outbound lines: the window is opened by the PERSON writing, not by us", () => {
|
|
71
|
+
expect(sessionStateFromChannelLog([{ direction: "out", provider_timestamp: 1790182801 }]).lastInboundAt).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("a conversation a later process comes back to", () => {
|
|
76
|
+
it("writes freely when the restored window is still open", async () => {
|
|
77
|
+
const { channel: c, backend } = channel(TUESDAY, Math.floor(TUESDAY.getTime() / 1000) - 3600);
|
|
78
|
+
expect(c.sessionOpen).toBe(true);
|
|
79
|
+
expect(c.sessionRemainingSeconds).toBe(SESSION_WINDOW_SECONDS - 3600);
|
|
80
|
+
const sent = await c.say("Recebemos, acordo quitado.");
|
|
81
|
+
expect(sent.refused).toBeUndefined();
|
|
82
|
+
expect(backend.delivered).toHaveLength(1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("refuses free text once the restored window has shut, which is the case the poll exists for", async () => {
|
|
86
|
+
const { channel: c, backend } = channel(FRIDAY, Math.floor(TUESDAY.getTime() / 1000));
|
|
87
|
+
expect(c.sessionOpen).toBe(false);
|
|
88
|
+
const sent = await c.say("Recebemos, acordo quitado.");
|
|
89
|
+
expect(sent.refused?.rule).toBe("session_window_closed");
|
|
90
|
+
expect(backend.delivered).toHaveLength(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("carries the declared template in the same breath, with the language the registry holds", async () => {
|
|
94
|
+
const { channel: c, backend } = channel(FRIDAY, Math.floor(TUESDAY.getTime() / 1000));
|
|
95
|
+
const declared = c.declaredTemplate("acordo_quitado");
|
|
96
|
+
expect(declared?.language).toBe("pt_BR");
|
|
97
|
+
const sent = await c.send({ kind: "template", template: declared!.name, language: declared!.language, variables: ["acordo-1042"] });
|
|
98
|
+
expect(sent.refused).toBeUndefined();
|
|
99
|
+
expect(backend.delivered).toEqual([{ kind: "template", template: "acordo_quitado", language: "pt_BR", variables: ["acordo-1042"] }]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("reads as shut when nothing restored it: a process with no record of the conversation assumes the worst", () => {
|
|
103
|
+
expect(channel(TUESDAY).channel.sessionOpen).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("when the provider's clock disagrees with ours", () => {
|
|
108
|
+
/** A provider whose window is shut whatever ours says: free text is refused the way Meta refuses it, templates go. */
|
|
109
|
+
class ShutProvider extends Recorder {
|
|
110
|
+
override async deliver(to: string, body: OutboundBody): Promise<SentMessage> {
|
|
111
|
+
if (body.kind === "template") return super.deliver(to, body);
|
|
112
|
+
return { id: "", state: "failed", refused: { rule: "session_window_closed", detail: "the provider refused the free-form message (400/131047)" } };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
it("believes the provider: after a 131047 the window reads shut, and a template still goes", async () => {
|
|
117
|
+
const { channel: c, backend } = channel(TUESDAY, Math.floor(TUESDAY.getTime() / 1000) - 3600, new ShutProvider());
|
|
118
|
+
expect(c.sessionOpen).toBe(true);
|
|
119
|
+
const sent = await c.say("Recebemos, acordo quitado.");
|
|
120
|
+
expect(sent.refused?.rule).toBe("session_window_closed");
|
|
121
|
+
expect(c.sessionOpen).toBe(false);
|
|
122
|
+
expect(c.sessionRemainingSeconds).toBe(0);
|
|
123
|
+
// Refused by our own rule from here, so a second free-form message never reaches the provider.
|
|
124
|
+
expect((await c.say("de novo")).refused?.detail).toContain("more than 24h");
|
|
125
|
+
const template = await c.send({ kind: "template", template: "acordo_quitado", language: "pt_BR", variables: ["acordo-1042"] });
|
|
126
|
+
expect(template.refused).toBeUndefined();
|
|
127
|
+
expect(backend.delivered).toHaveLength(1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("reopens when the person writes again, because that is what opens a window at Meta too", () => {
|
|
131
|
+
const window = new SessionWindow([], { lastInboundAt: Math.floor(TUESDAY.getTime() / 1000) - 3600 });
|
|
132
|
+
window.observeProviderShut();
|
|
133
|
+
expect(window.open(TUESDAY)).toBe(false);
|
|
134
|
+
window.observeInbound(Math.floor(TUESDAY.getTime() / 1000));
|
|
135
|
+
expect(window.open(TUESDAY)).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("the local registry refuses what Meta would refuse", () => {
|
|
140
|
+
it("a name the agent never declared", async () => {
|
|
141
|
+
const { channel: c, backend } = channel(FRIDAY, Math.floor(TUESDAY.getTime() / 1000));
|
|
142
|
+
const sent = await c.send({ kind: "template", template: "nao_registrado", language: "pt_BR", variables: [] });
|
|
143
|
+
expect(sent.refused?.rule).toBe("template_unknown");
|
|
144
|
+
expect(backend.delivered).toHaveLength(0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("a language the template was not registered in: an approved template does not exist in another", async () => {
|
|
148
|
+
const { channel: c, backend } = channel(FRIDAY, Math.floor(TUESDAY.getTime() / 1000));
|
|
149
|
+
const sent = await c.send({ kind: "template", template: "acordo_quitado", language: "es_MX", variables: ["acordo-1042"] });
|
|
150
|
+
expect(sent.refused?.rule).toBe("template_language_unknown");
|
|
151
|
+
expect(backend.delivered).toHaveLength(0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("a variable count the body has no placeholders for", async () => {
|
|
155
|
+
const { channel: c, backend } = channel(FRIDAY, Math.floor(TUESDAY.getTime() / 1000));
|
|
156
|
+
const sent = await c.send({ kind: "template", template: "acordo_quitado", language: "pt_BR", variables: ["acordo-1042", "a mais"] });
|
|
157
|
+
expect(sent.refused?.rule).toBe("template_variables_mismatch");
|
|
158
|
+
expect(sent.refused?.detail).toContain("takes 1 variable(s)");
|
|
159
|
+
expect(backend.delivered).toHaveLength(0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("counts the placeholders the way Meta numbers them", () => {
|
|
163
|
+
expect(templateArity("nenhuma")).toBe(0);
|
|
164
|
+
expect(templateArity("Oi {{1}}, sobre o {{2}}.")).toBe(2);
|
|
165
|
+
expect(templateArity("Oi {{ 1 }}.")).toBe(1);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -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
|
+
});
|