@codespar/cli 0.17.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/surface.js +53 -1
- package/dist/surface.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,596 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { ESCOLA, FUNCIONARIA, harness, MERCADO } from "./helpers.js";
|
|
5
|
+
|
|
6
|
+
const approver = { id: "usr_demo", channel: "terminal" };
|
|
7
|
+
|
|
8
|
+
describe("section 4.6: the model proposes, the code executes", () => {
|
|
9
|
+
it("human mode: drafted -> awaiting_approval -> approved -> executing -> settled, with an artifact and a receipt", async () => {
|
|
10
|
+
const h = harness({ mode: "human" });
|
|
11
|
+
const draft = await h.engine.draft({ items: [{ payee: "escola", amount: 185000, description: "outubro" }] });
|
|
12
|
+
expect(draft.ok).toBe(true);
|
|
13
|
+
if (!draft.ok) return;
|
|
14
|
+
expect(draft.execution.state).toBe("awaiting_approval");
|
|
15
|
+
expect(draft.execution.items[0]).toMatchObject({ alias: "escola", beneficiary: "Escola Aurora", payee: ESCOLA, amount: 185000 });
|
|
16
|
+
expect(draft.execution.total).toBe(185000);
|
|
17
|
+
|
|
18
|
+
const approved = h.engine.approve(draft.execution.id, approver);
|
|
19
|
+
expect(approved.state).toBe("approved");
|
|
20
|
+
expect(approved.approval_id).toMatch(/^apr_/);
|
|
21
|
+
const artifact = h.store.getApproval(approved.approval_id!)!;
|
|
22
|
+
expect(artifact.approver).toEqual({ type: "person", id: "usr_demo", channel: "terminal" });
|
|
23
|
+
expect(artifact.items_hash).toBe(approved.items_hash);
|
|
24
|
+
|
|
25
|
+
const settled = await h.engine.execute(approved.id);
|
|
26
|
+
expect(settled.state).toBe("settled");
|
|
27
|
+
expect(settled.history.map((t) => t.to)).toEqual(["awaiting_approval", "approved", "executing", "settled"]);
|
|
28
|
+
expect(settled.outcomes[0]?.receipt_id).toMatch(/^rcpt_stub_/);
|
|
29
|
+
const receipts = readdirSync(join(h.bundle.dir, "receipts"));
|
|
30
|
+
expect(receipts).toHaveLength(1);
|
|
31
|
+
expect(h.bundle.readApprovals()).toHaveLength(1);
|
|
32
|
+
expect(existsSync(join(h.bundle.dir, "events.jsonl"))).toBe(true);
|
|
33
|
+
for (const event of h.bundle.readEvents()) expect(event["actor"]).toBeDefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("mandate mode: drafted -> approved by the mandate -> settled, same trail shape", async () => {
|
|
37
|
+
const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
|
|
38
|
+
const draft = await h.engine.draft({ items: [{ payee: "escola", amount: 185000 }] });
|
|
39
|
+
if (!draft.ok) throw new Error("refused");
|
|
40
|
+
expect(draft.execution.state).toBe("approved");
|
|
41
|
+
const artifact = h.store.getApproval(draft.execution.approval_id!)!;
|
|
42
|
+
expect(artifact.approver).toEqual({ type: "mandate", id: "mdt_test_0001" });
|
|
43
|
+
const settled = await h.engine.execute(draft.execution.id);
|
|
44
|
+
expect(settled.state).toBe("settled");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("the core computes the total; the model's number is recorded and ignored", async () => {
|
|
48
|
+
const h = harness({ mode: "human" });
|
|
49
|
+
const draft = await h.engine.draft({ items: [{ payee: "escola", amount: 10000 }, { payee: "mercado", amount: 5000 }], claimed_total: 12000 });
|
|
50
|
+
if (!draft.ok) throw new Error("refused");
|
|
51
|
+
expect(draft.execution.total).toBe(15000);
|
|
52
|
+
expect(draft.execution.model_claimed_total).toBe(12000);
|
|
53
|
+
const approved = h.engine.approve(draft.execution.id, approver);
|
|
54
|
+
expect(h.store.getApproval(approved.approval_id!)!.items.reduce((s, i) => s + i.amount, 0)).toBe(15000);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("with model_total_mismatch: refuse, the execution is denied instead", async () => {
|
|
58
|
+
const h = harness({ mode: "human", guardrails: { model_total_mismatch: "refuse" } });
|
|
59
|
+
const draft = await h.engine.draft({ items: [{ payee: "escola", amount: 15000 }], claimed_total: 12000 });
|
|
60
|
+
if (!draft.ok) throw new Error("refused");
|
|
61
|
+
expect(draft.execution.state).toBe("denied");
|
|
62
|
+
expect(draft.execution.reason).toBe("model_total_mismatch");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("mandate limits are the core's, in both modes", () => {
|
|
67
|
+
it("per-transaction cap: denied, readable", async () => {
|
|
68
|
+
for (const mode of ["human", "mandate"] as const) {
|
|
69
|
+
const h = harness({ mode });
|
|
70
|
+
const draft = await h.engine.draft({ items: [{ payee: "escola", amount: 250001 }] });
|
|
71
|
+
if (!draft.ok) throw new Error("refused");
|
|
72
|
+
expect(draft.execution.state).toBe("denied");
|
|
73
|
+
expect(draft.execution.reason).toBe("per_tx_cap_exceeded");
|
|
74
|
+
expect(draft.execution.detail).toContain("per-payment cap");
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("window cap counts what is settled and in flight", async () => {
|
|
79
|
+
const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
|
|
80
|
+
for (let i = 0; i < 2; i += 1) {
|
|
81
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 250000 }] });
|
|
82
|
+
if (!d.ok) throw new Error("refused");
|
|
83
|
+
await h.engine.execute(d.execution.id);
|
|
84
|
+
}
|
|
85
|
+
const third = await h.engine.draft({ items: [{ payee: "mercado", amount: 100001 }] });
|
|
86
|
+
if (!third.ok) throw new Error("refused");
|
|
87
|
+
expect(third.execution.state).toBe("denied");
|
|
88
|
+
expect(third.execution.reason).toBe("window_cap_exceeded");
|
|
89
|
+
const fits = await h.engine.draft({ items: [{ payee: "mercado", amount: 100000 }] });
|
|
90
|
+
if (!fits.ok) throw new Error("refused");
|
|
91
|
+
expect(fits.execution.state).toBe("approved");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("payee outside the allowlist: denied in mandate, escalated in human, and never approvable", async () => {
|
|
95
|
+
const m = harness({ mode: "mandate" });
|
|
96
|
+
const denied = await m.engine.draft({ items: [{ payee: "chave-nova@banco.com", amount: 1000 }] });
|
|
97
|
+
if (!denied.ok) throw new Error("refused");
|
|
98
|
+
expect(denied.execution.state).toBe("denied");
|
|
99
|
+
expect(denied.execution.reason).toBe("beneficiary_not_allowed");
|
|
100
|
+
|
|
101
|
+
const hh = harness({ mode: "human" });
|
|
102
|
+
const escalated = await hh.engine.draft({ items: [{ payee: "chave-nova@banco.com", amount: 1000 }] });
|
|
103
|
+
if (!escalated.ok) throw new Error("refused");
|
|
104
|
+
expect(escalated.execution.state).toBe("awaiting_approval");
|
|
105
|
+
expect(escalated.execution.blocking_reasons).toEqual(["beneficiary_not_allowed"]);
|
|
106
|
+
const afterYes = hh.engine.approve(escalated.execution.id, approver);
|
|
107
|
+
expect(afterYes.state).toBe("denied");
|
|
108
|
+
expect(afterYes.reason).toBe("beneficiary_not_allowed");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("section 4.4 in the engine", () => {
|
|
113
|
+
it("amount above threshold in mandate mode goes to a human; the artifact records the trigger", async () => {
|
|
114
|
+
const h = harness({ mode: "mandate" });
|
|
115
|
+
// Make escola a known payee first, so only `amount` can fire.
|
|
116
|
+
const warm = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
|
|
117
|
+
if (!warm.ok) throw new Error("refused");
|
|
118
|
+
expect(warm.execution.escalation?.trigger).toBe("new_beneficiary");
|
|
119
|
+
await h.engine.execute(h.engine.approve(warm.execution.id, approver).id);
|
|
120
|
+
|
|
121
|
+
const big = await h.engine.draft({ items: [{ payee: "escola", amount: 150001 }] });
|
|
122
|
+
if (!big.ok) throw new Error("refused");
|
|
123
|
+
expect(big.execution.state).toBe("awaiting_approval");
|
|
124
|
+
expect(big.execution.escalation?.trigger).toBe("amount");
|
|
125
|
+
const approved = h.engine.approve(big.execution.id, approver);
|
|
126
|
+
expect(h.store.getApproval(approved.approval_id!)!.escalation?.trigger).toBe("amount");
|
|
127
|
+
|
|
128
|
+
await h.engine.execute(approved.id);
|
|
129
|
+
|
|
130
|
+
// The following one, below the threshold, runs alone: a human-approved payment is not fractioning.
|
|
131
|
+
const small = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
|
|
132
|
+
if (!small.ok) throw new Error("refused");
|
|
133
|
+
expect(small.execution.state).toBe("approved");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("fractioning: five parts below the threshold escalate by the window", async () => {
|
|
137
|
+
const h = harness({ mode: "mandate", manifest: { escalate_above: { amount: 150000 } }, guardrails: { escalate_above: { amount: 150000 } } });
|
|
138
|
+
const states: string[] = [];
|
|
139
|
+
for (let i = 0; i < 5; i += 1) {
|
|
140
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 40000 }] });
|
|
141
|
+
if (!d.ok) throw new Error("refused");
|
|
142
|
+
states.push(d.execution.state);
|
|
143
|
+
if (d.execution.state === "approved") await h.engine.execute(d.execution.id);
|
|
144
|
+
}
|
|
145
|
+
expect(states).toEqual(["approved", "approved", "approved", "awaiting_approval", "awaiting_approval"]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("outside hours escalates, or refuses when the guardrail says so", async () => {
|
|
149
|
+
const night = new Date("2026-09-24T02:00:00Z");
|
|
150
|
+
const a = harness({ mode: "mandate", now: night, manifest: { escalate_above: { outside_hours: "22:00-07:00" } }, guardrails: { escalate_above: { outside_hours: "22:00-07:00" } } });
|
|
151
|
+
const d1 = await a.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
|
|
152
|
+
if (!d1.ok) throw new Error("refused");
|
|
153
|
+
expect(d1.execution.state).toBe("awaiting_approval");
|
|
154
|
+
expect(d1.execution.escalation?.trigger).toBe("outside_hours");
|
|
155
|
+
|
|
156
|
+
const b = harness({ mode: "mandate", now: night, manifest: { escalate_above: { outside_hours: "22:00-07:00" } }, guardrails: { escalate_above: { outside_hours: "22:00-07:00" }, outside_hours_action: "refuse" } });
|
|
157
|
+
const d2 = await b.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
|
|
158
|
+
if (!d2.ok) throw new Error("refused");
|
|
159
|
+
expect(d2.execution.state).toBe("denied");
|
|
160
|
+
expect(d2.execution.reason).toBe("outside_hours");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("section 4.7: revocation and kill switch (against the stub)", () => {
|
|
165
|
+
it("a new request after revocation is refused before drafted", async () => {
|
|
166
|
+
const h = harness({ mode: "human" });
|
|
167
|
+
h.gate.revoke("mdt_test_0001");
|
|
168
|
+
const draft = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
|
|
169
|
+
expect(draft).toMatchObject({ ok: false, refused_before_draft: true, reason: "mandate_revoked" });
|
|
170
|
+
expect(h.store.listExecutions()).toHaveLength(0);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("revoked while awaiting approval: approved artifact does not help, execute answers denied", async () => {
|
|
174
|
+
const h = harness({ mode: "human" });
|
|
175
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
|
|
176
|
+
if (!d.ok) throw new Error("refused");
|
|
177
|
+
const approved = h.engine.approve(d.execution.id, approver);
|
|
178
|
+
h.gate.revoke("mdt_test_0001");
|
|
179
|
+
const out = await h.engine.execute(approved.id);
|
|
180
|
+
expect(out.state).toBe("denied");
|
|
181
|
+
expect(out.reason).toBe("mandate_revoked");
|
|
182
|
+
expect(h.store.listOutbox()).toHaveLength(0);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("org pauseAll denies with org_paused; paused mandate with mandate_paused", async () => {
|
|
186
|
+
const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
|
|
187
|
+
h.gate.pauseAll();
|
|
188
|
+
expect(await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] })).toMatchObject({ ok: false, reason: "org_paused" });
|
|
189
|
+
h.gate.resumeAll();
|
|
190
|
+
h.gate.pause("mdt_test_0001");
|
|
191
|
+
expect(await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] })).toMatchObject({ ok: false, reason: "mandate_paused" });
|
|
192
|
+
h.gate.resume("mdt_test_0001");
|
|
193
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
|
|
194
|
+
expect(d.ok && d.execution.state).toBe("approved");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("revoked while executing: reconciled from the rail, not cancelled", async () => {
|
|
198
|
+
const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
|
|
199
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
|
|
200
|
+
if (!d.ok) throw new Error("refused");
|
|
201
|
+
const attempt = `att_${d.execution.idempotency_key.slice(4)}_0`;
|
|
202
|
+
// The provider took the payment and answered late: uncertain now, in flight on the first lookup, settled on the second.
|
|
203
|
+
const h2 = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { uncertainOnce: [attempt], inFlightThenSettled: [attempt] } });
|
|
204
|
+
const stuck = await h2.engine.execute(d.execution.id);
|
|
205
|
+
expect(stuck.state).toBe("executing");
|
|
206
|
+
h2.gate.revoke("mdt_test_0001");
|
|
207
|
+
expect((await h2.engine.reconcile(stuck.id)).state).toBe("executing");
|
|
208
|
+
const closed = await h2.engine.reconcile(stuck.id);
|
|
209
|
+
expect(closed.state).toBe("settled");
|
|
210
|
+
expect(h2.rail.payCount).toBe(0);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("section 10: idempotency, restart, reconcile", () => {
|
|
215
|
+
it("killing after the rail accepted and resuming gives one payment and one receipt", async () => {
|
|
216
|
+
const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { afterDispatch: () => { throw new Error("SIGKILL simulated after dispatch"); } } });
|
|
217
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
|
|
218
|
+
if (!d.ok) throw new Error("refused");
|
|
219
|
+
await expect(h.engine.execute(d.execution.id)).rejects.toThrow("SIGKILL");
|
|
220
|
+
const stuck = h.store.getExecution(d.execution.id)!;
|
|
221
|
+
expect(stuck.state).toBe("executing");
|
|
222
|
+
expect(h.store.listOutbox({ status: ["sent"] })).toHaveLength(1);
|
|
223
|
+
|
|
224
|
+
// A fresh process over the same state.db.
|
|
225
|
+
const resumed = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
|
|
226
|
+
const done = await resumed.engine.reconcile(stuck.id);
|
|
227
|
+
expect(done.state).toBe("settled");
|
|
228
|
+
expect(readdirSync(join(resumed.bundle.dir, "receipts"))).toHaveLength(1);
|
|
229
|
+
// Exactly one attempt reached the rail, ever.
|
|
230
|
+
expect(resumed.store.stubRailGet(`att_${stuck.idempotency_key.slice(4)}_0`)).toBeDefined();
|
|
231
|
+
const events = resumed.store.listEvents({ execution_id: stuck.id }).filter((e) => e.type === "commerce.payment.succeeded");
|
|
232
|
+
expect(events).toHaveLength(1);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("an uncertain outcome is never retried blind: it stays executing, marked unresolved, until the rail answers", async () => {
|
|
236
|
+
const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
|
|
237
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
|
|
238
|
+
if (!d.ok) throw new Error("refused");
|
|
239
|
+
const attempt = `att_${d.execution.idempotency_key.slice(4)}_0`;
|
|
240
|
+
const flaky = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { uncertainOnce: [attempt] } });
|
|
241
|
+
const stuck = await flaky.engine.execute(d.execution.id);
|
|
242
|
+
expect(stuck.state).toBe("executing");
|
|
243
|
+
expect(stuck.reason).toBe("rail_uncertain");
|
|
244
|
+
expect(flaky.store.stubRailGet(attempt)).toBeUndefined();
|
|
245
|
+
const still = await flaky.engine.reconcile(stuck.id);
|
|
246
|
+
expect(still.state).toBe("executing");
|
|
247
|
+
expect(still.outcomes).toHaveLength(0);
|
|
248
|
+
expect(flaky.rail.payCount).toBe(0);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("duplicate and out-of-order rail events settle once", async () => {
|
|
252
|
+
const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
|
|
253
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
|
|
254
|
+
if (!d.ok) throw new Error("refused");
|
|
255
|
+
const attempt = `att_${d.execution.idempotency_key.slice(4)}_0`;
|
|
256
|
+
const flaky = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { uncertainOnce: [attempt] } });
|
|
257
|
+
const stuck = await flaky.engine.execute(d.execution.id);
|
|
258
|
+
expect(stuck.state).toBe("executing");
|
|
259
|
+
const paid = { event_id: "evt_paid_1", type: "commerce.payment.succeeded", attempt_id: attempt };
|
|
260
|
+
expect(flaky.engine.ingestExternalEvent(paid)).toEqual({ applied: true, reason: "settled" });
|
|
261
|
+
expect(flaky.engine.ingestExternalEvent(paid)).toEqual({ applied: false, reason: "duplicate event id" });
|
|
262
|
+
expect(flaky.engine.ingestExternalEvent({ event_id: "evt_created_late", type: "commerce.payment.created", attempt_id: attempt })).toMatchObject({ applied: false });
|
|
263
|
+
expect(flaky.engine.ingestExternalEvent({ event_id: "evt_paid_2", type: "commerce.payment.succeeded", attempt_id: attempt })).toMatchObject({ applied: false, reason: "execution already settled" });
|
|
264
|
+
expect(flaky.store.getExecution(stuck.id)!.state).toBe("settled");
|
|
265
|
+
expect(flaky.store.listEvents({ execution_id: stuck.id }).filter((e) => e.type === "execution.transition" && (e.payload as { to: string }).to === "settled")).toHaveLength(1);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("a failed rail answer closes as failed with the code, and a second draft to the same payee is a new execution", async () => {
|
|
269
|
+
const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { refusePayees: [MERCADO] } });
|
|
270
|
+
const d = await h.engine.draft({ items: [{ payee: "mercado", amount: 1000 }] });
|
|
271
|
+
if (!d.ok) throw new Error("refused");
|
|
272
|
+
const failed = await h.engine.execute(d.execution.id);
|
|
273
|
+
expect(failed.state).toBe("failed");
|
|
274
|
+
expect(failed.detail).toContain("psp_dispatch_failed");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("a stale open execution expires", async () => {
|
|
278
|
+
const h = harness({ mode: "human" });
|
|
279
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
|
|
280
|
+
if (!d.ok) throw new Error("refused");
|
|
281
|
+
h.setNow(new Date(h.now.getTime() + 16 * 60 * 1000));
|
|
282
|
+
const expired = h.engine.expireStale();
|
|
283
|
+
expect(expired.map((e) => e.state)).toEqual(["expired"]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("the items_hash is recomputed before executing; a changed list goes back to awaiting_approval", async () => {
|
|
287
|
+
const h = harness({ mode: "human" });
|
|
288
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
|
|
289
|
+
if (!d.ok) throw new Error("refused");
|
|
290
|
+
const approved = h.engine.approve(d.execution.id, approver);
|
|
291
|
+
// Tamper with the persisted list after approval, the way a bug or an attacker would.
|
|
292
|
+
h.store.saveExecution({ ...approved, items: [{ ...approved.items[0]!, amount: 999999 }] });
|
|
293
|
+
const back = await h.engine.execute(approved.id);
|
|
294
|
+
expect(back.state).toBe("awaiting_approval");
|
|
295
|
+
expect(back.reason).toBe("items_hash_mismatch");
|
|
296
|
+
expect(back.approval_id).toBeUndefined();
|
|
297
|
+
expect(h.store.listOutbox()).toHaveLength(0);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe("the policy runs at every gate, not only at draft (review of PR #1)", () => {
|
|
302
|
+
it("two drafts each inside the window cap cannot both settle: the second is refused by the reservation, and the last gate refuses what slipped past", async () => {
|
|
303
|
+
const h = harness({ mode: "human" });
|
|
304
|
+
const a = await h.engine.draft({ items: [{ payee: "escola", amount: 240000 }, { payee: "mercado", amount: 160000 }] }); // 400000
|
|
305
|
+
if (!a.ok) throw new Error("refused");
|
|
306
|
+
expect(a.execution.state).toBe("awaiting_approval");
|
|
307
|
+
const b = await h.engine.draft({ items: [{ payee: "funcionaria", amount: 200000 }, { payee: "mercado", amount: 100000 }] }); // 300000
|
|
308
|
+
if (!b.ok) throw new Error("refused");
|
|
309
|
+
expect(b.execution.state).toBe("denied");
|
|
310
|
+
expect(b.execution.reason).toBe("window_cap_exceeded");
|
|
311
|
+
|
|
312
|
+
// What slipped past: an approved execution whose window filled up before it ran (another process settled 300000 meanwhile).
|
|
313
|
+
const approvedA = h.engine.approve(a.execution.id, approver);
|
|
314
|
+
expect(approvedA.state).toBe("approved");
|
|
315
|
+
const foreign = { ...approvedA, id: "exe_other_process", state: "settled" as const, total: 300000, approval_id: undefined, history: [] };
|
|
316
|
+
h.store.saveExecution(foreign as never);
|
|
317
|
+
const last = await h.engine.execute(approvedA.id);
|
|
318
|
+
expect(last.state).toBe("denied");
|
|
319
|
+
expect(last.reason).toBe("window_cap_exceeded");
|
|
320
|
+
expect(h.store.listOutbox()).toHaveLength(0);
|
|
321
|
+
expect(h.engine.list({ state: "settled" }).reduce((s, e) => s + e.total, 0)).toBe(300000);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("a list tampered after approval goes back to awaiting_approval, and a yes on the tampered list is refused by the policy, not re-approved", async () => {
|
|
325
|
+
const h = harness({ mode: "human" });
|
|
326
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
|
|
327
|
+
if (!d.ok) throw new Error("refused");
|
|
328
|
+
const approved = h.engine.approve(d.execution.id, approver);
|
|
329
|
+
h.store.saveExecution({ ...approved, items: [{ ...approved.items[0]!, beneficiary: "atacante", payee: "pix@atacante.example.com" }] });
|
|
330
|
+
const back = await h.engine.execute(approved.id);
|
|
331
|
+
expect(back.state).toBe("awaiting_approval");
|
|
332
|
+
expect(back.reason).toBe("items_hash_mismatch");
|
|
333
|
+
const afterYes = h.engine.approve(back.id, approver);
|
|
334
|
+
expect(afterYes.state).toBe("denied");
|
|
335
|
+
expect(afterYes.reason).toBe("beneficiary_not_allowed");
|
|
336
|
+
expect(h.store.listOutbox()).toHaveLength(0);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("a mandate-approved execution re-checks escalate_above at the last gate; a person's approval satisfies it", async () => {
|
|
340
|
+
// Approved by the mandate during the day, executed at night: back to a human.
|
|
341
|
+
const day = new Date("2026-09-23T18:00:00Z");
|
|
342
|
+
const h = harness({ mode: "mandate", now: day, manifest: { escalate_above: { outside_hours: "22:00-07:00" } }, guardrails: { escalate_above: { outside_hours: "22:00-07:00" }, approval_ttl_minutes: 24 * 60 } });
|
|
343
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
|
|
344
|
+
if (!d.ok) throw new Error("refused");
|
|
345
|
+
expect(d.execution.state).toBe("approved");
|
|
346
|
+
h.setNow(new Date("2026-09-24T02:00:00Z"));
|
|
347
|
+
const night = await h.engine.execute(d.execution.id);
|
|
348
|
+
expect(night.state).toBe("awaiting_approval");
|
|
349
|
+
expect(night.escalation?.trigger).toBe("outside_hours");
|
|
350
|
+
const settled = await h.engine.execute(h.engine.approve(night.id, approver).id);
|
|
351
|
+
expect(settled.state).toBe("settled");
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe("reconcile never re-dispatches (review of PR #1)", () => {
|
|
356
|
+
async function stuckExecution(rail: import("../src/stubs/rail.js").StubRailOptions | undefined) {
|
|
357
|
+
const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
|
|
358
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
|
|
359
|
+
if (!d.ok) throw new Error("refused");
|
|
360
|
+
const attempt = `att_${d.execution.idempotency_key.slice(4)}_0`;
|
|
361
|
+
const flaky = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { uncertainOnce: [attempt], ...(rail ?? {}), inFlightThenSettled: [attempt] } });
|
|
362
|
+
const stuck = await flaky.engine.execute(d.execution.id);
|
|
363
|
+
expect(stuck.state).toBe("executing");
|
|
364
|
+
expect(stuck.reason).toBe("rail_uncertain");
|
|
365
|
+
return { h: flaky, stuck, attempt };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
it("in flight, then settled: two reconciles, zero new payments", async () => {
|
|
369
|
+
const { h, stuck, attempt } = await stuckExecution(undefined);
|
|
370
|
+
const paysBefore = h.rail.payCount;
|
|
371
|
+
const first = await h.engine.reconcile(stuck.id);
|
|
372
|
+
expect(first.state).toBe("executing");
|
|
373
|
+
expect(first.detail).toContain("in_flight");
|
|
374
|
+
expect(h.rail.payCount).toBe(paysBefore);
|
|
375
|
+
expect(h.store.stubRailGet(attempt)).toBeUndefined();
|
|
376
|
+
const second = await h.engine.reconcile(stuck.id);
|
|
377
|
+
expect(second.state).toBe("settled");
|
|
378
|
+
expect(h.rail.payCount).toBe(paysBefore);
|
|
379
|
+
expect(h.store.listOutbox()[0]?.status).toBe("done");
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("unknown to the rail after the outbox was sent: stays executing for a human, nothing re-sent", async () => {
|
|
383
|
+
const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
|
|
384
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
|
|
385
|
+
if (!d.ok) throw new Error("refused");
|
|
386
|
+
const attempt = `att_${d.execution.idempotency_key.slice(4)}_0`;
|
|
387
|
+
const flaky = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { uncertainOnce: [attempt] } });
|
|
388
|
+
const stuck = await flaky.engine.execute(d.execution.id);
|
|
389
|
+
expect(stuck.state).toBe("executing");
|
|
390
|
+
const again = await flaky.engine.reconcile(stuck.id);
|
|
391
|
+
expect(again.state).toBe("executing");
|
|
392
|
+
expect(again.reason).toBe("rail_uncertain");
|
|
393
|
+
expect(flaky.rail.payCount).toBe(0);
|
|
394
|
+
expect(flaky.store.stubRailGet(attempt)).toBeUndefined();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("outbox still pending (crash before any call): the attempts go out once, under the same ids", async () => {
|
|
398
|
+
const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { afterDispatch: () => { throw new Error("crash"); } } });
|
|
399
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
|
|
400
|
+
if (!d.ok) throw new Error("refused");
|
|
401
|
+
// Simulate a crash between the transaction and the first call: rewind the outbox to pending.
|
|
402
|
+
await expect(h.engine.execute(d.execution.id)).rejects.toThrow("crash");
|
|
403
|
+
const fresh = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
|
|
404
|
+
expect(fresh.store.getOutbox(d.execution.idempotency_key)?.status).toBe("sent");
|
|
405
|
+
const closed = await fresh.engine.reconcile(d.execution.id);
|
|
406
|
+
expect(closed.state).toBe("settled");
|
|
407
|
+
expect(fresh.rail.payCount).toBe(0);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe("a multi-item execution closes only when every attempt has its outcome (review of PR #1)", () => {
|
|
412
|
+
it("one rail event settles one attempt; the second closes the execution and the outbox", async () => {
|
|
413
|
+
const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
|
|
414
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }, { payee: "mercado", amount: 2000 }] });
|
|
415
|
+
if (!d.ok) throw new Error("refused");
|
|
416
|
+
const a0 = `att_${d.execution.idempotency_key.slice(4)}_0`;
|
|
417
|
+
const a1 = `att_${d.execution.idempotency_key.slice(4)}_1`;
|
|
418
|
+
const flaky = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { uncertainOnce: [a0, a1] } });
|
|
419
|
+
const stuck = await flaky.engine.execute(d.execution.id);
|
|
420
|
+
expect(stuck.state).toBe("executing");
|
|
421
|
+
expect(flaky.engine.ingestExternalEvent({ event_id: "e0", type: "commerce.payment.succeeded", attempt_id: a0 })).toMatchObject({ applied: true });
|
|
422
|
+
expect(flaky.store.getExecution(stuck.id)!.state).toBe("executing");
|
|
423
|
+
expect(flaky.store.getOutbox(stuck.idempotency_key)!.status).toBe("sent");
|
|
424
|
+
expect(flaky.engine.ingestExternalEvent({ event_id: "e0", type: "commerce.payment.succeeded", attempt_id: a0 })).toMatchObject({ applied: false, reason: "duplicate event id" });
|
|
425
|
+
expect(flaky.engine.ingestExternalEvent({ event_id: "e1", type: "commerce.payment.succeeded", attempt_id: a1 })).toEqual({ applied: true, reason: "settled" });
|
|
426
|
+
expect(flaky.store.getExecution(stuck.id)!.state).toBe("settled");
|
|
427
|
+
expect(flaky.store.getOutbox(stuck.idempotency_key)!.status).toBe("done");
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe("probes named in the second review: the mandate can change between approve and execute", () => {
|
|
432
|
+
it("(a) the mandate was tightened to a smaller per-payment cap: the last gate refuses", async () => {
|
|
433
|
+
const h = harness({ mode: "human" });
|
|
434
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 200000 }] });
|
|
435
|
+
if (!d.ok) throw new Error("refused");
|
|
436
|
+
const approved = h.engine.approve(d.execution.id, approver);
|
|
437
|
+
// A new process reads the tightened mandate (same version: an operator lowered the cap in place).
|
|
438
|
+
const tightened = harness({ mode: "human", dir: h.dir, mandate: { per_tx_cap_minor: 100000 } });
|
|
439
|
+
const out = await tightened.engine.execute(approved.id);
|
|
440
|
+
expect(out.state).toBe("denied");
|
|
441
|
+
expect(out.reason).toBe("per_tx_cap_exceeded");
|
|
442
|
+
expect(tightened.store.listOutbox()).toHaveLength(0);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("(a') the mandate was re-signed (new version): the old artifact no longer matches, and the yes that follows is judged again", async () => {
|
|
446
|
+
const h = harness({ mode: "human" });
|
|
447
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 200000 }] });
|
|
448
|
+
if (!d.ok) throw new Error("refused");
|
|
449
|
+
const approved = h.engine.approve(d.execution.id, approver);
|
|
450
|
+
const resigned = harness({ mode: "human", dir: h.dir, mandate: { version: 2, per_tx_cap_minor: 100000 } });
|
|
451
|
+
const back = await resigned.engine.execute(approved.id);
|
|
452
|
+
expect(back.state).toBe("awaiting_approval");
|
|
453
|
+
expect(back.reason).toBe("mandate_changed");
|
|
454
|
+
expect(back.mandate).toEqual({ id: "mdt_test_0001", version: 2 });
|
|
455
|
+
const afterYes = resigned.engine.approve(back.id, approver);
|
|
456
|
+
expect(afterYes.state).toBe("denied");
|
|
457
|
+
expect(afterYes.reason).toBe("per_tx_cap_exceeded");
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("(c) the payee left the allowlist between approve and execute: denied, nothing sent", async () => {
|
|
461
|
+
const h = harness({ mode: "human" });
|
|
462
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
|
|
463
|
+
if (!d.ok) throw new Error("refused");
|
|
464
|
+
const approved = h.engine.approve(d.execution.id, approver);
|
|
465
|
+
const narrowed = harness({ mode: "human", dir: h.dir, mandate: { merchant_allowlist: [MERCADO], beneficiaries: [{ alias: "mercado", name: "Mercado do Bairro", payee: MERCADO }] } });
|
|
466
|
+
const out = await narrowed.engine.execute(approved.id);
|
|
467
|
+
expect(out.state).toBe("denied");
|
|
468
|
+
expect(out.reason).toBe("beneficiary_not_allowed");
|
|
469
|
+
expect(narrowed.store.listOutbox()).toHaveLength(0);
|
|
470
|
+
expect(narrowed.rail.payCount).toBe(0);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
describe("reconcile is read-only; only resume dispatches, and only a pending outbox", () => {
|
|
475
|
+
it("outbox sent + lookup absent: reconcile never calls rail.pay and records execution.uncertain", async () => {
|
|
476
|
+
const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
|
|
477
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
|
|
478
|
+
if (!d.ok) throw new Error("refused");
|
|
479
|
+
const attempt = `att_${d.execution.idempotency_key.slice(4)}_0`;
|
|
480
|
+
const flaky = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { uncertainOnce: [attempt] } });
|
|
481
|
+
const stuck = await flaky.engine.execute(d.execution.id);
|
|
482
|
+
expect(flaky.store.getOutbox(stuck.idempotency_key)?.status).toBe("sent");
|
|
483
|
+
const pays = flaky.rail.payCount;
|
|
484
|
+
const still = await flaky.engine.reconcile(stuck.id);
|
|
485
|
+
expect(still.state).toBe("executing");
|
|
486
|
+
expect(flaky.rail.payCount).toBe(pays);
|
|
487
|
+
expect(flaky.store.stubRailGet(attempt)).toBeUndefined();
|
|
488
|
+
expect(flaky.store.listEvents({ execution_id: stuck.id }).some((e) => e.type === "execution.uncertain")).toBe(true);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("outbox pending: reconcile leaves it; resumePending dispatches once", async () => {
|
|
492
|
+
const h = harness({ mode: "mandate", manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
|
|
493
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
|
|
494
|
+
if (!d.ok) throw new Error("refused");
|
|
495
|
+
// Rewind the row to what a crash between the transaction and the first call leaves behind.
|
|
496
|
+
const armed = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, rail: { afterDispatch: () => { throw new Error("crash"); } } });
|
|
497
|
+
await expect(armed.engine.execute(d.execution.id)).rejects.toThrow("crash");
|
|
498
|
+
armed.store.updateOutbox(d.execution.idempotency_key, "pending", undefined, "2026-09-23T18:00:00.000Z");
|
|
499
|
+
const fresh = harness({ mode: "mandate", dir: h.dir, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} } });
|
|
500
|
+
const left = await fresh.engine.reconcile(d.execution.id);
|
|
501
|
+
expect(left.state).toBe("executing");
|
|
502
|
+
expect(fresh.rail.payCount).toBe(0);
|
|
503
|
+
const closed = await fresh.engine.resumePending(d.execution.id);
|
|
504
|
+
expect(closed.state).toBe("settled");
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
describe("multi-item partial failure is named item by item", () => {
|
|
509
|
+
it("first item settles with its receipt, second is refused: execution failed, outcomes and approval name both", async () => {
|
|
510
|
+
const h = harness({ mode: "human", rail: { refusePayees: [MERCADO] } });
|
|
511
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }, { payee: "mercado", amount: 2000 }] });
|
|
512
|
+
if (!d.ok) throw new Error("refused");
|
|
513
|
+
const out = await h.engine.execute(h.engine.approve(d.execution.id, approver).id);
|
|
514
|
+
expect(out.state).toBe("failed");
|
|
515
|
+
expect(out.outcomes).toEqual([
|
|
516
|
+
expect.objectContaining({ index: 0, status: "settled", receipt_id: expect.stringMatching(/^rcpt_stub_/) }),
|
|
517
|
+
expect.objectContaining({ index: 1, status: "failed", error: expect.stringContaining("psp_dispatch_failed") }),
|
|
518
|
+
]);
|
|
519
|
+
expect(readdirSync(join(h.bundle.dir, "receipts"))).toHaveLength(1);
|
|
520
|
+
expect(h.bundle.readApprovals()[0]?.items.map((i) => i.payee)).toEqual([ESCOLA, MERCADO]);
|
|
521
|
+
expect(h.store.getOutbox(out.idempotency_key)?.status).toBe("failed");
|
|
522
|
+
expect(h.store.getOutbox(out.idempotency_key)?.response).toEqual(out.outcomes);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("a refusal in the MIDDLE does not stop the items after it: every attempt is sent and every one is named", async () => {
|
|
526
|
+
const h = harness({ mode: "human", rail: { refusePayees: [MERCADO] } });
|
|
527
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }, { payee: "mercado", amount: 2000 }, { payee: "funcionaria", amount: 3000 }] });
|
|
528
|
+
if (!d.ok) throw new Error("refused");
|
|
529
|
+
const out = await h.engine.execute(h.engine.approve(d.execution.id, approver).id);
|
|
530
|
+
// Section 4.1: the execution closes `failed` because one attempt failed,
|
|
531
|
+
// and `outcomes` names EVERY item — including the one after the refusal.
|
|
532
|
+
expect(out.state).toBe("failed");
|
|
533
|
+
expect(out.outcomes.map((o) => o.status)).toEqual(["settled", "failed", "settled"]);
|
|
534
|
+
expect(h.rail.payCount).toBe(3); // all three reached the rail; the stub records a refusal too
|
|
535
|
+
expect(readdirSync(join(h.bundle.dir, "receipts"))).toHaveLength(2);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it("an unknown answer in the MIDDLE still sends the siblings, and the execution stays open until it is resolved", async () => {
|
|
539
|
+
const h = harness({ mode: "human", rail: { uncertainPayees: [MERCADO] } });
|
|
540
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }, { payee: "mercado", amount: 2000 }, { payee: "funcionaria", amount: 3000 }] });
|
|
541
|
+
if (!d.ok) throw new Error("refused");
|
|
542
|
+
const out = await h.engine.execute(h.engine.approve(d.execution.id, approver).id);
|
|
543
|
+
// Open, not closed: one attempt has no outcome at all, and §4.1 says an
|
|
544
|
+
// execution closes only when every attempt has one.
|
|
545
|
+
expect(out.state).toBe("executing");
|
|
546
|
+
expect(out.reason).toBe("rail_uncertain");
|
|
547
|
+
expect(out.outcomes.map((o) => o.index)).toEqual([0, 2]);
|
|
548
|
+
expect(out.detail).toContain("psp_dispatch_uncertain");
|
|
549
|
+
// Reconciliation resolves the unknown one and only then does it close.
|
|
550
|
+
// The stub answers `in_flight` on the first look and finds the provider
|
|
551
|
+
// finished on the second, which is the shape a real timeout has.
|
|
552
|
+
expect((await h.engine.reconcile(d.execution.id)).state).toBe("executing");
|
|
553
|
+
const closed = await h.engine.reconcile(d.execution.id);
|
|
554
|
+
expect(closed.state).toBe("settled");
|
|
555
|
+
expect(closed.outcomes.map((o) => o.status)).toEqual(["settled", "settled", "settled"]);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("an execution with a failed attempt AND an unknown one does not close as failed", async () => {
|
|
559
|
+
const h = harness({ mode: "human", rail: { refusePayees: [ESCOLA], uncertainPayees: [MERCADO] } });
|
|
560
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }, { payee: "mercado", amount: 2000 }, { payee: "funcionaria", amount: 3000 }] });
|
|
561
|
+
if (!d.ok) throw new Error("refused");
|
|
562
|
+
const out = await h.engine.execute(h.engine.approve(d.execution.id, approver).id);
|
|
563
|
+
// "One of them failed and one of them we cannot see" is not a closed
|
|
564
|
+
// outcome: reporting `failed` here would call a batch finished while a
|
|
565
|
+
// payment may still be in flight.
|
|
566
|
+
expect(out.state).toBe("executing");
|
|
567
|
+
expect(out.outcomes.map((o) => o.status)).toEqual(["failed", "settled"]);
|
|
568
|
+
expect((await h.engine.reconcile(d.execution.id)).state).toBe("executing");
|
|
569
|
+
const closed = await h.engine.reconcile(d.execution.id);
|
|
570
|
+
expect(closed.state).toBe("failed");
|
|
571
|
+
expect(closed.outcomes.map((o) => o.status)).toEqual(["failed", "settled", "settled"]);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it("why the siblings are sent and not deferred: an attempt that was never sent is recovered by nothing", async () => {
|
|
575
|
+
// This is the measurement the choice rests on. `reconcile` is read-only by
|
|
576
|
+
// contract and never dispatches; `resumePending` dispatches only while the
|
|
577
|
+
// outbox row is still `pending`, and it flips to `sent` before the first
|
|
578
|
+
// rail call. So an attempt skipped at dispatch is not sent later — it is
|
|
579
|
+
// never sent, by any path the runtime has.
|
|
580
|
+
const h = harness({ mode: "human", rail: { uncertainPayees: [MERCADO] } });
|
|
581
|
+
const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }, { payee: "mercado", amount: 2000 }, { payee: "funcionaria", amount: 3000 }] });
|
|
582
|
+
if (!d.ok) throw new Error("refused");
|
|
583
|
+
const out = await h.engine.execute(h.engine.approve(d.execution.id, approver).id);
|
|
584
|
+
const third = `att_${out.idempotency_key.slice(4)}_2`;
|
|
585
|
+
// Under the fix the third attempt WAS sent, so the rail knows it.
|
|
586
|
+
expect(h.store.stubRailGet(third)).toBeDefined();
|
|
587
|
+
expect(h.store.getOutbox(out.idempotency_key)?.status).toBe("sent");
|
|
588
|
+
// And that is the only reason it is recoverable: `resumePending` on a
|
|
589
|
+
// `sent` row reconciles instead of dispatching, and never pays again.
|
|
590
|
+
const before = h.rail.payCount;
|
|
591
|
+
expect((await h.engine.resumePending(d.execution.id)).state).toBe("executing");
|
|
592
|
+
const resumed = await h.engine.resumePending(d.execution.id);
|
|
593
|
+
expect(h.rail.payCount).toBe(before); // reconciled, never re-sent
|
|
594
|
+
expect(resumed.state).toBe("settled");
|
|
595
|
+
});
|
|
596
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { evaluateEscalation, isOutsideHours, localClock } from "../src/escalate.js";
|
|
3
|
+
import { GuardrailsSchema } from "../src/guardrails.js";
|
|
4
|
+
import type { ExecutionItem } from "../src/types.js";
|
|
5
|
+
|
|
6
|
+
const rule = { amount: 150000, new_beneficiary: true, outside_hours: "22:00-07:00" };
|
|
7
|
+
const guardrails = GuardrailsSchema.parse({ approval: "mandate", escalate_above: rule, velocity: { window_hours: 24 } });
|
|
8
|
+
const item = (payee: string, amount: number): ExecutionItem => ({ beneficiary: payee, payee, amount, currency: "BRL" });
|
|
9
|
+
// 15:00 in Sao Paulo (UTC-3) is 18:00Z.
|
|
10
|
+
const afternoon = new Date("2026-09-23T18:00:00Z");
|
|
11
|
+
const ctx = (over: Partial<Parameters<typeof evaluateEscalation>[3]> = {}) => ({ now: afternoon, timezone: "America/Sao_Paulo", knownPayees: new Set(["escola@x"]), recentByPayee: new Map(), ...over });
|
|
12
|
+
|
|
13
|
+
describe("section 4.4: escalate_above only tightens", () => {
|
|
14
|
+
it("amount above the threshold escalates, below it does not", () => {
|
|
15
|
+
expect(evaluateEscalation(rule, guardrails, [item("escola@x", 150001)], ctx())?.trigger).toBe("amount");
|
|
16
|
+
expect(evaluateEscalation(rule, guardrails, [item("escola@x", 150000)], ctx())).toBeUndefined();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("fractioning: parts below the threshold add up inside the velocity window", () => {
|
|
20
|
+
const recent = new Map([["escola@x", 120000]]);
|
|
21
|
+
const out = evaluateEscalation(rule, guardrails, [item("escola@x", 40000)], ctx({ recentByPayee: recent }));
|
|
22
|
+
expect(out?.trigger).toBe("amount");
|
|
23
|
+
expect(out?.detail).toContain("add up");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("first payment to a payee escalates; the second does not", () => {
|
|
27
|
+
expect(evaluateEscalation(rule, guardrails, [item("mercado@x", 1000)], ctx())?.trigger).toBe("new_beneficiary");
|
|
28
|
+
expect(evaluateEscalation(rule, guardrails, [item("escola@x", 1000)], ctx())).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("outside hours escalates, and the window crosses midnight", () => {
|
|
32
|
+
const night = new Date("2026-09-24T02:00:00Z"); // 23:00 in Sao Paulo
|
|
33
|
+
expect(localClock(night, "America/Sao_Paulo")).toBe("23:00");
|
|
34
|
+
expect(isOutsideHours("22:00-07:00", night, "America/Sao_Paulo")).toBe(true);
|
|
35
|
+
expect(isOutsideHours("22:00-07:00", afternoon, "America/Sao_Paulo")).toBe(false);
|
|
36
|
+
expect(evaluateEscalation(rule, guardrails, [item("escola@x", 1000)], ctx({ now: night }))?.trigger).toBe("outside_hours");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("no rule, no escalation", () => {
|
|
40
|
+
expect(evaluateEscalation(undefined, guardrails, [item("x", 10_000_000)], ctx())).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
});
|
package/templates/checkout-agent/packages/agent-core/test/fixtures/production-receipt-keys.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"issuer":"did:web:id.codespar.dev","key_namespace":"production","algorithm":"Ed25519","signing_string":"codespar-receipt:v1:<receipt_id>:<chain>","chain_recipe":{"canonicalization":"RFC 8785 (JCS)","digest":"SHA-256 over the UTF-8 bytes of the canonical JSON, lowercase hex","input":"{ \"v\": <chain_version>, \"links\": [ <each link below that applies, in this order> ] }","version":{"field":"chain_version","rule":"4 when `approval` is present; else 3 when `payment.amount_authorized` is present; else 2 when `payment.amount_atomic` is not null; else 1"},"optional_marker":"?","links":[{"link":"mandate","from":"mandate","when":"always","fields_by_version":{"1":["id","nonce","scope","currency","sig"],"2":["id","nonce","scope","currency","sig"],"3":["id","nonce","scope","currency","sig"],"4":["id","nonce","scope","currency","sig_sha256"]}},{"link":"quote","from":"quote","when":"quote is not null","fields":["seller","resource","price_minor","payee","session_id","sig","at"]},{"link":"approval","from":"approval","when":"chain_version is 4","fields":["items_hash","batch_hash"]},{"link":"payment","from":"payment","when":"always","fields":["rail","provider","tx_id","amount_minor","amount_atomic?","amount_authorized?","amount_charged?","amount_refunded?","metering?","sandbox?","attempt_id","money_moved","at"]},{"link":"delivery","from":"delivery","when":"delivery is not null","fields":["result","proof","kind","nfe_chave","at"]}],"notes":["`mandate.sig_sha256` is SHA-256 over the UTF-8 bytes of `mandate.sig`, lowercase hex. A v4 chain seals it in place of `sig`, which must not be shared; v1-v3 seal `sig` itself, so only the tenant, whose read carries `sig`, can recompute those. Anyone can still verify their signature over `chain`.","Recomputing needs `quote.payee` unmasked. Whoever shares a receipt decides whether to hand it over.","Timestamps are RFC 3339 UTC with milliseconds, exactly as read."]},"keys":[{"kid":"did:web:id.codespar.dev#1","kty":"OKP","crv":"Ed25519","x":"fz1svKf1AFlnIEZSseNqH5zCXkKqPmtkjx9mMNBz8pU","alg":"EdDSA","use":"sig","status":"retired","created_at":"2026-07-05T13:29:23.276Z","retired_at":"2026-09-25T02:43:45.971Z"},{"kid":"did:web:id.codespar.dev#production-2","kty":"OKP","crv":"Ed25519","x":"WKVTphnIma44d5T7bugYnLvI6h79LhaSdNDrkOFxmRs","alg":"EdDSA","use":"sig","status":"active","created_at":"2026-09-25T02:43:45.971Z","retired_at":null}]}
|