hecks 3.14.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  7. data/examples/pizzas/config.ru +22 -0
  8. data/lib/hecks/adapters/driven/prism.rb +0 -2
  9. data/lib/hecks/adapters/driven.rb +0 -2
  10. data/lib/hecks/adapters/driving/json.rb +1 -7
  11. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  12. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  13. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  14. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  15. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  16. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  17. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  18. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  19. data/lib/hecks/bluebook/translation.rb +18 -10
  20. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  21. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  22. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  23. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  24. data/lib/hecks/bluebook.rb +2 -0
  25. data/lib/hecks/chapters.rb +3 -3
  26. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  27. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  28. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  29. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  30. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  31. data/lib/hecks/composer/adapters/jev.rb +66 -0
  32. data/lib/hecks/composer/agent.rb +113 -0
  33. data/lib/hecks/composer/assistant.rb +110 -0
  34. data/lib/hecks/composer/behavior_run.rb +53 -0
  35. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  36. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  37. data/lib/hecks/composer/board.rb +65 -0
  38. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  39. data/lib/hecks/composer/board_edit/source.rb +84 -0
  40. data/lib/hecks/composer/board_edit.rb +144 -0
  41. data/lib/hecks/composer/check.rb +89 -0
  42. data/lib/hecks/composer/domains.rb +81 -0
  43. data/lib/hecks/composer/jobs.rb +134 -0
  44. data/lib/hecks/composer/layout.rb +37 -0
  45. data/lib/hecks/composer/live.rb +150 -0
  46. data/lib/hecks/composer/outline.rb +48 -0
  47. data/lib/hecks/composer/page.html +989 -0
  48. data/lib/hecks/composer/panels.rb +142 -0
  49. data/lib/hecks/composer/parse.rb +83 -0
  50. data/lib/hecks/composer/plain.rb +28 -0
  51. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  52. data/lib/hecks/composer/session.rb +141 -0
  53. data/lib/hecks/composer/toolbox.rb +166 -0
  54. data/lib/hecks/composer/web.rb +156 -0
  55. data/lib/hecks/composer/workbench.rb +51 -0
  56. data/lib/hecks/composer/workspace.rb +81 -0
  57. data/lib/hecks/composer.rb +17 -0
  58. data/lib/hecks/corpus/adapters.rb +22 -0
  59. data/lib/hecks/corpus/rust.rb +1 -1
  60. data/lib/hecks/corpus/worlds.rb +19 -0
  61. data/lib/hecks/corpus.rb +5 -0
  62. data/lib/hecks/forms/app/commands.rb +2 -3
  63. data/lib/hecks/forms/app/queries.rb +2 -3
  64. data/lib/hecks/forms/app/records.rb +17 -5
  65. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  66. data/lib/hecks/forms/field_controls.rb +2 -2
  67. data/lib/hecks/forms/field_renderer.rb +1 -1
  68. data/lib/hecks/forms/field_shape.rb +36 -17
  69. data/lib/hecks/forms/index_renderer.rb +2 -1
  70. data/lib/hecks/forms/page.rb +1 -1
  71. data/lib/hecks/forms/params.rb +0 -13
  72. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  73. data/lib/hecks/forms/record_renderer.rb +11 -7
  74. data/lib/hecks/forms/rule_words.rb +100 -0
  75. data/lib/hecks/forms/value_object_shape.rb +2 -1
  76. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  77. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  78. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  79. data/lib/hecks/gate/stages.yml +25 -0
  80. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  81. data/lib/hecks/grammar/evolve.rb +0 -3
  82. data/lib/hecks/grammar/translation.bluebook +1 -1
  83. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  84. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  85. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  86. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  87. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  88. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  89. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  90. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  91. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  92. data/lib/hecks/hecks/codebase.behaviors +6 -0
  93. data/lib/hecks/hecks/codebase.bluebook +21 -0
  94. data/lib/hecks/hecks/composer.behaviors +68 -0
  95. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  96. data/lib/hecks/hecks/custodian.behaviors +0 -6
  97. data/lib/hecks/hecks/custodian.bluebook +3 -80
  98. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  99. data/lib/hecks/hecks/hecks.world +1 -1
  100. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  101. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  102. data/lib/hecks/kernel_capabilities.rb +1 -1
  103. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  104. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  105. data/lib/hecks/ports/access_control.rb +2 -2
  106. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  107. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  108. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  109. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  110. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  111. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  112. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  113. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  114. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  118. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  119. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  120. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  121. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  122. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  123. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  124. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  125. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  126. data/lib/hecks/ports.rb +0 -1
  127. data/lib/hecks/projection_files.rb +3 -1
  128. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  129. data/lib/hecks/projections/glossary/entries.rb +10 -4
  130. data/lib/hecks/projections/glossary/groups.rb +42 -0
  131. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  132. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  133. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  134. data/lib/hecks/projections/glossary/html.rb +1 -2
  135. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  136. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  137. data/lib/hecks/projections/glossary/page.css +143 -153
  138. data/lib/hecks/projections/glossary/page.js +63 -3
  139. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  140. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  141. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  142. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  143. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  144. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  145. data/lib/hecks/projections/glossary.rb +2 -1
  146. data/lib/hecks/projections/plain_english.rb +97 -0
  147. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  148. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  149. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  150. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  151. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  152. data/lib/hecks/projections/statements/actions.rb +99 -0
  153. data/lib/hecks/projections/statements/relationships.rb +86 -0
  154. data/lib/hecks/projections/statements.rb +15 -24
  155. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  156. data/lib/hecks/projector/exporter/translations.rb +18 -35
  157. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  158. data/lib/hecks/projector/exporter.rb +5 -1
  159. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  160. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  161. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  162. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  163. data/lib/hecks/runtime/outbox.rb +2 -9
  164. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  165. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  166. data/lib/hecks/runtime/value.rb +13 -0
  167. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  168. data/lib/hecks/rust_build/wasm.rb +0 -12
  169. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  170. data/lib/hecks/rust_format.rb +77 -0
  171. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  172. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  173. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  174. data/lib/hecks/tools/translation_audit.rb +8 -10
  175. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  176. data/lib/hecks/version.rb +1 -1
  177. data/lib/hecks/vocabulary.rb +2 -1
  178. data/lib/hecks.rb +3 -0
  179. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
  180. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
  181. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json} +1 -1
  182. data/rust/Cargo.toml +40 -0
  183. data/rust/build/Cargo.toml +36 -0
  184. data/rust/build/clippy.toml +4 -0
  185. data/rust/build/src/adapters_pass.rs +196 -0
  186. data/rust/build/src/binding_pass.rs +54 -13
  187. data/rust/build/src/build_artifact.rs +23 -5
  188. data/rust/build/src/cargo_sync.rs +69 -26
  189. data/rust/build/src/lineage_pass.rs +101 -87
  190. data/rust/build/src/main.rs +11 -2
  191. data/rust/build/src/pipeline.rs +80 -19
  192. data/rust/build/src/reserved_names.rs +6 -50
  193. data/rust/build/src/resolve.rs +82 -35
  194. data/rust/build/src/rustfmt.rs +100 -0
  195. data/rust/build/src/seams_pass.rs +85 -16
  196. data/rust/build/src/subprocess.rs +30 -6
  197. data/rust/build/src/tmp.rs +5 -2
  198. data/rust/build/src/translation_pass.rs +72 -22
  199. data/rust/build/src/world_pass.rs +109 -0
  200. data/rust/clippy.toml +4 -0
  201. data/rust/codegen/Cargo.toml +36 -0
  202. data/rust/codegen/clippy.toml +4 -0
  203. data/rust/codegen/src/asks.rs +40 -14
  204. data/rust/codegen/src/attr.rs +2 -2
  205. data/rust/codegen/src/bridging.rs +676 -117
  206. data/rust/codegen/src/commands.rs +979 -373
  207. data/rust/codegen/src/constraints.rs +88 -25
  208. data/rust/codegen/src/dependency_planning.rs +128 -34
  209. data/rust/codegen/src/domain_generator.rs +300 -170
  210. data/rust/codegen/src/exemplar.rs +87 -32
  211. data/rust/codegen/src/expr_emitter.rs +54 -6
  212. data/rust/codegen/src/fielded.rs +193 -41
  213. data/rust/codegen/src/hecks_naming.rs +124 -16
  214. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  215. data/rust/codegen/src/json.rs +80 -44
  216. data/rust/codegen/src/json_codec.rs +585 -149
  217. data/rust/codegen/src/literal.rs +104 -138
  218. data/rust/codegen/src/main.rs +145 -52
  219. data/rust/codegen/src/manifest.rs +76 -14
  220. data/rust/codegen/src/mutations.rs +1212 -239
  221. data/rust/codegen/src/naming.rs +111 -60
  222. data/rust/codegen/src/optional_pass.rs +90 -28
  223. data/rust/codegen/src/ports.rs +5 -4
  224. data/rust/codegen/src/prelude.rs +139 -49
  225. data/rust/codegen/src/queries.rs +601 -226
  226. data/rust/codegen/src/reactions.rs +268 -74
  227. data/rust/codegen/src/read_models.rs +606 -205
  228. data/rust/codegen/src/reference_specs.rs +16 -3
  229. data/rust/codegen/src/registry.rs +128 -79
  230. data/rust/codegen/src/reserved_names.rs +6 -50
  231. data/rust/codegen/src/shared.rs +71 -22
  232. data/rust/codegen/src/sidecars.rs +15 -5
  233. data/rust/codegen/src/skip_reason.rs +8 -2
  234. data/rust/codegen/src/types.rs +306 -108
  235. data/rust/host/Cargo.lock +1 -0
  236. data/rust/host/Cargo.toml +40 -4
  237. data/rust/host/HECKS_RELEASE +1 -1
  238. data/rust/host/clippy.toml +4 -0
  239. data/rust/host/src/api.rs +698 -218
  240. data/rust/host/src/auth.rs +491 -214
  241. data/rust/host/src/b64.rs +91 -0
  242. data/rust/host/src/bin/expr_harness.rs +6 -2
  243. data/rust/host/src/bin/lineage_harness.rs +47 -11
  244. data/rust/host/src/bin/mint_harness.rs +70 -37
  245. data/rust/host/src/bin/validate_harness.rs +16 -8
  246. data/rust/host/src/boot.rs +144 -68
  247. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  248. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  249. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  250. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  251. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  252. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  253. data/rust/host/src/checkout.rs +78 -32
  254. data/rust/host/src/clock.rs +8 -0
  255. data/rust/host/src/commerce_ir.rs +333 -95
  256. data/rust/host/src/crypto.rs +33 -0
  257. data/rust/host/src/dispatch.rs +990 -331
  258. data/rust/host/src/expr_json.rs +1021 -273
  259. data/rust/host/src/extension.rs +24 -4
  260. data/rust/host/src/field_hints.rs +11 -7
  261. data/rust/host/src/fuzz_support.rs +85 -13
  262. data/rust/host/src/host.rs +100 -0
  263. data/rust/host/src/http.rs +22 -0
  264. data/rust/host/src/ir.rs +326 -16
  265. data/rust/host/src/journal.rs +277 -176
  266. data/rust/host/src/lambda_client.rs +219 -53
  267. data/rust/host/src/lib.rs +16 -4
  268. data/rust/host/src/log.rs +62 -11
  269. data/rust/host/src/mint.rs +1153 -326
  270. data/rust/host/src/needs.rs +185 -44
  271. data/rust/host/src/payments/keystore.rs +197 -45
  272. data/rust/host/src/payments/roles.rs +39 -7
  273. data/rust/host/src/payments/tests.rs +1427 -282
  274. data/rust/host/src/payments.rs +391 -112
  275. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  276. data/rust/host/src/platform_signin/config.rs +11 -11
  277. data/rust/host/src/platform_signin/cookie.rs +2 -2
  278. data/rust/host/src/platform_signin/extension.rs +2 -1
  279. data/rust/host/src/platform_signin/handler.rs +9 -8
  280. data/rust/host/src/platform_signin/jti.rs +4 -2
  281. data/rust/host/src/platform_signin/jwks.rs +2 -1
  282. data/rust/host/src/platform_signin/jws.rs +2 -37
  283. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  284. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  285. data/rust/host/src/platform_signin/pages.rs +20 -20
  286. data/rust/host/src/platform_signin/session.rs +8 -1
  287. data/rust/host/src/platform_signin/settings.rs +6 -6
  288. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  289. data/rust/host/src/platform_signin/verify.rs +41 -41
  290. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  291. data/rust/host/src/presentation.rs +207 -63
  292. data/rust/host/src/presentation_write.rs +502 -187
  293. data/rust/host/src/query_step.rs +69 -18
  294. data/rust/host/src/rate_limit.rs +541 -142
  295. data/rust/host/src/reference_transform.rs +77 -25
  296. data/rust/host/src/reference_validate.rs +443 -104
  297. data/rust/host/src/resend.rs +190 -55
  298. data/rust/host/src/secrets.rs +116 -21
  299. data/rust/host/src/server.rs +483 -104
  300. data/rust/host/src/storage_shape.rs +88 -21
  301. data/rust/host/src/test_pg.rs +17 -7
  302. data/rust/host/src/ui_schema.rs +495 -207
  303. data/rust/host/src/wasm_runner.rs +21 -6
  304. data/rust/host/src/web/commerce.rs +101 -23
  305. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  306. data/rust/host/src/web/newsletter/template.rs +26 -7
  307. data/rust/host/src/web/newsletter/tests.rs +112 -23
  308. data/rust/host/src/web/newsletter.rs +209 -59
  309. data/rust/host/src/web/newsletter_send.rs +251 -79
  310. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  311. data/rust/host/src/web/registration_receipt.rs +100 -24
  312. data/rust/host/src/web/registrations.rs +1384 -332
  313. data/rust/host/src/web/resend_webhook.rs +138 -80
  314. data/rust/host/src/web.rs +2416 -508
  315. data/rust/lsp/Cargo.toml +36 -0
  316. data/rust/lsp/clippy.toml +4 -0
  317. data/rust/lsp/src/json.rs +29 -22
  318. data/rust/lsp/src/main.rs +81 -31
  319. data/rust/lsp/src/outline.rs +23 -7
  320. data/rust/lsp/src/rpc.rs +17 -4
  321. data/rust/parser/Cargo.toml +36 -0
  322. data/rust/parser/clippy.toml +4 -0
  323. data/rust/parser/src/build/closed_sets.rs +6 -1
  324. data/rust/parser/src/build/identity.rs +4 -3
  325. data/rust/parser/src/build/mod.rs +1 -1
  326. data/rust/parser/src/build/named_pattern.rs +4 -1
  327. data/rust/parser/src/build/naming.rs +2 -80
  328. data/rust/parser/src/build/pattern_subset.rs +6 -8
  329. data/rust/parser/src/build/query_derive.rs +3 -1
  330. data/rust/parser/src/build/query_inference.rs +14 -5
  331. data/rust/parser/src/build/read_model.rs +4 -1
  332. data/rust/parser/src/build/references.rs +4 -3
  333. data/rust/parser/src/canonical.rs +34 -15
  334. data/rust/parser/src/emit.rs +187 -105
  335. data/rust/parser/src/expr/ast_json.rs +134 -42
  336. data/rust/parser/src/expr/evaluator.rs +27 -9
  337. data/rust/parser/src/expr/mod.rs +54 -13
  338. data/rust/parser/src/expr/resolver.rs +299 -92
  339. data/rust/parser/src/ir.rs +35 -3
  340. data/rust/parser/src/keywords.rs +7168 -516
  341. data/rust/parser/src/lex.rs +11 -25
  342. data/rust/parser/src/lib.rs +1 -0
  343. data/rust/parser/src/main.rs +65 -6
  344. data/rust/parser/src/parse/adapter.rs +157 -0
  345. data/rust/parser/src/parse/aggregate.rs +19 -10
  346. data/rust/parser/src/parse/chapter.rs +16 -17
  347. data/rust/parser/src/parse/command.rs +35 -16
  348. data/rust/parser/src/parse/domain_port.rs +4 -7
  349. data/rust/parser/src/parse/entity.rs +21 -11
  350. data/rust/parser/src/parse/hecksagon.rs +10 -4
  351. data/rust/parser/src/parse/lifecycle.rs +9 -5
  352. data/rust/parser/src/parse/mod.rs +22 -91
  353. data/rust/parser/src/parse/needs.rs +21 -12
  354. data/rust/parser/src/parse/policy.rs +2 -2
  355. data/rust/parser/src/parse/process_manager.rs +9 -11
  356. data/rust/parser/src/parse/query.rs +8 -4
  357. data/rust/parser/src/parse/read_model.rs +57 -16
  358. data/rust/parser/src/parse/value_object.rs +7 -2
  359. data/rust/parser/src/parse/world.rs +322 -0
  360. data/rust/parser/src/ruby_value.rs +7 -6
  361. data/rust/src/exemplar/json.rs +4 -4
  362. data/rust/src/exemplar/mutations.rs +18 -0
  363. data/rust/src/exemplar/read_models.rs +1 -10
  364. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  365. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  366. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  367. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  368. data/rust/src/kernel/cli.rs +229 -112
  369. data/rust/src/kernel/dispatch.rs +323 -115
  370. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  371. data/rust/src/kernel/expr.rs +203 -86
  372. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  373. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  374. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  375. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  376. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  377. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  378. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  379. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  380. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  381. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  382. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  383. data/rust/src/kernel/expression_operators/text.rs +120 -27
  384. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  385. data/rust/src/kernel/json.rs +230 -148
  386. data/rust/src/kernel/mod.rs +46 -33
  387. data/rust/src/kernel/named_query.rs +63 -15
  388. data/rust/src/kernel/needs.rs +112 -25
  389. data/rust/src/kernel/orchestrate.rs +980 -581
  390. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  391. data/rust/src/kernel/pattern.rs +107 -34
  392. data/rust/src/kernel/query_comparators.rs +118 -41
  393. data/rust/src/kernel/query_ordering.rs +13 -5
  394. data/rust/src/kernel/read_model.rs +479 -181
  395. data/rust/src/kernel/reference_lookup.rs +122 -26
  396. data/rust/src/kernel/repository.rs +293 -66
  397. data/rust/src/kernel/routing.rs +58 -42
  398. data/rust/src/kernel/ruby_float.rs +139 -0
  399. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  400. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  401. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  402. data/rust/src/kernel/vocab/mod.rs +6 -6
  403. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  404. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  405. data/rust/src/lib.rs +50 -0
  406. data/rust/src/main.rs +4 -1
  407. data/rust/web/Cargo.toml +36 -0
  408. data/rust/web/clippy.toml +4 -0
  409. metadata +82 -15
  410. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  411. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  412. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  413. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  414. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  415. data/lib/hecks/ports/authentication.port +0 -6
  416. data/lib/hecks/ports/authentication.rb +0 -67
  417. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  418. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  419. data/rust/host/src/approval.rs +0 -652
@@ -1,9 +1,9 @@
1
1
  //! Ties the journal and the sandbox together: replays history, then the
2
2
  //! new step, persisting it only if the module accepts it.
3
3
 
4
+ use crate::host::Host;
4
5
  use crate::journal;
5
- use crate::journal::LineageConfig;
6
- use crate::lambda_client::{self, LambdaInvoker};
6
+ use crate::lambda_client::{self};
7
7
  use crate::wasm_runner;
8
8
  use std::path::Path;
9
9
  use tokio::sync::Mutex;
@@ -16,7 +16,7 @@ pub struct Outcome {
16
16
 
17
17
  /// Who a command is dispatched for: the role they state and, when the host knows who they are, the
18
18
  /// actor Governance holds assignments for. Both absent is an unidentified caller.
19
- #[derive(Clone, Copy, Debug, Default, PartialEq)]
19
+ #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
20
20
  pub struct Caller<'a> {
21
21
  pub role: Option<&'a str>,
22
22
  pub actor_id: Option<&'a str>,
@@ -27,7 +27,7 @@ pub struct Caller<'a> {
27
27
  pub const ANONYMOUS_ROLE: &str = "Anonymous";
28
28
 
29
29
  /// What the host does about the role a command declares (`HECKS_ROLE_ENFORCEMENT`).
30
- #[derive(Clone, Copy, Debug, PartialEq)]
30
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
31
31
  pub enum Enforcement {
32
32
  /// A caller with no role is unchecked, as before.
33
33
  Off,
@@ -56,9 +56,12 @@ impl Enforcement {
56
56
 
57
57
  /// The caller the kernel is asked to check: an unidentified caller becomes the anonymous role
58
58
  /// unless enforcement is off.
59
- pub fn effective<'a>(self, caller: Caller<'a>) -> Caller<'a> {
59
+ pub fn effective(self, caller: Caller<'_>) -> Caller<'_> {
60
60
  if self != Self::Off && caller.role.is_none() && caller.actor_id.is_none() {
61
- return Caller { role: Some(ANONYMOUS_ROLE), actor_id: None };
61
+ return Caller {
62
+ role: Some(ANONYMOUS_ROLE),
63
+ actor_id: None,
64
+ };
62
65
  }
63
66
  caller
64
67
  }
@@ -75,9 +78,13 @@ enum Origin {
75
78
 
76
79
  /// Whether the kernel refused the command for the caller's role.
77
80
  fn refused_for_role(result: &serde_json::Value) -> bool {
78
- result.get("refusals").and_then(|r| r.as_array()).is_some_and(|all| {
79
- all.iter().any(|r| r.get("kind").and_then(|k| k.as_str()) == Some("Unauthorized"))
80
- })
81
+ result
82
+ .get("refusals")
83
+ .and_then(|r| r.as_array())
84
+ .is_some_and(|all| {
85
+ all.iter()
86
+ .any(|r| r.get("kind").and_then(|k| k.as_str()) == Some("Unauthorized"))
87
+ })
81
88
  }
82
89
 
83
90
  // Distinguishes a module refusal from a kernel failure to run at all.
@@ -92,9 +99,16 @@ enum KernelAnswer {
92
99
  // refusals doesn't by itself mean the run succeeded.
93
100
  fn classify(result: &serde_json::Value) -> KernelAnswer {
94
101
  if let Some(error) = result.get("error") {
95
- return KernelAnswer::Failed(error.as_str().map(str::to_string).unwrap_or_else(|| error.to_string()));
102
+ return KernelAnswer::Failed(
103
+ error
104
+ .as_str()
105
+ .map_or_else(|| error.to_string(), str::to_string),
106
+ );
96
107
  }
97
- let refused = result.get("refusals").and_then(|r| r.as_array()).is_some_and(|r| !r.is_empty());
108
+ let refused = result
109
+ .get("refusals")
110
+ .and_then(|r| r.as_array())
111
+ .is_some_and(|r| !r.is_empty());
98
112
  if refused {
99
113
  KernelAnswer::Refused
100
114
  } else {
@@ -107,18 +121,26 @@ fn classify(result: &serde_json::Value) -> KernelAnswer {
107
121
  fn parse_kernel_output(output: &str) -> anyhow::Result<serde_json::Value> {
108
122
  let result: serde_json::Value = serde_json::from_str(output)?;
109
123
  match classify(&result) {
110
- KernelAnswer::Failed(message) => anyhow::bail!("the kernel failed before it could run the command: {message}"),
124
+ KernelAnswer::Failed(message) => {
125
+ anyhow::bail!("the kernel failed before it could run the command: {message}")
126
+ }
111
127
  KernelAnswer::Accepted | KernelAnswer::Refused => Ok(result),
112
128
  }
113
129
  }
114
130
 
115
131
  // Kept as one opaque object under the journal's `args` column; the kernel
116
132
  // unwraps `to`/`with` itself, so this needs no journal schema migration.
117
- pub fn routed_invocation(to: serde_json::Value, facts: serde_json::Value) -> anyhow::Result<serde_json::Value> {
133
+ pub fn routed_invocation(
134
+ to: serde_json::Value,
135
+ facts: serde_json::Value,
136
+ ) -> anyhow::Result<serde_json::Value> {
118
137
  if !facts.is_object() {
119
138
  anyhow::bail!("with must be an object of command facts");
120
139
  }
121
- Ok(serde_json::json!({ "to": to, "with": facts }))
140
+ let mut invocation = serde_json::Map::new();
141
+ invocation.insert("to".to_string(), to);
142
+ invocation.insert("with".to_string(), facts);
143
+ Ok(serde_json::Value::Object(invocation))
122
144
  }
123
145
 
124
146
  // No `to` route: used only when a command creates a new aggregate, so
@@ -127,66 +149,52 @@ pub fn facts_invocation(facts: serde_json::Value) -> anyhow::Result<serde_json::
127
149
  if !facts.is_object() {
128
150
  anyhow::bail!("with must be an object of command facts");
129
151
  }
130
- Ok(serde_json::json!({ "with": facts }))
152
+ let mut invocation = serde_json::Map::new();
153
+ invocation.insert("with".to_string(), facts);
154
+ Ok(serde_json::Value::Object(invocation))
131
155
  }
132
156
 
133
157
  // Route-aware entry point; `handle` below stays the compatibility path
134
158
  // for callers still sending the older mixed args shape.
135
- #[allow(clippy::too_many_arguments)]
136
159
  pub async fn handle_routed(
137
- client: &Mutex<Client>,
138
- wasm_path: &Path,
160
+ host: Host<'_>,
139
161
  verb: &str,
140
162
  to: serde_json::Value,
141
163
  facts: serde_json::Value,
142
164
  role: Option<&str>,
143
- config: &LineageConfig,
144
- invoker: &dyn LambdaInvoker,
145
165
  ) -> anyhow::Result<Outcome> {
146
- handle(client, wasm_path, verb, routed_invocation(to, facts)?, role, config, invoker).await
166
+ handle(host, verb, routed_invocation(to, facts)?, role).await
147
167
  }
148
168
 
149
169
  /// `handle_routed` for a caller the host can name.
150
- #[allow(clippy::too_many_arguments)]
151
170
  pub async fn handle_routed_as(
152
- client: &Mutex<Client>,
153
- wasm_path: &Path,
171
+ host: Host<'_>,
154
172
  verb: &str,
155
173
  to: serde_json::Value,
156
174
  facts: serde_json::Value,
157
175
  caller: Caller<'_>,
158
- config: &LineageConfig,
159
- invoker: &dyn LambdaInvoker,
160
176
  ) -> anyhow::Result<Outcome> {
161
- handle_as(client, wasm_path, verb, routed_invocation(to, facts)?, caller, config, invoker).await
177
+ handle_as(host, verb, routed_invocation(to, facts)?, caller).await
162
178
  }
163
179
 
164
180
  // Facts-only counterpart to `handle_routed`.
165
- #[allow(clippy::too_many_arguments)]
166
181
  pub async fn handle_facts(
167
- client: &Mutex<Client>,
168
- wasm_path: &Path,
182
+ host: Host<'_>,
169
183
  verb: &str,
170
184
  facts: serde_json::Value,
171
185
  role: Option<&str>,
172
- config: &LineageConfig,
173
- invoker: &dyn LambdaInvoker,
174
186
  ) -> anyhow::Result<Outcome> {
175
- handle(client, wasm_path, verb, facts_invocation(facts)?, role, config, invoker).await
187
+ handle(host, verb, facts_invocation(facts)?, role).await
176
188
  }
177
189
 
178
190
  /// `handle_facts` for a caller the host can name.
179
- #[allow(clippy::too_many_arguments)]
180
191
  pub async fn handle_facts_as(
181
- client: &Mutex<Client>,
182
- wasm_path: &Path,
192
+ host: Host<'_>,
183
193
  verb: &str,
184
194
  facts: serde_json::Value,
185
195
  caller: Caller<'_>,
186
- config: &LineageConfig,
187
- invoker: &dyn LambdaInvoker,
188
196
  ) -> anyhow::Result<Outcome> {
189
- handle_as(client, wasm_path, verb, facts_invocation(facts)?, caller, config, invoker).await
197
+ handle_as(host, verb, facts_invocation(facts)?, caller).await
190
198
  }
191
199
 
192
200
  // A Mutex, not a bare Arc<Client>: `handle` needs `Client::transaction`,
@@ -205,68 +213,95 @@ pub async fn handle_facts_as(
205
213
  // host-internal dispatch and is unchecked in every enforcement mode. A call that names a role is
206
214
  // still checked against it. Callers outside the host go through the `_as` entry points.
207
215
  pub async fn handle(
208
- client: &Mutex<Client>,
209
- wasm_path: &Path,
216
+ host: Host<'_>,
210
217
  verb: &str,
211
218
  args: serde_json::Value,
212
219
  role: Option<&str>,
213
- config: &LineageConfig,
214
- invoker: &dyn LambdaInvoker,
215
220
  ) -> anyhow::Result<Outcome> {
216
- let origin = if role.is_none() { Origin::HostInternal } else { Origin::External };
217
- dispatch_in(Enforcement::from_env(), origin, client, wasm_path, verb, args, Caller { role, actor_id: None }, config, invoker).await
221
+ let origin = if role.is_none() {
222
+ Origin::HostInternal
223
+ } else {
224
+ Origin::External
225
+ };
226
+ dispatch_in(
227
+ Enforcement::from_env(),
228
+ origin,
229
+ host,
230
+ verb,
231
+ args,
232
+ Caller {
233
+ role,
234
+ actor_id: None,
235
+ },
236
+ )
237
+ .await
218
238
  }
219
239
 
220
240
  /// Dispatches for a caller, applying `HECKS_ROLE_ENFORCEMENT`: unidentified callers are refused (or,
221
241
  /// in shadow, logged as would-be refusals and let through).
222
242
  pub async fn handle_as(
223
- client: &Mutex<Client>,
224
- wasm_path: &Path,
243
+ host: Host<'_>,
225
244
  verb: &str,
226
245
  args: serde_json::Value,
227
246
  caller: Caller<'_>,
228
- config: &LineageConfig,
229
- invoker: &dyn LambdaInvoker,
230
247
  ) -> anyhow::Result<Outcome> {
231
- dispatch_in(Enforcement::from_env(), Origin::External, client, wasm_path, verb, args, caller, config, invoker).await
248
+ dispatch_in(
249
+ Enforcement::from_env(),
250
+ Origin::External,
251
+ host,
252
+ verb,
253
+ args,
254
+ caller,
255
+ )
256
+ .await
232
257
  }
233
258
 
234
- #[allow(clippy::too_many_arguments)]
235
259
  async fn dispatch_in(
236
260
  mode: Enforcement,
237
261
  origin: Origin,
238
- client: &Mutex<Client>,
239
- wasm_path: &Path,
262
+ host: Host<'_>,
240
263
  verb: &str,
241
264
  args: serde_json::Value,
242
265
  caller: Caller<'_>,
243
- config: &LineageConfig,
244
- invoker: &dyn LambdaInvoker,
245
266
  ) -> anyhow::Result<Outcome> {
246
- let checked = if origin == Origin::HostInternal { caller } else { mode.effective(caller) };
247
- let outcome = run(client, wasm_path, verb, args.clone(), checked, config, invoker).await?;
267
+ let checked = if origin == Origin::HostInternal {
268
+ caller
269
+ } else {
270
+ mode.effective(caller)
271
+ };
272
+ let outcome = run(host, verb, args.clone(), checked).await?;
248
273
  // Only a caller that stated no role (unidentified, or an actor Governance holds no grant for)
249
274
  // is let through; a stated role that does not match stays refused, as it is under `Off`.
250
- if mode == Enforcement::Shadow && caller.role.is_none() && !outcome.accepted && refused_for_role(&outcome.result) {
251
- crate::log::error("would_refuse_role", serde_json::json!({
252
- "verb": verb, "role": checked.role, "actor_id": checked.actor_id,
253
- "refusals": outcome.result.get("refusals"),
254
- }));
255
- return run(client, wasm_path, verb, args, Caller::default(), config, invoker).await;
275
+ if mode == Enforcement::Shadow
276
+ && caller.role.is_none()
277
+ && !outcome.accepted
278
+ && refused_for_role(&outcome.result)
279
+ {
280
+ crate::log::error(
281
+ "would_refuse_role",
282
+ serde_json::json!({
283
+ "verb": verb, "role": checked.role, "actor_id": checked.actor_id,
284
+ "refusals": outcome.result.get("refusals"),
285
+ }),
286
+ );
287
+ return run(host, verb, args, Caller::default()).await;
256
288
  }
257
289
  Ok(outcome)
258
290
  }
259
291
 
260
- #[allow(clippy::too_many_arguments)]
261
292
  async fn run(
262
- client: &Mutex<Client>,
263
- wasm_path: &Path,
293
+ host: Host<'_>,
264
294
  verb: &str,
265
295
  args: serde_json::Value,
266
296
  caller: Caller<'_>,
267
- config: &LineageConfig,
268
- invoker: &dyn LambdaInvoker,
269
297
  ) -> anyhow::Result<Outcome> {
298
+ let Host {
299
+ client,
300
+ wasm_path,
301
+ config,
302
+ invoker,
303
+ ..
304
+ } = host;
270
305
  let role = caller.role;
271
306
  let mut guard = client.lock().await;
272
307
  let txn = guard.transaction().await?;
@@ -327,7 +362,9 @@ async fn run(
327
362
  let seed = if needs_saga_backfill {
328
363
  serde_json::json!({})
329
364
  } else {
330
- snapshot.as_ref().map(|s| s.seed.clone()).unwrap_or_else(|| serde_json::json!({}))
365
+ snapshot
366
+ .as_ref()
367
+ .map_or_else(|| serde_json::json!({}), |s| s.seed.clone())
331
368
  };
332
369
 
333
370
  // Read inside the same advisory-locked transaction: the lock covers
@@ -378,7 +415,10 @@ async fn run(
378
415
 
379
416
  // The kernel's "instances" output is already the exact seed
380
417
  // shape, so nothing here re-derives or filters it.
381
- let new_seed = result.get("instances").cloned().unwrap_or_else(|| serde_json::json!({}));
418
+ let new_seed = result
419
+ .get("instances")
420
+ .cloned()
421
+ .unwrap_or_else(|| serde_json::json!({}));
382
422
  journal::save_snapshot(&txn, ordinal, &new_seed).await?;
383
423
 
384
424
  // Persisted in the same transaction, so a saga checkpoint can
@@ -386,24 +426,42 @@ async fn run(
386
426
  // Reconciled against `saga_rows` (pre-run state): present now
387
427
  // is upserted, present before but absent now is deleted.
388
428
  if let Some(saga_snapshot) = result.get("saga_snapshot").and_then(|v| v.as_array()) {
389
- let mut still_present: std::collections::HashSet<(String, String)> = std::collections::HashSet::new();
429
+ let mut still_present: std::collections::HashSet<(String, String)> =
430
+ std::collections::HashSet::new();
390
431
  for entry in saga_snapshot {
391
432
  let process_manager = entry
392
433
  .get("process_manager")
393
434
  .and_then(|v| v.as_str())
394
- .ok_or_else(|| anyhow::anyhow!("saga_snapshot entry missing \"process_manager\": {entry}"))?;
435
+ .ok_or_else(|| {
436
+ anyhow::anyhow!("saga_snapshot entry missing \"process_manager\": {entry}")
437
+ })?;
395
438
  let correlation = entry
396
439
  .get("correlation")
397
440
  .and_then(|v| v.as_str())
398
- .ok_or_else(|| anyhow::anyhow!("saga_snapshot entry missing \"correlation\": {entry}"))?;
399
- let state = entry
400
- .get("state")
401
- .and_then(|v| v.as_str())
402
- .ok_or_else(|| anyhow::anyhow!("saga_snapshot entry missing \"state\": {entry}"))?;
403
- let memory = entry.get("memory").cloned().unwrap_or_else(|| serde_json::json!({}));
404
- let completed_compensations = entry.get("completed_compensations").cloned().unwrap_or_else(|| serde_json::json!([]));
405
-
406
- journal::save_saga(&txn, process_manager, correlation, state, &memory, &completed_compensations).await?;
441
+ .ok_or_else(|| {
442
+ anyhow::anyhow!("saga_snapshot entry missing \"correlation\": {entry}")
443
+ })?;
444
+ let state = entry.get("state").and_then(|v| v.as_str()).ok_or_else(|| {
445
+ anyhow::anyhow!("saga_snapshot entry missing \"state\": {entry}")
446
+ })?;
447
+ let memory = entry
448
+ .get("memory")
449
+ .cloned()
450
+ .unwrap_or_else(|| serde_json::json!({}));
451
+ let completed_compensations = entry
452
+ .get("completed_compensations")
453
+ .cloned()
454
+ .unwrap_or_else(|| serde_json::json!([]));
455
+
456
+ journal::save_saga(
457
+ &txn,
458
+ process_manager,
459
+ correlation,
460
+ state,
461
+ &memory,
462
+ &completed_compensations,
463
+ )
464
+ .await?;
407
465
  still_present.insert((process_manager.to_string(), correlation.to_string()));
408
466
  }
409
467
  for row in &saga_rows {
@@ -434,7 +492,9 @@ async fn run(
434
492
  let aggregate = mutation
435
493
  .get("aggregate")
436
494
  .and_then(|v| v.as_str())
437
- .ok_or_else(|| anyhow::anyhow!("mutation record missing \"aggregate\": {mutation}"))?;
495
+ .ok_or_else(|| {
496
+ anyhow::anyhow!("mutation record missing \"aggregate\": {mutation}")
497
+ })?;
438
498
  // `era.is_some()` means the lineage subsystem exists, not that
439
499
  // this aggregate has a mirror: `mint` only provisions head
440
500
  // snapshots for the capable set, so mirroring anything else
@@ -449,7 +509,9 @@ async fn run(
449
509
  let operation = mutation
450
510
  .get("operation")
451
511
  .and_then(|v| v.as_str())
452
- .ok_or_else(|| anyhow::anyhow!("mutation record missing \"operation\": {mutation}"))?;
512
+ .ok_or_else(|| {
513
+ anyhow::anyhow!("mutation record missing \"operation\": {mutation}")
514
+ })?;
453
515
  let state = mutation
454
516
  .get("state")
455
517
  .ok_or_else(|| anyhow::anyhow!("mutation record missing \"state\": {mutation}"))?;
@@ -457,7 +519,12 @@ async fn run(
457
519
  journal::append_lineage_mutation(
458
520
  &txn,
459
521
  config,
460
- &journal::Mutation { aggregate, id, operation, state },
522
+ &journal::Mutation {
523
+ aggregate,
524
+ id,
525
+ operation,
526
+ state,
527
+ },
461
528
  )
462
529
  .await?;
463
530
  }
@@ -495,7 +562,9 @@ async fn run(
495
562
  match lambda_client::deliver_with_retry(invoker, reaction).await {
496
563
  Ok(record) => {
497
564
  if let Some(response) = result.as_object_mut() {
498
- if let Some(reactions) = response.get_mut("reactions").and_then(|r| r.as_array_mut()) {
565
+ if let Some(reactions) =
566
+ response.get_mut("reactions").and_then(|r| r.as_array_mut())
567
+ {
499
568
  reactions.push(serde_json::json!({
500
569
  "policy": record.policy, "on": reaction.get("on").and_then(|v| v.as_str()).unwrap_or_default(),
501
570
  "trigger": record.target_verb, "delivered": record.delivered, "reason": record.reason,
@@ -506,10 +575,13 @@ async fn run(
506
575
  }
507
576
  Err(failure) => {
508
577
  let error_text = format!("{:#}", failure.error);
509
- crate::log::error("cross_domain_delivery_failed", serde_json::json!({
510
- "policy": failure.policy, "target_domain": failure.target_domain,
511
- "target_verb": failure.target_verb, "attempts": failure.attempts, "error": error_text,
512
- }));
578
+ crate::log::error(
579
+ "cross_domain_delivery_failed",
580
+ serde_json::json!({
581
+ "policy": failure.policy, "target_domain": failure.target_domain,
582
+ "target_verb": failure.target_verb, "attempts": failure.attempts, "error": error_text,
583
+ }),
584
+ );
513
585
  journal::record_dead_letter(
514
586
  &*guard,
515
587
  &failure.policy,
@@ -530,7 +602,10 @@ async fn run(
530
602
  return Err(error);
531
603
  }
532
604
  if let Some(response) = result.as_object_mut() {
533
- response.insert("cross_domain_deliveries".to_string(), serde_json::Value::Array(cross_domain_deliveries));
605
+ response.insert(
606
+ "cross_domain_deliveries".to_string(),
607
+ serde_json::Value::Array(cross_domain_deliveries),
608
+ );
534
609
  }
535
610
 
536
611
  log_command(verb, role, accepted, &result);
@@ -548,11 +623,21 @@ fn log_command(verb: &str, role: Option<&str>, accepted: bool, result: &serde_js
548
623
  }
549
624
  }
550
625
 
551
- fn command_fields(verb: &str, role: Option<&str>, accepted: bool, result: &serde_json::Value) -> serde_json::Value {
626
+ fn command_fields(
627
+ verb: &str,
628
+ role: Option<&str>,
629
+ accepted: bool,
630
+ result: &serde_json::Value,
631
+ ) -> serde_json::Value {
552
632
  let events: Vec<serde_json::Value> = result
553
633
  .get("events")
554
634
  .and_then(|v| v.as_array())
555
- .map(|items| items.iter().filter_map(|item| item.get("name").cloned()).collect())
635
+ .map(|items| {
636
+ items
637
+ .iter()
638
+ .filter_map(|item| item.get("name").cloned())
639
+ .collect()
640
+ })
556
641
  .unwrap_or_default();
557
642
  serde_json::json!({
558
643
  "verb": verb, "role": role, "accepted": accepted, "events": events,
@@ -586,7 +671,8 @@ pub async fn read(client: &Mutex<Client>, wasm_path: &Path) -> anyhow::Result<se
586
671
  let input = serde_json::json!({ "seed": &s.seed, "steps": steps_since }).to_string();
587
672
  let owned_wasm_path = wasm_path.to_path_buf();
588
673
  let output =
589
- tokio::task::spawn_blocking(move || wasm_runner::run(&owned_wasm_path, &input)).await??;
674
+ tokio::task::spawn_blocking(move || wasm_runner::run(&owned_wasm_path, &input))
675
+ .await??;
590
676
  return parse_kernel_output(&output);
591
677
  }
592
678
 
@@ -617,10 +703,15 @@ pub async fn query(
617
703
  crate::needs::fill_query_needs(domain_ir, question, &mut args, &crate::needs::ProcessClock);
618
704
  }
619
705
  let state = read(client, wasm_path).await?;
620
- let seed = state.get("instances").cloned().unwrap_or_else(|| serde_json::json!({}));
621
- let input = serde_json::json!({ "seed": seed, "steps": [{ "query": question, "args": args }] }).to_string();
706
+ let seed = state
707
+ .get("instances")
708
+ .cloned()
709
+ .unwrap_or_else(|| serde_json::json!({}));
710
+ let input = serde_json::json!({ "seed": seed, "steps": [{ "query": question, "args": args }] })
711
+ .to_string();
622
712
  let owned_wasm_path = wasm_path.to_path_buf();
623
- let output = tokio::task::spawn_blocking(move || wasm_runner::run(&owned_wasm_path, &input)).await??;
713
+ let output =
714
+ tokio::task::spawn_blocking(move || wasm_runner::run(&owned_wasm_path, &input)).await??;
624
715
  parse_kernel_output(&output)
625
716
  }
626
717
 
@@ -642,6 +733,8 @@ pub(crate) mod tests {
642
733
  assert!(!fields.to_string().contains("a@b.c"));
643
734
  }
644
735
  use super::*;
736
+ use crate::journal::LineageConfig;
737
+ use crate::lambda_client::LambdaInvoker;
645
738
  use tokio_postgres::NoTls;
646
739
 
647
740
  // A throwaway Postgres database per test, uniquely named so
@@ -663,10 +756,9 @@ pub(crate) mod tests {
663
756
  .await
664
757
  .unwrap();
665
758
 
666
- let (client, conn) =
667
- tokio_postgres::connect(&crate::test_pg::conninfo(&name), NoTls)
668
- .await
669
- .expect("connect to scratch db");
759
+ let (client, conn) = tokio_postgres::connect(&crate::test_pg::conninfo(name), NoTls)
760
+ .await
761
+ .expect("connect to scratch db");
670
762
  tokio::spawn(async move {
671
763
  let _ = conn.await;
672
764
  });
@@ -677,11 +769,18 @@ pub(crate) mod tests {
677
769
  // Not Ruby's real provisioning — just enough structure for
678
770
  // `journal::current_era` and `append_lineage_mutation` to succeed,
679
771
  // without reproducing Ruby's full DDL.
680
- pub(crate) async fn provision_lineage(client: &Client, domain: &str, era: i32, aggregate_storage_names: &[&str]) {
772
+ pub(crate) async fn provision_lineage(
773
+ client: &Client,
774
+ domain: &str,
775
+ era: i32,
776
+ aggregate_storage_names: &[&str],
777
+ ) {
681
778
  // `int`, matching Ruby's real DDL: tokio_postgres requires the
682
779
  // Rust and Postgres types to match, not just be compatible.
683
780
  client
684
- .batch_execute("CREATE TABLE IF NOT EXISTS hecks_eras (domain text, ordinal int, held_text text)")
781
+ .batch_execute(
782
+ "CREATE TABLE IF NOT EXISTS hecks_eras (domain text, ordinal int, held_text text)",
783
+ )
685
784
  .await
686
785
  .unwrap();
687
786
  client
@@ -710,7 +809,10 @@ pub(crate) mod tests {
710
809
  for name in aggregate_storage_names {
711
810
  // Domain-qualified (ADR 0059), matching what a real write
712
811
  // targets.
713
- let snapshot_table = journal::qualified_name(domain, &format!("{}_head_snapshot_{era}", journal::snake(name)));
812
+ let snapshot_table = journal::qualified_name(
813
+ domain,
814
+ &format!("{}_head_snapshot_{era}", journal::snake(name)),
815
+ );
714
816
  client
715
817
  .batch_execute(&format!(
716
818
  "CREATE TABLE IF NOT EXISTS \"{snapshot_table}\" (id text PRIMARY KEY, ordinal bigint NOT NULL, \
@@ -748,15 +850,34 @@ pub(crate) mod tests {
748
850
 
749
851
  // Mirroring everything is the outage: no head snapshot exists to
750
852
  // upsert into.
751
- let everything = LineageConfig { domain: "Banking".to_string(), era: Some(1), mirrored: None };
752
- let refused = handle_facts(&client, &wasm, "Banking::Customer.Register", open.clone(), None, &everything, &invoker).await;
853
+ let everything = LineageConfig {
854
+ domain: "Banking".to_string(),
855
+ era: Some(1),
856
+ mirrored: None,
857
+ };
858
+ let refused = handle_facts(
859
+ Host::new(&client, &wasm, &everything, &invoker),
860
+ "Banking::Customer.Register",
861
+ open.clone(),
862
+ None,
863
+ )
864
+ .await;
753
865
  let message = match refused {
754
866
  Ok(_) => panic!("expected the missing-relation failure"),
755
867
  Err(e) => format!("{e:#}"),
756
868
  };
757
- assert!(message.contains("banking_customer_head_snapshot_1"), "names the relation it could not write: {message}");
758
- assert!(message.contains("does not exist"), "carries the database's own words: {message}");
759
- assert!(message.contains("Banking::Customer"), "names the record: {message}");
869
+ assert!(
870
+ message.contains("banking_customer_head_snapshot_1"),
871
+ "names the relation it could not write: {message}"
872
+ );
873
+ assert!(
874
+ message.contains("does not exist"),
875
+ "carries the database's own words: {message}"
876
+ );
877
+ assert!(
878
+ message.contains("Banking::Customer"),
879
+ "names the record: {message}"
880
+ );
760
881
 
761
882
  // Mirroring what the IR actually declares — nothing — writes
762
883
  // cleanly, into this crate's own journal.
@@ -765,17 +886,38 @@ pub(crate) mod tests {
765
886
  era: Some(1),
766
887
  mirrored: Some(std::collections::BTreeSet::new()),
767
888
  };
768
- let outcome = handle_facts(&client, &wasm, "Banking::Customer.Register", open, None, &declared, &invoker)
769
- .await
770
- .expect("writes");
889
+ let outcome = handle_facts(
890
+ Host::new(&client, &wasm, &declared, &invoker),
891
+ "Banking::Customer.Register",
892
+ open,
893
+ None,
894
+ )
895
+ .await
896
+ .expect("writes");
771
897
  assert!(outcome.accepted, "{}", outcome.result);
772
898
 
773
899
  let guard = client.lock().await;
774
- let journalled: i64 = guard.query_one("SELECT count(*) FROM hecks_lambda_journal", &[]).await.unwrap().get(0);
775
- assert_eq!(journalled, 1, "durable in this crate's own journal, which is what `read` replays");
776
- let mirror: Option<String> =
777
- guard.query_one("SELECT to_regclass('banking_customer_head_snapshot_1')::text", &[]).await.unwrap().get(0);
778
- assert!(mirror.is_none(), "and no mirror was invented for an aggregate the IR never called capable");
900
+ let journalled: i64 = guard
901
+ .query_one("SELECT count(*) FROM hecks_lambda_journal", &[])
902
+ .await
903
+ .unwrap()
904
+ .get(0);
905
+ assert_eq!(
906
+ journalled, 1,
907
+ "durable in this crate's own journal, which is what `read` replays"
908
+ );
909
+ let mirror: Option<String> = guard
910
+ .query_one(
911
+ "SELECT to_regclass('banking_customer_head_snapshot_1')::text",
912
+ &[],
913
+ )
914
+ .await
915
+ .unwrap()
916
+ .get(0);
917
+ assert!(
918
+ mirror.is_none(),
919
+ "and no mirror was invented for an aggregate the IR never called capable"
920
+ );
779
921
  }
780
922
 
781
923
  // The other half: an aggregate the IR does call capable is still
@@ -787,29 +929,38 @@ pub(crate) mod tests {
787
929
  let config = LineageConfig {
788
930
  domain: "Banking".to_string(),
789
931
  era: Some(1),
790
- mirrored: Some(["Banking::Customer".to_string()].into_iter().collect()),
932
+ mirrored: Some(std::iter::once("Banking::Customer".to_string()).collect()),
791
933
  };
792
934
 
793
935
  handle_facts(
794
- &client,
795
- &wasm_path(),
936
+ Host::new(
937
+ &client,
938
+ &wasm_path(),
939
+ &config,
940
+ &crate::lambda_client::NeverInvoker,
941
+ ),
796
942
  "Banking::Customer.Register",
797
943
  register("CUST-2"),
798
944
  None,
799
- &config,
800
- &crate::lambda_client::NeverInvoker,
801
945
  )
802
946
  .await
803
947
  .expect("writes");
804
948
 
805
949
  let guard = client.lock().await;
806
- let mirrored: i64 =
807
- guard.query_one("SELECT count(*) FROM banking_customer_head_snapshot_1", &[]).await.unwrap().get(0);
950
+ let mirrored: i64 = guard
951
+ .query_one("SELECT count(*) FROM banking_customer_head_snapshot_1", &[])
952
+ .await
953
+ .unwrap()
954
+ .get(0);
808
955
  assert_eq!(mirrored, 1);
809
956
  }
810
957
 
811
958
  fn test_config(domain: &str, era: i32) -> LineageConfig {
812
- LineageConfig { domain: domain.to_string(), era: Some(era), mirrored: None }
959
+ LineageConfig {
960
+ domain: domain.to_string(),
961
+ era: Some(era),
962
+ mirrored: None,
963
+ }
813
964
  }
814
965
 
815
966
  pub(crate) fn wasm_path() -> std::path::PathBuf {
@@ -832,28 +983,40 @@ pub(crate) mod tests {
832
983
  provision_lineage(&*client.lock().await, "Banking", 1, &["Customer"]).await;
833
984
  let config = test_config("Banking", 1);
834
985
 
835
- handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0100"), None, &config, &lambda_client::NeverInvoker)
836
- .await
837
- .unwrap()
838
- .accepted
839
- .then_some(())
840
- .expect("registering a customer should succeed");
986
+ handle(
987
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
988
+ "Banking::Customer.Register",
989
+ register("CUST-0100"),
990
+ None,
991
+ )
992
+ .await
993
+ .unwrap()
994
+ .accepted
995
+ .then_some(())
996
+ .expect("registering a customer should succeed");
841
997
 
842
998
  // `Customer.Suspended` is `where status == "suspended"` — a
843
999
  // freshly registered customer is active, so the query has to
844
1000
  // answer with a real, empty row set, not everything.
845
- let before = query(&client, &wasm_path(), "Banking::Customer.Suspended", serde_json::json!({})).await.unwrap();
1001
+ let before = query(
1002
+ &client,
1003
+ &wasm_path(),
1004
+ "Banking::Customer.Suspended",
1005
+ serde_json::json!({}),
1006
+ )
1007
+ .await
1008
+ .unwrap();
846
1009
  assert_eq!(before["queries"][0]["query"], "Banking::Customer.Suspended");
847
- assert_eq!(before["queries"][0]["rows"].as_array().expect("rows").len(), 0);
1010
+ assert_eq!(
1011
+ before["queries"][0]["rows"].as_array().expect("rows").len(),
1012
+ 0
1013
+ );
848
1014
 
849
1015
  handle(
850
- &client,
851
- &wasm_path(),
1016
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
852
1017
  "Banking::Customer.Suspend",
853
1018
  serde_json::json!({ "reference": "CUST-0100", "standing": { "value": "watch" } }),
854
1019
  None,
855
- &config,
856
- &lambda_client::NeverInvoker,
857
1020
  )
858
1021
  .await
859
1022
  .unwrap()
@@ -861,9 +1024,20 @@ pub(crate) mod tests {
861
1024
  .then_some(())
862
1025
  .expect("suspending a customer should succeed");
863
1026
 
864
- let after = query(&client, &wasm_path(), "Banking::Customer.Suspended", serde_json::json!({})).await.unwrap();
1027
+ let after = query(
1028
+ &client,
1029
+ &wasm_path(),
1030
+ "Banking::Customer.Suspended",
1031
+ serde_json::json!({}),
1032
+ )
1033
+ .await
1034
+ .unwrap();
865
1035
  let rows = after["queries"][0]["rows"].as_array().expect("rows");
866
- assert_eq!(rows.len(), 1, "the suspended customer should be the one row: {after}");
1036
+ assert_eq!(
1037
+ rows.len(),
1038
+ 1,
1039
+ "the suspended customer should be the one row: {after}"
1040
+ );
867
1041
  assert_eq!(rows[0]["id"], "CUST-0100");
868
1042
  }
869
1043
 
@@ -873,13 +1047,15 @@ pub(crate) mod tests {
873
1047
  provision_lineage(&*client.lock().await, "Banking", 1, &["Customer"]).await;
874
1048
 
875
1049
  let outcome = handle(
876
- &client,
877
- &wasm_path(),
1050
+ Host::new(
1051
+ &client,
1052
+ &wasm_path(),
1053
+ &test_config("Banking", 1),
1054
+ &lambda_client::NeverInvoker,
1055
+ ),
878
1056
  "Banking::Customer.Register",
879
1057
  register("CUST-0001"),
880
1058
  None,
881
- &test_config("Banking", 1),
882
- &lambda_client::NeverInvoker,
883
1059
  )
884
1060
  .await
885
1061
  .unwrap();
@@ -893,7 +1069,10 @@ pub(crate) mod tests {
893
1069
  // The lineage write itself: a row landed in the era-tagged,
894
1070
  // storage-name-keyed journal shape, not just the flat log above.
895
1071
  let journal_rows = guard
896
- .query("SELECT era, aggregate, aggregate_id, operation FROM hecks_journal_banking", &[])
1072
+ .query(
1073
+ "SELECT era, aggregate, aggregate_id, operation FROM hecks_journal_banking",
1074
+ &[],
1075
+ )
897
1076
  .await
898
1077
  .unwrap();
899
1078
  assert_eq!(journal_rows.len(), 1);
@@ -902,14 +1081,26 @@ pub(crate) mod tests {
902
1081
  let aggregate: String = row.get(1);
903
1082
  let aggregate_id: String = row.get(2);
904
1083
  let operation: String = row.get(3);
905
- assert_eq!((era, aggregate.as_str(), aggregate_id.as_str(), operation.as_str()), (1, "customer", "CUST-0001", "save"));
1084
+ assert_eq!(
1085
+ (
1086
+ era,
1087
+ aggregate.as_str(),
1088
+ aggregate_id.as_str(),
1089
+ operation.as_str()
1090
+ ),
1091
+ (1, "customer", "CUST-0001", "save")
1092
+ );
906
1093
 
907
1094
  // domain-qualified (docs/decisions/0059) — snake("Banking") == "banking".
908
1095
  let snapshot_rows = guard
909
1096
  .query("SELECT id FROM banking_customer_head_snapshot_1", &[])
910
1097
  .await
911
1098
  .unwrap();
912
- assert_eq!(snapshot_rows.len(), 1, "the head-snapshot table should carry exactly the one live record");
1099
+ assert_eq!(
1100
+ snapshot_rows.len(),
1101
+ 1,
1102
+ "the head-snapshot table should carry exactly the one live record"
1103
+ );
913
1104
  let id: String = snapshot_rows[0].get(0);
914
1105
  assert_eq!(id, "CUST-0001");
915
1106
  }
@@ -924,13 +1115,15 @@ pub(crate) mod tests {
924
1115
 
925
1116
  let before = crate::auth::httpdate_now();
926
1117
  let outcome = handle(
927
- &client,
928
- &wasm_path(),
1118
+ Host::new(
1119
+ &client,
1120
+ &wasm_path(),
1121
+ &test_config("Banking", 1),
1122
+ &lambda_client::NeverInvoker,
1123
+ ),
929
1124
  "Banking::Customer.Register",
930
1125
  register("CUST-OCCURRED-AT"),
931
1126
  None,
932
- &test_config("Banking", 1),
933
- &lambda_client::NeverInvoker,
934
1127
  )
935
1128
  .await
936
1129
  .unwrap();
@@ -939,11 +1132,16 @@ pub(crate) mod tests {
939
1132
  assert!(outcome.accepted, "{:?}", outcome.result["refusals"]);
940
1133
  let events = outcome.result["events"].as_array().unwrap();
941
1134
  assert_eq!(events.len(), 1);
942
- let occurred_at = events[0]["occurred_at"].as_str().expect("occurred_at should be a real string, not null or absent");
1135
+ let occurred_at = events[0]["occurred_at"]
1136
+ .as_str()
1137
+ .expect("occurred_at should be a real string, not null or absent");
943
1138
  // Lexical comparison is valid: this format's digit-then-letter
944
1139
  // layout sorts identically to chronological order for stamps
945
1140
  // less than 10,000 years apart.
946
- assert!(occurred_at >= before.as_str() && occurred_at <= after.as_str(), "{occurred_at} should fall between {before} and {after}");
1141
+ assert!(
1142
+ occurred_at >= before.as_str() && occurred_at <= after.as_str(),
1143
+ "{occurred_at} should fall between {before} and {after}"
1144
+ );
947
1145
  }
948
1146
 
949
1147
  #[tokio::test]
@@ -953,13 +1151,10 @@ pub(crate) mod tests {
953
1151
  let config = test_config("Banking", 1);
954
1152
 
955
1153
  let first = handle(
956
- &client,
957
- &wasm_path(),
1154
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
958
1155
  "Banking::Customer.Register",
959
1156
  register("CUST-0001"),
960
1157
  None,
961
- &config,
962
- &lambda_client::NeverInvoker,
963
1158
  )
964
1159
  .await
965
1160
  .unwrap();
@@ -973,13 +1168,10 @@ pub(crate) mod tests {
973
1168
  // succeeded independently: the second registration only refuses
974
1169
  // if the first's effect was genuinely rebuilt from Postgres.
975
1170
  let second = handle(
976
- &client,
977
- &wasm_path(),
1171
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
978
1172
  "Banking::Customer.Register",
979
1173
  register("CUST-0001"),
980
1174
  None,
981
- &config,
982
- &lambda_client::NeverInvoker,
983
1175
  )
984
1176
  .await
985
1177
  .unwrap();
@@ -1007,17 +1199,28 @@ pub(crate) mod tests {
1007
1199
  let config = test_config("Banking", 1);
1008
1200
 
1009
1201
  for reference in ["CUST-0004", "CUST-0005", "CUST-0006"] {
1010
- let outcome = handle(&client, &wasm_path(), "Banking::Customer.Register", register(reference), None, &config, &lambda_client::NeverInvoker)
1011
- .await
1012
- .unwrap();
1013
- assert!(outcome.accepted, "registering {reference} should succeed: {:?}", outcome.result);
1202
+ let outcome = handle(
1203
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
1204
+ "Banking::Customer.Register",
1205
+ register(reference),
1206
+ None,
1207
+ )
1208
+ .await
1209
+ .unwrap();
1210
+ assert!(
1211
+ outcome.accepted,
1212
+ "registering {reference} should succeed: {:?}",
1213
+ outcome.result
1214
+ );
1014
1215
 
1015
1216
  let guard = client.lock().await;
1016
1217
  let snapshot = journal::load_snapshot(&*guard)
1017
1218
  .await
1018
1219
  .unwrap()
1019
1220
  .expect("a snapshot should exist after the first accepted command");
1020
- let tail = journal::load_steps_after(&*guard, snapshot.ordinal).await.unwrap();
1221
+ let tail = journal::load_steps_after(&*guard, snapshot.ordinal)
1222
+ .await
1223
+ .unwrap();
1021
1224
  assert!(tail.is_empty(), "the snapshot should already reflect every accepted command, leaving nothing to replay");
1022
1225
 
1023
1226
  // And the seed is right, not just non-empty: it should carry
@@ -1029,7 +1232,10 @@ pub(crate) mod tests {
1029
1232
  );
1030
1233
  }
1031
1234
 
1032
- let final_snapshot = journal::load_snapshot(&*client.lock().await).await.unwrap().unwrap();
1235
+ let final_snapshot = journal::load_snapshot(&*client.lock().await)
1236
+ .await
1237
+ .unwrap()
1238
+ .unwrap();
1033
1239
  let final_seed = final_snapshot.seed.as_object().unwrap();
1034
1240
  assert_eq!(final_seed.len(), 3, "the snapshot should accumulate all three registrations, not just the latest: {final_seed:?}");
1035
1241
  }
@@ -1037,8 +1243,17 @@ pub(crate) mod tests {
1037
1243
  #[test]
1038
1244
  fn a_top_level_error_is_a_kernel_failure_and_nothing_else_is() {
1039
1245
  let failed = serde_json::json!({"error": "invalid seed: Registration.status: missing from JSON args"});
1040
- assert_eq!(classify(&failed), KernelAnswer::Failed("invalid seed: Registration.status: missing from JSON args".to_string()));
1041
- assert_eq!(classify(&serde_json::json!({"error": {"code": 7}})), KernelAnswer::Failed("{\"code\":7}".to_string()), "a non-text error is still a failure");
1246
+ assert_eq!(
1247
+ classify(&failed),
1248
+ KernelAnswer::Failed(
1249
+ "invalid seed: Registration.status: missing from JSON args".to_string()
1250
+ )
1251
+ );
1252
+ assert_eq!(
1253
+ classify(&serde_json::json!({"error": {"code": 7}})),
1254
+ KernelAnswer::Failed("{\"code\":7}".to_string()),
1255
+ "a non-text error is still a failure"
1256
+ );
1042
1257
 
1043
1258
  let accepted = serde_json::json!({"instances": {}, "events": [], "refusals": []});
1044
1259
  assert_eq!(classify(&accepted), KernelAnswer::Accepted);
@@ -1058,9 +1273,18 @@ pub(crate) mod tests {
1058
1273
 
1059
1274
  #[test]
1060
1275
  fn parse_kernel_output_turns_a_failure_into_an_error_and_keeps_refusals_as_results() {
1061
- let error = parse_kernel_output(r#"{"error":"invalid seed: Customer.status: missing from JSON args"}"#).unwrap_err();
1062
- assert!(format!("{error:#}").contains("invalid seed: Customer.status: missing from JSON args"), "{error:#}");
1063
- let refused = parse_kernel_output(r#"{"instances":{},"refusals":[{"verb":"v","error":"no","kind":"K"}]}"#).unwrap();
1276
+ let error = parse_kernel_output(
1277
+ r#"{"error":"invalid seed: Customer.status: missing from JSON args"}"#,
1278
+ )
1279
+ .unwrap_err();
1280
+ assert!(
1281
+ format!("{error:#}").contains("invalid seed: Customer.status: missing from JSON args"),
1282
+ "{error:#}"
1283
+ );
1284
+ let refused = parse_kernel_output(
1285
+ r#"{"instances":{},"refusals":[{"verb":"v","error":"no","kind":"K"}]}"#,
1286
+ )
1287
+ .unwrap();
1064
1288
  assert_eq!(classify(&refused), KernelAnswer::Refused);
1065
1289
  assert!(parse_kernel_output("not json").is_err());
1066
1290
  }
@@ -1075,55 +1299,153 @@ pub(crate) mod tests {
1075
1299
  let config = test_config("Banking", 1);
1076
1300
  let noinvoke = lambda_client::NeverInvoker;
1077
1301
 
1078
- let first = handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0001"), None, &config, &noinvoke).await.unwrap();
1302
+ let first = handle(
1303
+ Host::new(&client, &wasm_path(), &config, &noinvoke),
1304
+ "Banking::Customer.Register",
1305
+ register("CUST-0001"),
1306
+ None,
1307
+ )
1308
+ .await
1309
+ .unwrap();
1079
1310
  assert!(first.accepted, "{:?}", first.result);
1080
1311
 
1081
- let whole_world = journal::load_snapshot(&*client.lock().await).await.unwrap().unwrap().seed;
1312
+ let whole_world = journal::load_snapshot(&*client.lock().await)
1313
+ .await
1314
+ .unwrap()
1315
+ .unwrap()
1316
+ .seed;
1082
1317
  let mut damaged = whole_world.clone();
1083
- let customer = damaged.as_object_mut().unwrap().values_mut().next().unwrap().as_object_mut().unwrap();
1084
- assert!(customer.remove("status").is_some(), "the fixture Customer carries a status");
1085
- client.lock().await.execute("UPDATE hecks_lambda_snapshot SET seed = $1", &[&damaged]).await.unwrap();
1318
+ let customer = damaged
1319
+ .as_object_mut()
1320
+ .unwrap()
1321
+ .values_mut()
1322
+ .next()
1323
+ .unwrap()
1324
+ .as_object_mut()
1325
+ .unwrap();
1326
+ assert!(
1327
+ customer.remove("status").is_some(),
1328
+ "the fixture Customer carries a status"
1329
+ );
1330
+ client
1331
+ .lock()
1332
+ .await
1333
+ .execute("UPDATE hecks_lambda_snapshot SET seed = $1", &[&damaged])
1334
+ .await
1335
+ .unwrap();
1086
1336
 
1087
1337
  let before = snapshot_and_journal(&client).await;
1088
- let failure = handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0002"), None, &config, &noinvoke).await;
1089
- let message = format!("{:#}", failure.err().expect("a kernel failure must be an error, not an accepted outcome"));
1090
- assert!(message.contains("invalid seed") && message.contains("status"), "{message}");
1338
+ let failure = handle(
1339
+ Host::new(&client, &wasm_path(), &config, &noinvoke),
1340
+ "Banking::Customer.Register",
1341
+ register("CUST-0002"),
1342
+ None,
1343
+ )
1344
+ .await;
1345
+ let message = format!(
1346
+ "{:#}",
1347
+ failure
1348
+ .err()
1349
+ .expect("a kernel failure must be an error, not an accepted outcome")
1350
+ );
1351
+ assert!(
1352
+ message.contains("invalid seed") && message.contains("status"),
1353
+ "{message}"
1354
+ );
1091
1355
 
1092
- assert_eq!(snapshot_and_journal(&client).await, before, "the failed command left the journal and the snapshot byte-identical");
1356
+ assert_eq!(
1357
+ snapshot_and_journal(&client).await,
1358
+ before,
1359
+ "the failed command left the journal and the snapshot byte-identical"
1360
+ );
1093
1361
  assert_eq!(before.2, 1, "and only the first command was ever journaled");
1094
- let lineage_rows: i64 = client.lock().await.query_one("SELECT count(*) FROM hecks_journal_banking", &[]).await.unwrap().get(0);
1095
- assert_eq!(lineage_rows, 1, "nothing was mirrored into the era journal either");
1362
+ let lineage_rows: i64 = client
1363
+ .lock()
1364
+ .await
1365
+ .query_one("SELECT count(*) FROM hecks_journal_banking", &[])
1366
+ .await
1367
+ .unwrap()
1368
+ .get(0);
1369
+ assert_eq!(
1370
+ lineage_rows, 1,
1371
+ "nothing was mirrored into the era journal either"
1372
+ );
1096
1373
 
1097
1374
  // A read that must run the kernel over the damaged snapshot fails
1098
1375
  // the same way, instead of answering an empty world.
1099
1376
  client
1100
1377
  .lock()
1101
1378
  .await
1102
- .execute("INSERT INTO hecks_lambda_journal (verb, args) VALUES ($1, $2)", &[&"Banking::Customer.Register", &register("CUST-0009")])
1379
+ .execute(
1380
+ "INSERT INTO hecks_lambda_journal (verb, args) VALUES ($1, $2)",
1381
+ &[&"Banking::Customer.Register", &register("CUST-0009")],
1382
+ )
1103
1383
  .await
1104
1384
  .unwrap();
1105
- let read_failure = read(&client, &wasm_path()).await.err().expect("a kernel failure is not an empty read");
1106
- assert!(format!("{read_failure:#}").contains("invalid seed"), "{read_failure:#}");
1107
- let query_failure = query(&client, &wasm_path(), "Banking::Customer.Suspended", serde_json::json!({})).await.err();
1385
+ let read_failure = read(&client, &wasm_path())
1386
+ .await
1387
+ .expect_err("a kernel failure is not an empty read");
1388
+ assert!(
1389
+ format!("{read_failure:#}").contains("invalid seed"),
1390
+ "{read_failure:#}"
1391
+ );
1392
+ let query_failure = query(
1393
+ &client,
1394
+ &wasm_path(),
1395
+ "Banking::Customer.Suspended",
1396
+ serde_json::json!({}),
1397
+ )
1398
+ .await
1399
+ .err();
1108
1400
  assert!(query_failure.is_some_and(|e| format!("{e:#}").contains("invalid seed")));
1109
1401
  client.lock().await.execute("DELETE FROM hecks_lambda_journal WHERE verb = 'Banking::Customer.Register' AND ordinal > 1", &[]).await.unwrap();
1110
1402
 
1111
1403
  // Once the snapshot is whole again the next command works, and the
1112
1404
  // snapshot afterwards holds the whole world, not just its own instance.
1113
- client.lock().await.execute("UPDATE hecks_lambda_snapshot SET seed = $1", &[&whole_world]).await.unwrap();
1114
- let second = handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0002"), None, &config, &noinvoke).await.unwrap();
1405
+ client
1406
+ .lock()
1407
+ .await
1408
+ .execute(
1409
+ "UPDATE hecks_lambda_snapshot SET seed = $1",
1410
+ &[&whole_world],
1411
+ )
1412
+ .await
1413
+ .unwrap();
1414
+ let second = handle(
1415
+ Host::new(&client, &wasm_path(), &config, &noinvoke),
1416
+ "Banking::Customer.Register",
1417
+ register("CUST-0002"),
1418
+ None,
1419
+ )
1420
+ .await
1421
+ .unwrap();
1115
1422
  assert!(second.accepted, "{:?}", second.result);
1116
- let after = journal::load_snapshot(&*client.lock().await).await.unwrap().unwrap().seed;
1423
+ let after = journal::load_snapshot(&*client.lock().await)
1424
+ .await
1425
+ .unwrap()
1426
+ .unwrap()
1427
+ .seed;
1117
1428
  let keys: Vec<&String> = after.as_object().unwrap().keys().collect();
1118
1429
  assert_eq!(keys.len(), 2, "the snapshot holds both customers: {keys:?}");
1119
- assert!(keys.iter().any(|k| k.contains("CUST-0001")) && keys.iter().any(|k| k.contains("CUST-0002")), "{keys:?}");
1430
+ assert!(
1431
+ keys.iter().any(|k| k.contains("CUST-0001"))
1432
+ && keys.iter().any(|k| k.contains("CUST-0002")),
1433
+ "{keys:?}"
1434
+ );
1120
1435
  }
1121
1436
 
1122
1437
  // The snapshot's ordinal and exact seed text, and how many commands are journaled.
1123
1438
  async fn snapshot_and_journal(client: &Mutex<Client>) -> (i64, String, i64) {
1124
1439
  let guard = client.lock().await;
1125
- let snapshot = guard.query_one("SELECT ordinal, seed::text FROM hecks_lambda_snapshot", &[]).await.unwrap();
1126
- let journaled: i64 = guard.query_one("SELECT count(*) FROM hecks_lambda_journal", &[]).await.unwrap().get(0);
1440
+ let snapshot = guard
1441
+ .query_one("SELECT ordinal, seed::text FROM hecks_lambda_snapshot", &[])
1442
+ .await
1443
+ .unwrap();
1444
+ let journaled: i64 = guard
1445
+ .query_one("SELECT count(*) FROM hecks_lambda_journal", &[])
1446
+ .await
1447
+ .unwrap()
1448
+ .get(0);
1127
1449
  (snapshot.get(0), snapshot.get(1), journaled)
1128
1450
  }
1129
1451
 
@@ -1141,8 +1463,18 @@ pub(crate) mod tests {
1141
1463
  let wasm_path = wasm_path();
1142
1464
 
1143
1465
  let (first, second) = tokio::join!(
1144
- handle(&client, &wasm_path, "Banking::Customer.Register", register("CUST-0002"), None, &config, &lambda_client::NeverInvoker),
1145
- handle(&client, &wasm_path, "Banking::Customer.Register", register("CUST-0002"), None, &config, &lambda_client::NeverInvoker),
1466
+ handle(
1467
+ Host::new(&client, &wasm_path, &config, &lambda_client::NeverInvoker),
1468
+ "Banking::Customer.Register",
1469
+ register("CUST-0002"),
1470
+ None
1471
+ ),
1472
+ handle(
1473
+ Host::new(&client, &wasm_path, &config, &lambda_client::NeverInvoker),
1474
+ "Banking::Customer.Register",
1475
+ register("CUST-0002"),
1476
+ None
1477
+ ),
1146
1478
  );
1147
1479
  let (first, second) = (first.unwrap(), second.unwrap());
1148
1480
 
@@ -1152,7 +1484,11 @@ pub(crate) mod tests {
1152
1484
  first.result, second.result
1153
1485
  );
1154
1486
  let steps = journal::load_steps(&*client.lock().await).await.unwrap();
1155
- assert_eq!(steps.len(), 1, "only the accepted registration should be persisted");
1487
+ assert_eq!(
1488
+ steps.len(),
1489
+ 1,
1490
+ "only the accepted registration should be persisted"
1491
+ );
1156
1492
  }
1157
1493
 
1158
1494
  #[tokio::test]
@@ -1161,13 +1497,15 @@ pub(crate) mod tests {
1161
1497
  provision_lineage(&*client.lock().await, "Banking", 1, &["Customer"]).await;
1162
1498
 
1163
1499
  handle(
1164
- &client,
1165
- &wasm_path(),
1500
+ Host::new(
1501
+ &client,
1502
+ &wasm_path(),
1503
+ &test_config("Banking", 1),
1504
+ &lambda_client::NeverInvoker,
1505
+ ),
1166
1506
  "Banking::Customer.Register",
1167
1507
  register("CUST-0003"),
1168
1508
  None,
1169
- &test_config("Banking", 1),
1170
- &lambda_client::NeverInvoker,
1171
1509
  )
1172
1510
  .await
1173
1511
  .unwrap()
@@ -1202,31 +1540,54 @@ pub(crate) mod tests {
1202
1540
 
1203
1541
  impl RecordingInvoker {
1204
1542
  fn new() -> Self {
1205
- Self { calls: std::sync::Mutex::new(Vec::new()) }
1543
+ Self {
1544
+ calls: std::sync::Mutex::new(Vec::new()),
1545
+ }
1206
1546
  }
1207
1547
  }
1208
1548
 
1209
1549
  #[async_trait::async_trait]
1210
1550
  impl LambdaInvoker for RecordingInvoker {
1211
- async fn invoke(&self, function_name: &str, payload: &str) -> anyhow::Result<lambda_client::InvokeOutcome> {
1212
- self.calls.lock().unwrap().push((function_name.to_string(), payload.to_string()));
1213
- Ok(lambda_client::InvokeOutcome { body: serde_json::json!({ "refusals": [] }), function_error: false })
1551
+ async fn invoke(
1552
+ &self,
1553
+ function_name: &str,
1554
+ payload: &str,
1555
+ ) -> anyhow::Result<lambda_client::InvokeOutcome> {
1556
+ self.calls
1557
+ .lock()
1558
+ .unwrap()
1559
+ .push((function_name.to_string(), payload.to_string()));
1560
+ Ok(lambda_client::InvokeOutcome {
1561
+ body: serde_json::json!({ "refusals": [] }),
1562
+ function_error: false,
1563
+ })
1214
1564
  }
1215
1565
  }
1216
1566
 
1217
1567
  #[tokio::test]
1218
1568
  async fn a_cross_domain_policy_delivers_through_the_real_rehydrate_replay_path() {
1219
1569
  let client = scratch_db("rust_host_dispatch_test_6").await;
1220
- provision_lineage(&*client.lock().await, "Banking", 1, &["Customer", "Account"]).await;
1570
+ provision_lineage(
1571
+ &*client.lock().await,
1572
+ "Banking",
1573
+ 1,
1574
+ &["Customer", "Account"],
1575
+ )
1576
+ .await;
1221
1577
  let config = test_config("Banking", 1);
1222
1578
  let invoker = RecordingInvoker::new();
1223
1579
 
1224
- handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0007"), None, &config, &invoker)
1225
- .await
1226
- .unwrap()
1227
- .accepted
1228
- .then_some(())
1229
- .expect("registration should succeed");
1580
+ handle(
1581
+ Host::new(&client, &wasm_path(), &config, &invoker),
1582
+ "Banking::Customer.Register",
1583
+ register("CUST-0007"),
1584
+ None,
1585
+ )
1586
+ .await
1587
+ .unwrap()
1588
+ .accepted
1589
+ .then_some(())
1590
+ .expect("registration should succeed");
1230
1591
 
1231
1592
  let open_args = serde_json::json!({
1232
1593
  "number": { "value": "acct-freeze-me" },
@@ -1234,24 +1595,44 @@ pub(crate) mod tests {
1234
1595
  "daily_limit": { "cents": 50000 },
1235
1596
  "customer": "CUST-0007",
1236
1597
  });
1237
- handle(&client, &wasm_path(), "Banking::Account.Open", open_args, None, &config, &invoker)
1238
- .await
1239
- .unwrap()
1240
- .accepted
1241
- .then_some(())
1242
- .expect("account open should succeed");
1598
+ handle(
1599
+ Host::new(&client, &wasm_path(), &config, &invoker),
1600
+ "Banking::Account.Open",
1601
+ open_args,
1602
+ None,
1603
+ )
1604
+ .await
1605
+ .unwrap()
1606
+ .accepted
1607
+ .then_some(())
1608
+ .expect("account open should succeed");
1243
1609
 
1244
1610
  // `FreezeAccount` announces `AccountFrozen`, matched by
1245
1611
  // `ReviewOnFreeze` (`across "Compliance"`) — the real trigger
1246
1612
  // this feature exists for.
1247
1613
  let freeze_args = serde_json::json!({ "number": { "value": "acct-freeze-me" } });
1248
- let outcome = handle(&client, &wasm_path(), "Banking::Account.FreezeAccount", freeze_args, None, &config, &invoker)
1249
- .await
1250
- .unwrap();
1251
- assert!(outcome.accepted, "freeze should succeed: {:?}", outcome.result);
1614
+ let outcome = handle(
1615
+ Host::new(&client, &wasm_path(), &config, &invoker),
1616
+ "Banking::Account.FreezeAccount",
1617
+ freeze_args,
1618
+ None,
1619
+ )
1620
+ .await
1621
+ .unwrap();
1622
+ assert!(
1623
+ outcome.accepted,
1624
+ "freeze should succeed: {:?}",
1625
+ outcome.result
1626
+ );
1252
1627
 
1253
- let deliveries = outcome.result["cross_domain_deliveries"].as_array().unwrap();
1254
- assert_eq!(deliveries.len(), 1, "exactly one cross-domain reaction should have fired: {deliveries:?}");
1628
+ let deliveries = outcome.result["cross_domain_deliveries"]
1629
+ .as_array()
1630
+ .unwrap();
1631
+ assert_eq!(
1632
+ deliveries.len(),
1633
+ 1,
1634
+ "exactly one cross-domain reaction should have fired: {deliveries:?}"
1635
+ );
1255
1636
  assert_eq!(deliveries[0]["policy"], "ReviewOnFreeze");
1256
1637
  assert_eq!(deliveries[0]["target_domain"], "Compliance");
1257
1638
  assert_eq!(deliveries[0]["delivered"], true);
@@ -1260,7 +1641,10 @@ pub(crate) mod tests {
1260
1641
  // ({policy, on, trigger, delivered, reason}), alongside the
1261
1642
  // differently-shaped "cross_domain_deliveries" entry above.
1262
1643
  let reactions = outcome.result["reactions"].as_array().unwrap();
1263
- let merged = reactions.iter().find(|r| r["policy"] == "ReviewOnFreeze").expect("ReviewOnFreeze should appear in \"reactions\" too");
1644
+ let merged = reactions
1645
+ .iter()
1646
+ .find(|r| r["policy"] == "ReviewOnFreeze")
1647
+ .expect("ReviewOnFreeze should appear in \"reactions\" too");
1264
1648
  assert_eq!(merged["on"], "AccountFrozen");
1265
1649
  assert_eq!(merged["trigger"], "Compliance::AccountFreezeReview.Open");
1266
1650
  assert_eq!(merged["delivered"], true);
@@ -1281,10 +1665,19 @@ pub(crate) mod tests {
1281
1665
  // Never re-delivered on a later replay: `pending_cross_domain`
1282
1666
  // only ever reads the last step's own reactions, so rehydrating
1283
1667
  // history here doesn't re-invoke the sibling Lambda.
1284
- handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0008"), None, &config, &invoker)
1285
- .await
1286
- .unwrap();
1287
- assert_eq!(invoker.calls.lock().unwrap().len(), 1, "replaying prior history must not re-deliver its cross-domain reaction");
1668
+ handle(
1669
+ Host::new(&client, &wasm_path(), &config, &invoker),
1670
+ "Banking::Customer.Register",
1671
+ register("CUST-0008"),
1672
+ None,
1673
+ )
1674
+ .await
1675
+ .unwrap();
1676
+ assert_eq!(
1677
+ invoker.calls.lock().unwrap().len(),
1678
+ 1,
1679
+ "replaying prior history must not re-deliver its cross-domain reaction"
1680
+ );
1288
1681
  }
1289
1682
 
1290
1683
  // Pins the fix: this is the first test that reaches `check_role`
@@ -1301,13 +1694,10 @@ pub(crate) mod tests {
1301
1694
  // The correct role succeeds, same as every other registration,
1302
1695
  // just now with a caller actually bound and matching.
1303
1696
  let matching = handle(
1304
- &client,
1305
- &wasm_path(),
1697
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
1306
1698
  "Banking::Customer.Register",
1307
1699
  register("CUST-0009"),
1308
1700
  Some("Branch clerk"),
1309
- &config,
1310
- &lambda_client::NeverInvoker,
1311
1701
  )
1312
1702
  .await
1313
1703
  .unwrap();
@@ -1321,13 +1711,10 @@ pub(crate) mod tests {
1321
1711
  // caller stating the wrong role must be refused, not silently
1322
1712
  // admitted via the unchecked no-role path.
1323
1713
  let mismatched = handle(
1324
- &client,
1325
- &wasm_path(),
1714
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
1326
1715
  "Banking::Customer.Register",
1327
1716
  register("CUST-0010"),
1328
1717
  Some("Teller"),
1329
- &config,
1330
- &lambda_client::NeverInvoker,
1331
1718
  )
1332
1719
  .await
1333
1720
  .unwrap();
@@ -1348,7 +1735,11 @@ pub(crate) mod tests {
1348
1735
  // And the refused command was never persisted — same discipline
1349
1736
  // every other refusal in this file is held to.
1350
1737
  let steps = journal::load_steps(&*client.lock().await).await.unwrap();
1351
- assert_eq!(steps.len(), 1, "only the correctly-authorized registration should be persisted");
1738
+ assert_eq!(
1739
+ steps.len(),
1740
+ 1,
1741
+ "only the correctly-authorized registration should be persisted"
1742
+ );
1352
1743
  }
1353
1744
 
1354
1745
  #[test]
@@ -1366,16 +1757,27 @@ pub(crate) mod tests {
1366
1757
  assert_eq!(Enforcement::Off.effective(nobody), nobody);
1367
1758
  for mode in [Enforcement::Shadow, Enforcement::Enforce] {
1368
1759
  assert_eq!(mode.effective(nobody).role, Some(ANONYMOUS_ROLE));
1369
- let stated = Caller { role: Some("Teller"), actor_id: None };
1760
+ let stated = Caller {
1761
+ role: Some("Teller"),
1762
+ actor_id: None,
1763
+ };
1370
1764
  assert_eq!(mode.effective(stated), stated, "a stated role is kept");
1371
- let identified = Caller { role: None, actor_id: Some("u1") };
1372
- assert_eq!(mode.effective(identified), identified, "an identified caller is kept");
1765
+ let identified = Caller {
1766
+ role: None,
1767
+ actor_id: Some("u1"),
1768
+ };
1769
+ assert_eq!(
1770
+ mode.effective(identified),
1771
+ identified,
1772
+ "an identified caller is kept"
1773
+ );
1373
1774
  }
1374
1775
  }
1375
1776
 
1376
1777
  // The unidentified caller is the case the host never refused: no role at all.
1377
1778
  #[tokio::test]
1378
- async fn an_unidentified_caller_is_refused_when_enforced_and_let_through_when_off_or_shadowed() {
1779
+ async fn an_unidentified_caller_is_refused_when_enforced_and_let_through_when_off_or_shadowed()
1780
+ {
1379
1781
  let client = scratch_db("rust_host_dispatch_test_enforce").await;
1380
1782
  provision_lineage(&*client.lock().await, "Banking", 1, &["Customer"]).await;
1381
1783
  let config = test_config("Banking", 1);
@@ -1383,26 +1785,52 @@ pub(crate) mod tests {
1383
1785
  let client = &client;
1384
1786
  let config = &config;
1385
1787
  async move {
1386
- dispatch_in(mode, Origin::External, client, &wasm_path(), "Banking::Customer.Register", register(id), Caller::default(), config, &lambda_client::NeverInvoker)
1387
- .await
1388
- .unwrap()
1788
+ dispatch_in(
1789
+ mode,
1790
+ Origin::External,
1791
+ Host::new(client, &wasm_path(), config, &lambda_client::NeverInvoker),
1792
+ "Banking::Customer.Register",
1793
+ register(id),
1794
+ Caller::default(),
1795
+ )
1796
+ .await
1797
+ .unwrap()
1389
1798
  }
1390
1799
  };
1391
1800
 
1392
1801
  let off = go(Enforcement::Off, "CUST-0021").await;
1393
- assert!(off.accepted, "off keeps the old behaviour: {:?}", off.result);
1802
+ assert!(
1803
+ off.accepted,
1804
+ "off keeps the old behaviour: {:?}",
1805
+ off.result
1806
+ );
1394
1807
 
1395
1808
  let enforced = go(Enforcement::Enforce, "CUST-0022").await;
1396
- assert!(!enforced.accepted, "an unidentified caller must be refused: {:?}", enforced.result);
1809
+ assert!(
1810
+ !enforced.accepted,
1811
+ "an unidentified caller must be refused: {:?}",
1812
+ enforced.result
1813
+ );
1397
1814
  assert!(refused_for_role(&enforced.result), "{:?}", enforced.result);
1398
1815
  let error = enforced.result["refusals"][0]["error"].as_str().unwrap();
1399
- assert!(error.contains("Branch clerk") && error.contains(ANONYMOUS_ROLE), "{error}");
1816
+ assert!(
1817
+ error.contains("Branch clerk") && error.contains(ANONYMOUS_ROLE),
1818
+ "{error}"
1819
+ );
1400
1820
 
1401
1821
  let shadowed = go(Enforcement::Shadow, "CUST-0023").await;
1402
- assert!(shadowed.accepted, "shadow lets the command through: {:?}", shadowed.result);
1822
+ assert!(
1823
+ shadowed.accepted,
1824
+ "shadow lets the command through: {:?}",
1825
+ shadowed.result
1826
+ );
1403
1827
 
1404
1828
  let steps = journal::load_steps(&*client.lock().await).await.unwrap();
1405
- assert_eq!(steps.len(), 2, "the enforced refusal was not persisted; off and shadow were");
1829
+ assert_eq!(
1830
+ steps.len(),
1831
+ 2,
1832
+ "the enforced refusal was not persisted; off and shadow were"
1833
+ );
1406
1834
  }
1407
1835
 
1408
1836
  // Shadow may only let through what Off would: a caller that stated a wrong role is refused.
@@ -1415,24 +1843,60 @@ pub(crate) mod tests {
1415
1843
  let client = &client;
1416
1844
  let config = &config;
1417
1845
  async move {
1418
- dispatch_in(mode, origin, client, &wasm_path(), "Banking::Customer.Register", register(id), caller, config, &lambda_client::NeverInvoker)
1419
- .await
1420
- .unwrap()
1846
+ dispatch_in(
1847
+ mode,
1848
+ origin,
1849
+ Host::new(client, &wasm_path(), config, &lambda_client::NeverInvoker),
1850
+ "Banking::Customer.Register",
1851
+ register(id),
1852
+ caller,
1853
+ )
1854
+ .await
1855
+ .unwrap()
1421
1856
  }
1422
1857
  };
1423
- let wrong = Caller { role: Some("Auditor"), actor_id: None };
1858
+ let wrong = Caller {
1859
+ role: Some("Auditor"),
1860
+ actor_id: None,
1861
+ };
1424
1862
 
1425
1863
  let shadow_wrong = go(Enforcement::Shadow, Origin::External, wrong, "CUST-0031").await;
1426
- assert!(!shadow_wrong.accepted, "shadow must not accept a stated wrong role: {:?}", shadow_wrong.result);
1427
- assert!(refused_for_role(&shadow_wrong.result), "{:?}", shadow_wrong.result);
1864
+ assert!(
1865
+ !shadow_wrong.accepted,
1866
+ "shadow must not accept a stated wrong role: {:?}",
1867
+ shadow_wrong.result
1868
+ );
1869
+ assert!(
1870
+ refused_for_role(&shadow_wrong.result),
1871
+ "{:?}",
1872
+ shadow_wrong.result
1873
+ );
1428
1874
  let off_wrong = go(Enforcement::Off, Origin::External, wrong, "CUST-0032").await;
1429
- assert!(!off_wrong.accepted, "off refuses it too: {:?}", off_wrong.result);
1875
+ assert!(
1876
+ !off_wrong.accepted,
1877
+ "off refuses it too: {:?}",
1878
+ off_wrong.result
1879
+ );
1430
1880
 
1431
- let nobody = go(Enforcement::Shadow, Origin::External, Caller::default(), "CUST-0033").await;
1432
- assert!(nobody.accepted, "shadow lets an unidentified caller through: {:?}", nobody.result);
1881
+ let nobody = go(
1882
+ Enforcement::Shadow,
1883
+ Origin::External,
1884
+ Caller::default(),
1885
+ "CUST-0033",
1886
+ )
1887
+ .await;
1888
+ assert!(
1889
+ nobody.accepted,
1890
+ "shadow lets an unidentified caller through: {:?}",
1891
+ nobody.result
1892
+ );
1433
1893
 
1434
1894
  let steps = journal::load_steps(&*client.lock().await).await.unwrap();
1435
- assert_eq!(steps.len(), 1, "only the unidentified caller's registration was persisted");
1895
+ assert_eq!(
1896
+ steps.len(),
1897
+ 1,
1898
+ "only the unidentified caller's registration was persisted"
1899
+ );
1436
1900
  }
1437
1901
 
1438
1902
  // A host-internal dispatch names no caller and is unchecked in every mode, while an external
@@ -1446,15 +1910,26 @@ pub(crate) mod tests {
1446
1910
  let client = &client;
1447
1911
  let config = &config;
1448
1912
  async move {
1449
- dispatch_in(mode, origin, client, &wasm_path(), "Banking::Customer.Register", register(id), Caller::default(), config, &lambda_client::NeverInvoker)
1450
- .await
1451
- .unwrap()
1913
+ dispatch_in(
1914
+ mode,
1915
+ origin,
1916
+ Host::new(client, &wasm_path(), config, &lambda_client::NeverInvoker),
1917
+ "Banking::Customer.Register",
1918
+ register(id),
1919
+ Caller::default(),
1920
+ )
1921
+ .await
1922
+ .unwrap()
1452
1923
  }
1453
1924
  };
1454
1925
 
1455
1926
  let external = go(Enforcement::Enforce, Origin::External, "CUST-0041").await;
1456
1927
  assert!(!external.accepted, "{:?}", external.result);
1457
- for (mode, id) in [(Enforcement::Enforce, "CUST-0042"), (Enforcement::Shadow, "CUST-0043"), (Enforcement::Off, "CUST-0044")] {
1928
+ for (mode, id) in [
1929
+ (Enforcement::Enforce, "CUST-0042"),
1930
+ (Enforcement::Shadow, "CUST-0043"),
1931
+ (Enforcement::Off, "CUST-0044"),
1932
+ ] {
1458
1933
  let internal = go(mode, Origin::HostInternal, id).await;
1459
1934
  assert!(internal.accepted, "{mode:?}: {:?}", internal.result);
1460
1935
  }
@@ -1466,29 +1941,58 @@ pub(crate) mod tests {
1466
1941
  // after `begin_saga` already wrote into `hecks_lambda_sagas`,
1467
1942
  // proving that write rolls back with the rest of the transaction.
1468
1943
  let client = scratch_db("rust_host_dispatch_test_9").await;
1469
- provision_lineage(&*client.lock().await, "Banking", 1, &["Customer", "Account"]).await;
1944
+ provision_lineage(
1945
+ &*client.lock().await,
1946
+ "Banking",
1947
+ 1,
1948
+ &["Customer", "Account"],
1949
+ )
1950
+ .await;
1470
1951
  let config = test_config("Banking", 1);
1471
1952
 
1472
- handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0012"), None, &config, &lambda_client::NeverInvoker)
1473
- .await.unwrap().accepted.then_some(()).expect("registration should succeed");
1953
+ handle(
1954
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
1955
+ "Banking::Customer.Register",
1956
+ register("CUST-0012"),
1957
+ None,
1958
+ )
1959
+ .await
1960
+ .unwrap()
1961
+ .accepted
1962
+ .then_some(())
1963
+ .expect("registration should succeed");
1474
1964
 
1475
1965
  let open_src = serde_json::json!({
1476
1966
  "number": { "value": "src-rollback" }, "kind": { "name": "current" },
1477
1967
  "daily_limit": { "cents": 50000 }, "customer": "CUST-0012",
1478
1968
  });
1479
- handle(&client, &wasm_path(), "Banking::Account.Open", open_src, None, &config, &lambda_client::NeverInvoker)
1480
- .await.unwrap().accepted.then_some(()).expect("source account open should succeed");
1969
+ handle(
1970
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
1971
+ "Banking::Account.Open",
1972
+ open_src,
1973
+ None,
1974
+ )
1975
+ .await
1976
+ .unwrap()
1977
+ .accepted
1978
+ .then_some(())
1979
+ .expect("source account open should succeed");
1481
1980
  let open_dst = serde_json::json!({
1482
1981
  "number": { "value": "dst-rollback" }, "kind": { "name": "current" },
1483
1982
  "daily_limit": { "cents": 50000 }, "customer": "CUST-0012",
1484
1983
  });
1485
- handle(&client, &wasm_path(), "Banking::Account.Open", open_dst, None, &config, &lambda_client::NeverInvoker)
1486
- .await.unwrap().accepted.then_some(()).expect("destination account open should succeed");
1487
1984
  handle(
1488
- &client, &wasm_path(), "Banking::Account.Credit",
1489
- serde_json::json!({ "number": "src-rollback", "amount": { "cents": 1000 }, "narrative": { "text": "opening balance" } }),
1490
- None, &config, &lambda_client::NeverInvoker,
1985
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
1986
+ "Banking::Account.Open",
1987
+ open_dst,
1988
+ None,
1491
1989
  )
1990
+ .await
1991
+ .unwrap()
1992
+ .accepted
1993
+ .then_some(())
1994
+ .expect("destination account open should succeed");
1995
+ handle(Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker), "Banking::Account.Credit", serde_json::json!({ "number": "src-rollback", "amount": { "cents": 1000 }, "narrative": { "text": "opening balance" } }), None)
1492
1996
  .await.unwrap().accepted.then_some(()).expect("credit should succeed");
1493
1997
 
1494
1998
  // `Transfer.Request`'s own mutation is the first one this step
@@ -1498,11 +2002,26 @@ pub(crate) mod tests {
1498
2002
  "reference": { "value": "tr-rollback" }, "amount": { "cents": 200 },
1499
2003
  "narrative": { "text": "rollback test" }, "source": "src-rollback", "destination": "dst-rollback",
1500
2004
  });
1501
- let outcome = handle(&client, &wasm_path(), "Banking::Transfer.Request", transfer_args, None, &config, &lambda_client::NeverInvoker).await;
1502
- assert!(outcome.is_err(), "the unprovisioned Transfer lineage table should make this whole invocation fail");
2005
+ let outcome = handle(
2006
+ Host::new(&client, &wasm_path(), &config, &lambda_client::NeverInvoker),
2007
+ "Banking::Transfer.Request",
2008
+ transfer_args,
2009
+ None,
2010
+ )
2011
+ .await;
2012
+ assert!(
2013
+ outcome.is_err(),
2014
+ "the unprovisioned Transfer lineage table should make this whole invocation fail"
2015
+ );
1503
2016
 
1504
2017
  let guard = client.lock().await;
1505
- let saga_rows = guard.query("SELECT process_manager, correlation FROM hecks_lambda_sagas", &[]).await.unwrap();
2018
+ let saga_rows = guard
2019
+ .query(
2020
+ "SELECT process_manager, correlation FROM hecks_lambda_sagas",
2021
+ &[],
2022
+ )
2023
+ .await
2024
+ .unwrap();
1506
2025
  assert_eq!(
1507
2026
  saga_rows.len(), 0,
1508
2027
  "a saga write inside a transaction that ultimately fails must roll back with it, not commit independently: {saga_rows:?}"
@@ -1512,49 +2031,102 @@ pub(crate) mod tests {
1512
2031
  #[tokio::test]
1513
2032
  async fn a_pre_existing_snapshot_without_the_sagas_backfilled_latch_forces_one_full_replay() {
1514
2033
  let client = scratch_db("rust_host_dispatch_test_10").await;
1515
- provision_lineage(&*client.lock().await, "Banking", 1, &["Customer", "Account", "Transfer"]).await;
2034
+ provision_lineage(
2035
+ &*client.lock().await,
2036
+ "Banking",
2037
+ 1,
2038
+ &["Customer", "Account", "Transfer"],
2039
+ )
2040
+ .await;
1516
2041
  let config = test_config("Banking", 1);
1517
2042
  // `ReviewOnFreeze` fires on every `FreezeAccount` in this domain
1518
2043
  // (proven above), so `NeverInvoker` would panic here.
1519
2044
  let invoker = RecordingInvoker::new();
1520
2045
 
1521
- handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0013"), None, &config, &invoker)
1522
- .await.unwrap().accepted.then_some(()).expect("registration should succeed");
2046
+ handle(
2047
+ Host::new(&client, &wasm_path(), &config, &invoker),
2048
+ "Banking::Customer.Register",
2049
+ register("CUST-0013"),
2050
+ None,
2051
+ )
2052
+ .await
2053
+ .unwrap()
2054
+ .accepted
2055
+ .then_some(())
2056
+ .expect("registration should succeed");
1523
2057
 
1524
2058
  let open_src = serde_json::json!({
1525
2059
  "number": { "value": "src-backfill" }, "kind": { "name": "current" },
1526
2060
  "daily_limit": { "cents": 50000 }, "customer": "CUST-0013",
1527
2061
  });
1528
- handle(&client, &wasm_path(), "Banking::Account.Open", open_src, None, &config, &invoker)
1529
- .await.unwrap().accepted.then_some(()).expect("source account open should succeed");
2062
+ handle(
2063
+ Host::new(&client, &wasm_path(), &config, &invoker),
2064
+ "Banking::Account.Open",
2065
+ open_src,
2066
+ None,
2067
+ )
2068
+ .await
2069
+ .unwrap()
2070
+ .accepted
2071
+ .then_some(())
2072
+ .expect("source account open should succeed");
1530
2073
  let open_dst = serde_json::json!({
1531
2074
  "number": { "value": "dst-backfill" }, "kind": { "name": "current" },
1532
2075
  "daily_limit": { "cents": 50000 }, "customer": "CUST-0013",
1533
2076
  });
1534
- handle(&client, &wasm_path(), "Banking::Account.Open", open_dst, None, &config, &invoker)
1535
- .await.unwrap().accepted.then_some(()).expect("destination account open should succeed");
1536
2077
  handle(
1537
- &client, &wasm_path(), "Banking::Account.Credit",
1538
- serde_json::json!({ "number": "src-backfill", "amount": { "cents": 1000 }, "narrative": { "text": "opening balance" } }),
1539
- None, &config, &invoker,
2078
+ Host::new(&client, &wasm_path(), &config, &invoker),
2079
+ "Banking::Account.Open",
2080
+ open_dst,
2081
+ None,
1540
2082
  )
2083
+ .await
2084
+ .unwrap()
2085
+ .accepted
2086
+ .then_some(())
2087
+ .expect("destination account open should succeed");
2088
+ handle(Host::new(&client, &wasm_path(), &config, &invoker), "Banking::Account.Credit", serde_json::json!({ "number": "src-backfill", "amount": { "cents": 1000 }, "narrative": { "text": "opening balance" } }), None)
1541
2089
  .await.unwrap().accepted.then_some(()).expect("credit should succeed");
1542
2090
 
1543
2091
  // Freezing the destination first means the credit leg refuses
1544
2092
  // and Settlement compensates, leaving the saga stuck "reversed"
1545
2093
  // — exactly the mid-flight shape this test needs to recover.
1546
- handle(&client, &wasm_path(), "Banking::Account.FreezeAccount", serde_json::json!({ "number": { "value": "dst-backfill" } }), None, &config, &invoker)
1547
- .await.unwrap().accepted.then_some(()).expect("freeze should succeed");
2094
+ handle(
2095
+ Host::new(&client, &wasm_path(), &config, &invoker),
2096
+ "Banking::Account.FreezeAccount",
2097
+ serde_json::json!({ "number": { "value": "dst-backfill" } }),
2098
+ None,
2099
+ )
2100
+ .await
2101
+ .unwrap()
2102
+ .accepted
2103
+ .then_some(())
2104
+ .expect("freeze should succeed");
1548
2105
  let transfer_args = serde_json::json!({
1549
2106
  "reference": { "value": "tr-backfill" }, "amount": { "cents": 200 },
1550
2107
  "narrative": { "text": "backfill test" }, "source": "src-backfill", "destination": "dst-backfill",
1551
2108
  });
1552
- handle(&client, &wasm_path(), "Banking::Transfer.Request", transfer_args, None, &config, &invoker)
1553
- .await.unwrap().accepted.then_some(()).expect("the Transfer.Request COMMAND succeeds; the saga's own credit leg is what refuses");
2109
+ handle(
2110
+ Host::new(&client, &wasm_path(), &config, &invoker),
2111
+ "Banking::Transfer.Request",
2112
+ transfer_args,
2113
+ None,
2114
+ )
2115
+ .await
2116
+ .unwrap()
2117
+ .accepted
2118
+ .then_some(())
2119
+ .expect("the Transfer.Request COMMAND succeeds; the saga's own credit leg is what refuses");
1554
2120
 
1555
2121
  {
1556
2122
  let guard = client.lock().await;
1557
- let before = guard.query("SELECT process_manager, correlation, state FROM hecks_lambda_sagas", &[]).await.unwrap();
2123
+ let before = guard
2124
+ .query(
2125
+ "SELECT process_manager, correlation, state FROM hecks_lambda_sagas",
2126
+ &[],
2127
+ )
2128
+ .await
2129
+ .unwrap();
1558
2130
  assert_eq!(before.len(), 1, "the reversed Settlement instance should already be tracked before the simulated reset: {before:?}");
1559
2131
  }
1560
2132
 
@@ -1563,18 +2135,42 @@ pub(crate) mod tests {
1563
2135
  // mid-flight.
1564
2136
  {
1565
2137
  let guard = client.lock().await;
1566
- guard.execute("UPDATE hecks_lambda_snapshot SET sagas_backfilled = false", &[]).await.unwrap();
1567
- guard.execute("DELETE FROM hecks_lambda_sagas", &[]).await.unwrap();
2138
+ guard
2139
+ .execute(
2140
+ "UPDATE hecks_lambda_snapshot SET sagas_backfilled = false",
2141
+ &[],
2142
+ )
2143
+ .await
2144
+ .unwrap();
2145
+ guard
2146
+ .execute("DELETE FROM hecks_lambda_sagas", &[])
2147
+ .await
2148
+ .unwrap();
1568
2149
  }
1569
2150
 
1570
2151
  // One more command should trigger a full replay, whose saga
1571
2152
  // cascade re-running as a side effect repopulates
1572
2153
  // `hecks_lambda_sagas`.
1573
- handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0014"), None, &config, &invoker)
1574
- .await.unwrap().accepted.then_some(()).expect("second registration should succeed");
2154
+ handle(
2155
+ Host::new(&client, &wasm_path(), &config, &invoker),
2156
+ "Banking::Customer.Register",
2157
+ register("CUST-0014"),
2158
+ None,
2159
+ )
2160
+ .await
2161
+ .unwrap()
2162
+ .accepted
2163
+ .then_some(())
2164
+ .expect("second registration should succeed");
1575
2165
 
1576
2166
  let guard = client.lock().await;
1577
- let saga_rows = guard.query("SELECT process_manager, correlation, state FROM hecks_lambda_sagas", &[]).await.unwrap();
2167
+ let saga_rows = guard
2168
+ .query(
2169
+ "SELECT process_manager, correlation, state FROM hecks_lambda_sagas",
2170
+ &[],
2171
+ )
2172
+ .await
2173
+ .unwrap();
1578
2174
  assert_eq!(saga_rows.len(), 1, "the backfill replay should have recovered the in-flight Settlement instance: {saga_rows:?}");
1579
2175
  let process_manager: String = saga_rows[0].get(0);
1580
2176
  let correlation: String = saga_rows[0].get(1);
@@ -1583,7 +2179,11 @@ pub(crate) mod tests {
1583
2179
  assert_eq!(correlation, "tr-backfill");
1584
2180
  assert_eq!(state, "reversed");
1585
2181
 
1586
- let latched: bool = guard.query_one("SELECT sagas_backfilled FROM hecks_lambda_snapshot", &[]).await.unwrap().get(0);
2182
+ let latched: bool = guard
2183
+ .query_one("SELECT sagas_backfilled FROM hecks_lambda_snapshot", &[])
2184
+ .await
2185
+ .unwrap()
2186
+ .get(0);
1587
2187
  assert!(latched, "after the backfill replay, the latch should be set so this doesn't repeat every invocation");
1588
2188
  }
1589
2189
 
@@ -1595,31 +2195,49 @@ pub(crate) mod tests {
1595
2195
 
1596
2196
  impl AlwaysFailingInvoker {
1597
2197
  fn new() -> Self {
1598
- Self { calls: std::sync::Mutex::new(0) }
2198
+ Self {
2199
+ calls: std::sync::Mutex::new(0),
2200
+ }
1599
2201
  }
1600
2202
  }
1601
2203
 
1602
2204
  #[async_trait::async_trait]
1603
2205
  impl LambdaInvoker for AlwaysFailingInvoker {
1604
- async fn invoke(&self, _function_name: &str, _payload: &str) -> anyhow::Result<lambda_client::InvokeOutcome> {
2206
+ async fn invoke(
2207
+ &self,
2208
+ _function_name: &str,
2209
+ _payload: &str,
2210
+ ) -> anyhow::Result<lambda_client::InvokeOutcome> {
1605
2211
  *self.calls.lock().unwrap() += 1;
1606
2212
  anyhow::bail!("ResourceNotFoundException: function not found")
1607
2213
  }
1608
2214
  }
1609
2215
 
1610
2216
  #[tokio::test]
1611
- async fn a_cross_domain_delivery_that_exhausts_every_retry_dead_letters_and_still_fails_the_invocation() {
2217
+ async fn a_cross_domain_delivery_that_exhausts_every_retry_dead_letters_and_still_fails_the_invocation(
2218
+ ) {
1612
2219
  let client = scratch_db("rust_host_dispatch_test_8").await;
1613
- provision_lineage(&*client.lock().await, "Banking", 1, &["Customer", "Account"]).await;
2220
+ provision_lineage(
2221
+ &*client.lock().await,
2222
+ "Banking",
2223
+ 1,
2224
+ &["Customer", "Account"],
2225
+ )
2226
+ .await;
1614
2227
  let config = test_config("Banking", 1);
1615
2228
  let invoker = AlwaysFailingInvoker::new();
1616
2229
 
1617
- handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0011"), None, &config, &invoker)
1618
- .await
1619
- .unwrap()
1620
- .accepted
1621
- .then_some(())
1622
- .expect("registration should succeed");
2230
+ handle(
2231
+ Host::new(&client, &wasm_path(), &config, &invoker),
2232
+ "Banking::Customer.Register",
2233
+ register("CUST-0011"),
2234
+ None,
2235
+ )
2236
+ .await
2237
+ .unwrap()
2238
+ .accepted
2239
+ .then_some(())
2240
+ .expect("registration should succeed");
1623
2241
 
1624
2242
  let open_args = serde_json::json!({
1625
2243
  "number": { "value": "acct-freeze-dead-letter" },
@@ -1627,18 +2245,29 @@ pub(crate) mod tests {
1627
2245
  "daily_limit": { "cents": 50000 },
1628
2246
  "customer": "CUST-0011",
1629
2247
  });
1630
- handle(&client, &wasm_path(), "Banking::Account.Open", open_args, None, &config, &invoker)
1631
- .await
1632
- .unwrap()
1633
- .accepted
1634
- .then_some(())
1635
- .expect("account open should succeed");
2248
+ handle(
2249
+ Host::new(&client, &wasm_path(), &config, &invoker),
2250
+ "Banking::Account.Open",
2251
+ open_args,
2252
+ None,
2253
+ )
2254
+ .await
2255
+ .unwrap()
2256
+ .accepted
2257
+ .then_some(())
2258
+ .expect("account open should succeed");
1636
2259
 
1637
2260
  // `ReviewOnFreeze` fires here — every attempt against
1638
2261
  // `AlwaysFailingInvoker` fails, so `deliver_with_retry` exhausts
1639
2262
  // `MAX_DELIVERY_ATTEMPTS` before `handle` ever sees a result.
1640
2263
  let freeze_args = serde_json::json!({ "number": { "value": "acct-freeze-dead-letter" } });
1641
- let outcome = handle(&client, &wasm_path(), "Banking::Account.FreezeAccount", freeze_args, None, &config, &invoker).await;
2264
+ let outcome = handle(
2265
+ Host::new(&client, &wasm_path(), &config, &invoker),
2266
+ "Banking::Account.FreezeAccount",
2267
+ freeze_args,
2268
+ None,
2269
+ )
2270
+ .await;
1642
2271
 
1643
2272
  assert!(
1644
2273
  outcome.is_err(),
@@ -1660,7 +2289,11 @@ pub(crate) mod tests {
1660
2289
  .query("SELECT policy, target_domain, target_verb, attempts FROM hecks_cross_domain_dead_letters", &[])
1661
2290
  .await
1662
2291
  .unwrap();
1663
- assert_eq!(dead_letters.len(), 1, "exactly one exhausted delivery should have been dead-lettered: {dead_letters:?}");
2292
+ assert_eq!(
2293
+ dead_letters.len(),
2294
+ 1,
2295
+ "exactly one exhausted delivery should have been dead-lettered: {dead_letters:?}"
2296
+ );
1664
2297
  let policy: String = dead_letters[0].get(0);
1665
2298
  let target_domain: String = dead_letters[0].get(1);
1666
2299
  let target_verb: String = dead_letters[0].get(2);
@@ -1679,16 +2312,27 @@ pub(crate) mod tests {
1679
2312
  #[tokio::test]
1680
2313
  async fn a_failed_cross_domain_delivery_does_not_drop_a_sibling_reaction_from_the_same_step() {
1681
2314
  let client = scratch_db("rust_host_dispatch_test_11").await;
1682
- provision_lineage(&*client.lock().await, "Banking", 1, &["Customer", "SafeDepositBox"]).await;
2315
+ provision_lineage(
2316
+ &*client.lock().await,
2317
+ "Banking",
2318
+ 1,
2319
+ &["Customer", "SafeDepositBox"],
2320
+ )
2321
+ .await;
1683
2322
  let config = test_config("Banking", 1);
1684
2323
  let invoker = AlwaysFailingInvoker::new();
1685
2324
 
1686
- handle(&client, &wasm_path(), "Banking::Customer.Register", register("CUST-0022"), None, &config, &invoker)
1687
- .await
1688
- .unwrap()
1689
- .accepted
1690
- .then_some(())
1691
- .expect("registration should succeed");
2325
+ handle(
2326
+ Host::new(&client, &wasm_path(), &config, &invoker),
2327
+ "Banking::Customer.Register",
2328
+ register("CUST-0022"),
2329
+ None,
2330
+ )
2331
+ .await
2332
+ .unwrap()
2333
+ .accepted
2334
+ .then_some(())
2335
+ .expect("registration should succeed");
1692
2336
 
1693
2337
  let rent_args = serde_json::json!({
1694
2338
  "branch_code": { "value": "downtown" },
@@ -1696,22 +2340,34 @@ pub(crate) mod tests {
1696
2340
  "size": { "value": "small" },
1697
2341
  "customer": "CUST-0022",
1698
2342
  });
1699
- handle(&client, &wasm_path(), "Banking::SafeDepositBox.Rent", rent_args, None, &config, &invoker)
1700
- .await
1701
- .unwrap()
1702
- .accepted
1703
- .then_some(())
1704
- .expect("rent should succeed");
2343
+ handle(
2344
+ Host::new(&client, &wasm_path(), &config, &invoker),
2345
+ "Banking::SafeDepositBox.Rent",
2346
+ rent_args,
2347
+ None,
2348
+ )
2349
+ .await
2350
+ .unwrap()
2351
+ .accepted
2352
+ .then_some(())
2353
+ .expect("rent should succeed");
1705
2354
 
1706
2355
  // Both ReviewOnBoxSurrender (-> Compliance) and FlagKeyReturn
1707
2356
  // (-> Notifications) fire here; both deliveries exhaust every
1708
2357
  // retry against AlwaysFailingInvoker.
1709
2358
  let surrender_args = serde_json::json!({ "branch_code": { "value": "downtown" }, "box_number": { "value": 12 } });
1710
- let outcome =
1711
- handle(&client, &wasm_path(), "Banking::SafeDepositBox.Surrender", surrender_args, None, &config, &invoker)
1712
- .await;
2359
+ let outcome = handle(
2360
+ Host::new(&client, &wasm_path(), &config, &invoker),
2361
+ "Banking::SafeDepositBox.Surrender",
2362
+ surrender_args,
2363
+ None,
2364
+ )
2365
+ .await;
1713
2366
 
1714
- assert!(outcome.is_err(), "still fails the invocation visibly, same contract as a single failed delivery");
2367
+ assert!(
2368
+ outcome.is_err(),
2369
+ "still fails the invocation visibly, same contract as a single failed delivery"
2370
+ );
1715
2371
  assert_eq!(
1716
2372
  *invoker.calls.lock().unwrap(),
1717
2373
  lambda_client::MAX_DELIVERY_ATTEMPTS * 2,
@@ -1721,7 +2377,10 @@ pub(crate) mod tests {
1721
2377
 
1722
2378
  let guard = client.lock().await;
1723
2379
  let dead_letters = guard
1724
- .query("SELECT policy FROM hecks_cross_domain_dead_letters ORDER BY policy", &[])
2380
+ .query(
2381
+ "SELECT policy FROM hecks_cross_domain_dead_letters ORDER BY policy",
2382
+ &[],
2383
+ )
1725
2384
  .await
1726
2385
  .unwrap();
1727
2386
  let policies: Vec<String> = dead_letters.iter().map(|row| row.get(0)).collect();