hecks 3.13.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.
- checksums.yaml +4 -4
- data/examples/pizzas/adapters/fake_processor.rb +34 -0
- data/examples/pizzas/adapters/payment_signature.rb +66 -0
- data/examples/pizzas/adapters/payment_webhook.rb +87 -0
- data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
- data/examples/pizzas/bluebook/pizzas.bluebook +4 -4
- data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
- data/examples/pizzas/config.ru +22 -0
- data/lib/hecks/adapters/driven/prism.rb +0 -2
- data/lib/hecks/adapters/driven.rb +0 -2
- data/lib/hecks/adapters/driving/json.rb +1 -7
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +15 -5
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
- data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
- data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
- data/lib/hecks/bluebook/hecksagon.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
- data/lib/hecks/bluebook/meta_validator.rb +0 -11
- data/lib/hecks/bluebook/named_pattern.rb +33 -0
- data/lib/hecks/bluebook/translation.rb +18 -10
- data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
- data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
- data/lib/hecks/bluebook/translation_expression.rb +39 -0
- data/lib/hecks/bluebook/translation_plan.rb +70 -0
- data/lib/hecks/bluebook.rb +3 -0
- data/lib/hecks/chapters.rb +3 -3
- data/lib/hecks/cli/domain_stub.rb +1 -1
- data/lib/hecks/cli/interview_aggregate.rb +4 -4
- data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
- data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
- data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
- data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
- data/lib/hecks/composer/adapters/jev.adapter +3 -0
- data/lib/hecks/composer/adapters/jev.rb +66 -0
- data/lib/hecks/composer/agent.rb +113 -0
- data/lib/hecks/composer/assistant.rb +110 -0
- data/lib/hecks/composer/behavior_run.rb +53 -0
- data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
- data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
- data/lib/hecks/composer/board.rb +65 -0
- data/lib/hecks/composer/board_edit/command_text.rb +39 -0
- data/lib/hecks/composer/board_edit/source.rb +84 -0
- data/lib/hecks/composer/board_edit.rb +144 -0
- data/lib/hecks/composer/check.rb +89 -0
- data/lib/hecks/composer/domains.rb +81 -0
- data/lib/hecks/composer/jobs.rb +134 -0
- data/lib/hecks/composer/layout.rb +37 -0
- data/lib/hecks/composer/live.rb +150 -0
- data/lib/hecks/composer/outline.rb +48 -0
- data/lib/hecks/composer/page.html +989 -0
- data/lib/hecks/composer/panels.rb +142 -0
- data/lib/hecks/composer/parse.rb +83 -0
- data/lib/hecks/composer/plain.rb +28 -0
- data/lib/hecks/composer/ports/composer_agent.port +7 -0
- data/lib/hecks/composer/session.rb +141 -0
- data/lib/hecks/composer/toolbox.rb +166 -0
- data/lib/hecks/composer/web.rb +156 -0
- data/lib/hecks/composer/workbench.rb +51 -0
- data/lib/hecks/composer/workspace.rb +81 -0
- data/lib/hecks/composer.rb +17 -0
- data/lib/hecks/corpus/adapters.rb +22 -0
- data/lib/hecks/corpus/rust.rb +1 -1
- data/lib/hecks/corpus/worlds.rb +19 -0
- data/lib/hecks/corpus.rb +5 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +34 -34
- data/lib/hecks/forms/app/commands.rb +2 -3
- data/lib/hecks/forms/app/queries.rb +2 -3
- data/lib/hecks/forms/app/records.rb +17 -5
- data/lib/hecks/forms/command_form_renderer.rb +42 -16
- data/lib/hecks/forms/field_controls.rb +2 -2
- data/lib/hecks/forms/field_renderer.rb +1 -1
- data/lib/hecks/forms/field_shape.rb +36 -17
- data/lib/hecks/forms/index_renderer.rb +2 -1
- data/lib/hecks/forms/page.rb +1 -1
- data/lib/hecks/forms/params.rb +0 -13
- data/lib/hecks/forms/query_form_renderer.rb +7 -6
- data/lib/hecks/forms/record_renderer.rb +11 -7
- data/lib/hecks/forms/rule_words.rb +100 -0
- data/lib/hecks/forms/value_object_shape.rb +2 -1
- data/lib/hecks/framework/bluebook/change.bluebook +2 -2
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
- data/lib/hecks/fuzzing/domain_generator/block_rendering.rb +1 -1
- data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
- data/lib/hecks/fuzzing/replay/filters.rb +2 -6
- data/lib/hecks/gate/stages.yml +25 -0
- data/lib/hecks/grammar/evolve/seeds.rb +0 -6
- data/lib/hecks/grammar/evolve.rb +0 -3
- data/lib/hecks/grammar/translation.bluebook +1 -1
- data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
- data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
- data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
- data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
- data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
- data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
- data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
- data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
- data/lib/hecks/hecks/codebase.behaviors +6 -0
- data/lib/hecks/hecks/codebase.bluebook +58 -37
- data/lib/hecks/hecks/composer.behaviors +68 -0
- data/lib/hecks/hecks/context_map.hecksagon +1 -1
- data/lib/hecks/hecks/custodian.behaviors +0 -6
- data/lib/hecks/hecks/custodian.bluebook +15 -92
- data/lib/hecks/hecks/hecks.bluebook +2 -2
- data/lib/hecks/hecks/hecks.hecksagon +2 -2
- data/lib/hecks/hecks/hecks.world +1 -1
- data/lib/hecks/hecks/rehearsal.bluebook +10 -16
- data/lib/hecks/hecks/tooling.bluebook +22 -22
- data/lib/hecks/kernel_capabilities/templates.rb +4 -4
- data/lib/hecks/kernel_capabilities.rb +1 -1
- data/lib/hecks/language/bluebook/aggregate.bluebook +1 -0
- data/lib/hecks/language/bluebook/command.bluebook +1 -0
- data/lib/hecks/language/bluebook/entity.bluebook +1 -0
- data/lib/hecks/language/bluebook/query.bluebook +1 -0
- data/lib/hecks/language/bluebook/shape.bluebook +8 -7
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
- data/lib/hecks/language/hecksagon/port_operation.bluebook +1 -0
- data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
- data/lib/hecks/ports/access_control.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
- data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
- data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
- data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
- data/lib/hecks/ports.rb +0 -1
- data/lib/hecks/projection_files.rb +3 -1
- data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
- data/lib/hecks/projections/glossary/entries.rb +10 -4
- data/lib/hecks/projections/glossary/groups.rb +42 -0
- data/lib/hecks/projections/glossary/html/page.rb +7 -2
- data/lib/hecks/projections/glossary/html/parser.rb +1 -1
- data/lib/hecks/projections/glossary/html/sections.rb +31 -11
- data/lib/hecks/projections/glossary/html.rb +1 -2
- data/lib/hecks/projections/glossary/markdown.rb +31 -28
- data/lib/hecks/projections/glossary/mermaid.rb +3 -0
- data/lib/hecks/projections/glossary/page.css +143 -153
- data/lib/hecks/projections/glossary/page.js +63 -3
- data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
- data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
- data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
- data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
- data/lib/hecks/projections/glossary/sentences.rb +13 -3
- data/lib/hecks/projections/glossary/slugs.rb +4 -1
- data/lib/hecks/projections/glossary.rb +2 -1
- data/lib/hecks/projections/plain_english.rb +97 -0
- data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
- data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary.rb +14 -6
- data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
- data/lib/hecks/projections/statements/actions.rb +99 -0
- data/lib/hecks/projections/statements/relationships.rb +86 -0
- data/lib/hecks/projections/statements.rb +15 -24
- data/lib/hecks/projector/exporter/adapters.rb +112 -0
- data/lib/hecks/projector/exporter/translations.rb +18 -35
- data/lib/hecks/projector/exporter/worlds.rb +103 -0
- data/lib/hecks/projector/exporter.rb +5 -1
- data/lib/hecks/quality_control/adapters/agent.rb +6 -7
- data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
- data/lib/hecks/quality_control/quality_control.bluebook +51 -51
- data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
- data/lib/hecks/runtime/outbox.rb +2 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
- data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
- data/lib/hecks/runtime/value.rb +13 -0
- data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
- data/lib/hecks/rust_build/wasm.rb +0 -12
- data/lib/hecks/rust_build/write_if_changed.rb +12 -12
- data/lib/hecks/rust_format.rb +77 -0
- data/lib/hecks/site/bluebook/site.bluebook +5 -5
- data/lib/hecks/sme/bluebook/sme.bluebook +12 -12
- data/lib/hecks/tickets/bluebook/tickets.bluebook +3 -3
- data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
- data/lib/hecks/tools/fuzz_sweep.rb +1 -3
- data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
- data/lib/hecks/tools/translation_audit.rb +8 -10
- data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +2 -1
- data/lib/hecks.rb +3 -0
- data/prebuilt/{58208399bada10b0db99928c48e38789288fc58f7b71a72b21bfa386680e32ed.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
- data/prebuilt/{2b59737fbdbd5f44edda0fddce2611f45d76087b3a7c782d1f1a17ea58c77948.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
- data/prebuilt/verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json +1 -0
- data/rust/Cargo.toml +40 -0
- data/rust/build/Cargo.toml +36 -0
- data/rust/build/clippy.toml +4 -0
- data/rust/build/src/adapters_pass.rs +196 -0
- data/rust/build/src/binding_pass.rs +54 -13
- data/rust/build/src/build_artifact.rs +23 -5
- data/rust/build/src/cargo_sync.rs +69 -26
- data/rust/build/src/lineage_pass.rs +101 -87
- data/rust/build/src/main.rs +11 -2
- data/rust/build/src/pipeline.rs +80 -19
- data/rust/build/src/reserved_names.rs +6 -50
- data/rust/build/src/resolve.rs +82 -35
- data/rust/build/src/rustfmt.rs +100 -0
- data/rust/build/src/seams_pass.rs +85 -16
- data/rust/build/src/subprocess.rs +30 -6
- data/rust/build/src/tmp.rs +5 -2
- data/rust/build/src/translation_pass.rs +72 -22
- data/rust/build/src/world_pass.rs +109 -0
- data/rust/clippy.toml +4 -0
- data/rust/codegen/Cargo.toml +36 -0
- data/rust/codegen/clippy.toml +4 -0
- data/rust/codegen/src/asks.rs +40 -14
- data/rust/codegen/src/attr.rs +2 -2
- data/rust/codegen/src/bridging.rs +676 -117
- data/rust/codegen/src/commands.rs +979 -373
- data/rust/codegen/src/constraints.rs +88 -25
- data/rust/codegen/src/dependency_planning.rs +128 -34
- data/rust/codegen/src/domain_generator.rs +300 -170
- data/rust/codegen/src/exemplar.rs +87 -32
- data/rust/codegen/src/expr_emitter.rs +54 -6
- data/rust/codegen/src/fielded.rs +193 -41
- data/rust/codegen/src/hecks_naming.rs +124 -16
- data/rust/codegen/src/hecks_naming_tests.rs +81 -0
- data/rust/codegen/src/json.rs +80 -44
- data/rust/codegen/src/json_codec.rs +585 -149
- data/rust/codegen/src/literal.rs +104 -138
- data/rust/codegen/src/main.rs +145 -52
- data/rust/codegen/src/manifest.rs +76 -14
- data/rust/codegen/src/mutations.rs +1212 -239
- data/rust/codegen/src/naming.rs +111 -60
- data/rust/codegen/src/optional_pass.rs +90 -28
- data/rust/codegen/src/ports.rs +5 -4
- data/rust/codegen/src/prelude.rs +139 -49
- data/rust/codegen/src/queries.rs +601 -226
- data/rust/codegen/src/reactions.rs +268 -74
- data/rust/codegen/src/read_models.rs +606 -205
- data/rust/codegen/src/reference_specs.rs +16 -3
- data/rust/codegen/src/registry.rs +128 -79
- data/rust/codegen/src/reserved_names.rs +6 -50
- data/rust/codegen/src/shared.rs +71 -22
- data/rust/codegen/src/sidecars.rs +15 -5
- data/rust/codegen/src/skip_reason.rs +8 -2
- data/rust/codegen/src/types.rs +306 -108
- data/rust/host/Cargo.lock +1 -0
- data/rust/host/Cargo.toml +40 -4
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/clippy.toml +4 -0
- data/rust/host/src/api.rs +698 -218
- data/rust/host/src/auth.rs +491 -214
- data/rust/host/src/b64.rs +91 -0
- data/rust/host/src/bin/expr_harness.rs +6 -2
- data/rust/host/src/bin/lineage_harness.rs +47 -11
- data/rust/host/src/bin/mint_harness.rs +70 -37
- data/rust/host/src/bin/validate_harness.rs +16 -8
- data/rust/host/src/boot.rs +144 -68
- data/rust/host/src/boundary_fuzz/api.rs +130 -32
- data/rust/host/src/boundary_fuzz/auth.rs +108 -23
- data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
- data/rust/host/src/boundary_fuzz/ir.rs +164 -12
- data/rust/host/src/boundary_fuzz/server.rs +283 -36
- data/rust/host/src/boundary_fuzz/web.rs +56 -8
- data/rust/host/src/checkout.rs +78 -32
- data/rust/host/src/clock.rs +8 -0
- data/rust/host/src/commerce_ir.rs +333 -95
- data/rust/host/src/crypto.rs +33 -0
- data/rust/host/src/dispatch.rs +990 -331
- data/rust/host/src/expr_json.rs +1021 -273
- data/rust/host/src/extension.rs +24 -4
- data/rust/host/src/field_hints.rs +11 -7
- data/rust/host/src/fuzz_support.rs +85 -13
- data/rust/host/src/host.rs +100 -0
- data/rust/host/src/http.rs +22 -0
- data/rust/host/src/ir.rs +326 -16
- data/rust/host/src/journal.rs +277 -176
- data/rust/host/src/lambda_client.rs +219 -53
- data/rust/host/src/lib.rs +16 -4
- data/rust/host/src/log.rs +62 -11
- data/rust/host/src/mint.rs +1153 -326
- data/rust/host/src/needs.rs +185 -44
- data/rust/host/src/payments/keystore.rs +197 -45
- data/rust/host/src/payments/roles.rs +191 -0
- data/rust/host/src/payments/tests.rs +1445 -275
- data/rust/host/src/payments.rs +409 -121
- data/rust/host/src/platform_signin/attack_tests.rs +5 -5
- data/rust/host/src/platform_signin/config.rs +11 -11
- data/rust/host/src/platform_signin/cookie.rs +2 -2
- data/rust/host/src/platform_signin/extension.rs +2 -1
- data/rust/host/src/platform_signin/handler.rs +9 -8
- data/rust/host/src/platform_signin/jti.rs +4 -2
- data/rust/host/src/platform_signin/jwks.rs +2 -1
- data/rust/host/src/platform_signin/jws.rs +2 -37
- data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
- data/rust/host/src/platform_signin/mode_tests.rs +5 -3
- data/rust/host/src/platform_signin/pages.rs +20 -20
- data/rust/host/src/platform_signin/session.rs +8 -1
- data/rust/host/src/platform_signin/settings.rs +6 -6
- data/rust/host/src/platform_signin/strict_json.rs +12 -9
- data/rust/host/src/platform_signin/verify.rs +41 -41
- data/rust/host/src/platform_signin/wiring_support.rs +13 -18
- data/rust/host/src/presentation.rs +207 -63
- data/rust/host/src/presentation_write.rs +502 -187
- data/rust/host/src/query_step.rs +69 -18
- data/rust/host/src/rate_limit.rs +541 -142
- data/rust/host/src/reference_transform.rs +77 -25
- data/rust/host/src/reference_validate.rs +443 -104
- data/rust/host/src/resend.rs +190 -55
- data/rust/host/src/secrets.rs +116 -21
- data/rust/host/src/server.rs +483 -104
- data/rust/host/src/storage_shape.rs +88 -21
- data/rust/host/src/test_pg.rs +17 -7
- data/rust/host/src/ui_schema.rs +495 -207
- data/rust/host/src/wasm_runner.rs +21 -6
- data/rust/host/src/web/commerce.rs +101 -23
- data/rust/host/src/web/newsletter/cooldown.rs +10 -3
- data/rust/host/src/web/newsletter/template.rs +26 -7
- data/rust/host/src/web/newsletter/tests.rs +112 -23
- data/rust/host/src/web/newsletter.rs +209 -59
- data/rust/host/src/web/newsletter_send.rs +251 -79
- data/rust/host/src/web/registration_receipt/tests.rs +16 -4
- data/rust/host/src/web/registration_receipt.rs +100 -24
- data/rust/host/src/web/registrations.rs +1384 -332
- data/rust/host/src/web/resend_webhook.rs +138 -80
- data/rust/host/src/web.rs +2416 -508
- data/rust/lsp/Cargo.toml +36 -0
- data/rust/lsp/clippy.toml +4 -0
- data/rust/lsp/src/json.rs +29 -22
- data/rust/lsp/src/main.rs +81 -31
- data/rust/lsp/src/outline.rs +23 -7
- data/rust/lsp/src/rpc.rs +17 -4
- data/rust/parser/Cargo.toml +36 -0
- data/rust/parser/clippy.toml +4 -0
- data/rust/parser/src/build/closed_sets.rs +6 -1
- data/rust/parser/src/build/identity.rs +4 -3
- data/rust/parser/src/build/mod.rs +1 -0
- data/rust/parser/src/build/named_pattern.rs +26 -0
- data/rust/parser/src/build/naming.rs +2 -80
- data/rust/parser/src/build/pattern_subset.rs +6 -8
- data/rust/parser/src/build/query_derive.rs +3 -1
- data/rust/parser/src/build/query_inference.rs +14 -5
- data/rust/parser/src/build/read_model.rs +4 -1
- data/rust/parser/src/build/references.rs +4 -3
- data/rust/parser/src/canonical.rs +34 -15
- data/rust/parser/src/emit.rs +187 -105
- data/rust/parser/src/expr/ast_json.rs +134 -42
- data/rust/parser/src/expr/evaluator.rs +27 -9
- data/rust/parser/src/expr/mod.rs +54 -13
- data/rust/parser/src/expr/resolver.rs +299 -92
- data/rust/parser/src/ir.rs +35 -3
- data/rust/parser/src/keywords.rs +7168 -510
- data/rust/parser/src/lex.rs +11 -25
- data/rust/parser/src/lib.rs +1 -0
- data/rust/parser/src/main.rs +65 -6
- data/rust/parser/src/parse/adapter.rs +157 -0
- data/rust/parser/src/parse/aggregate.rs +19 -10
- data/rust/parser/src/parse/chapter.rs +16 -17
- data/rust/parser/src/parse/command.rs +35 -16
- data/rust/parser/src/parse/domain_port.rs +4 -7
- data/rust/parser/src/parse/entity.rs +21 -11
- data/rust/parser/src/parse/hecksagon.rs +10 -4
- data/rust/parser/src/parse/lifecycle.rs +9 -5
- data/rust/parser/src/parse/mod.rs +43 -91
- data/rust/parser/src/parse/needs.rs +21 -12
- data/rust/parser/src/parse/policy.rs +2 -2
- data/rust/parser/src/parse/process_manager.rs +9 -11
- data/rust/parser/src/parse/query.rs +8 -4
- data/rust/parser/src/parse/read_model.rs +57 -16
- data/rust/parser/src/parse/value_object.rs +7 -2
- data/rust/parser/src/parse/world.rs +322 -0
- data/rust/parser/src/ruby_value.rs +7 -6
- data/rust/src/exemplar/json.rs +4 -4
- data/rust/src/exemplar/mutations.rs +18 -0
- data/rust/src/exemplar/read_models.rs +1 -10
- data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
- data/rust/src/kernel/attribute_shapes/list.rs +2 -2
- data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
- data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
- data/rust/src/kernel/cli.rs +229 -112
- data/rust/src/kernel/dispatch.rs +323 -115
- data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
- data/rust/src/kernel/expr.rs +203 -86
- data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
- data/rust/src/kernel/expression_operators/comparison.rs +87 -2
- data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
- data/rust/src/kernel/expression_operators/logical.rs +9 -3
- data/rust/src/kernel/expression_operators/membership.rs +3 -1
- data/rust/src/kernel/expression_operators/mod.rs +34 -21
- data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
- data/rust/src/kernel/expression_operators/positional.rs +84 -30
- data/rust/src/kernel/expression_operators/presence.rs +52 -13
- data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
- data/rust/src/kernel/expression_operators/sized.rs +24 -8
- data/rust/src/kernel/expression_operators/text.rs +120 -27
- data/rust/src/kernel/expression_operators/to_string.rs +10 -4
- data/rust/src/kernel/json.rs +230 -148
- data/rust/src/kernel/mod.rs +46 -33
- data/rust/src/kernel/named_query.rs +63 -15
- data/rust/src/kernel/needs.rs +112 -25
- data/rust/src/kernel/orchestrate.rs +980 -581
- data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
- data/rust/src/kernel/pattern.rs +107 -34
- data/rust/src/kernel/query_comparators.rs +118 -41
- data/rust/src/kernel/query_ordering.rs +13 -5
- data/rust/src/kernel/read_model.rs +479 -181
- data/rust/src/kernel/reference_lookup.rs +122 -26
- data/rust/src/kernel/repository.rs +293 -66
- data/rust/src/kernel/routing.rs +58 -42
- data/rust/src/kernel/ruby_float.rs +139 -0
- data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
- data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
- data/rust/src/kernel/vocab/field_hint.rs +19 -25
- data/rust/src/kernel/vocab/mod.rs +6 -6
- data/rust/src/kernel/vocab/query_comparator.rs +23 -24
- data/rust/src/kernel/vocab/refusal_template.rs +501 -297
- data/rust/src/lib.rs +50 -0
- data/rust/src/main.rs +4 -1
- data/rust/web/Cargo.toml +36 -0
- data/rust/web/clippy.toml +4 -0
- metadata +85 -15
- data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
- data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
- data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
- data/lib/hecks/ports/authentication.port +0 -6
- data/lib/hecks/ports/authentication.rb +0 -67
- data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
- data/prebuilt/verdicts-48318ca172edd21443b1ad02801c697fff100b746e828f09db987647641a5df2.json +0 -1
- data/rust/host/src/approval.rs +0 -652
data/rust/host/src/dispatch.rs
CHANGED
|
@@ -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::
|
|
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
|
|
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 {
|
|
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
|
|
79
|
-
|
|
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(
|
|
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
|
|
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) =>
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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() {
|
|
217
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
247
|
-
|
|
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
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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)> =
|
|
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(||
|
|
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(||
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
let
|
|
405
|
-
|
|
406
|
-
|
|
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(||
|
|
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(||
|
|
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 {
|
|
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) =
|
|
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(
|
|
510
|
-
"
|
|
511
|
-
|
|
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(
|
|
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(
|
|
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|
|
|
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))
|
|
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
|
|
621
|
-
|
|
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 =
|
|
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
|
-
|
|
668
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
752
|
-
|
|
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!(
|
|
758
|
-
|
|
759
|
-
|
|
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(
|
|
769
|
-
|
|
770
|
-
.
|
|
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
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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(
|
|
932
|
+
mirrored: Some(std::iter::once("Banking::Customer".to_string()).collect()),
|
|
791
933
|
};
|
|
792
934
|
|
|
793
935
|
handle_facts(
|
|
794
|
-
|
|
795
|
-
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
836
|
-
|
|
837
|
-
.
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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(
|
|
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!(
|
|
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(
|
|
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!(
|
|
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
|
-
|
|
877
|
-
|
|
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(
|
|
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!(
|
|
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!(
|
|
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
|
-
|
|
928
|
-
|
|
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"]
|
|
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!(
|
|
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(
|
|
1011
|
-
|
|
1012
|
-
.
|
|
1013
|
-
|
|
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)
|
|
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)
|
|
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!(
|
|
1041
|
-
|
|
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(
|
|
1062
|
-
|
|
1063
|
-
|
|
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(
|
|
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)
|
|
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
|
|
1084
|
-
|
|
1085
|
-
|
|
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(
|
|
1089
|
-
|
|
1090
|
-
|
|
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!(
|
|
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
|
|
1095
|
-
|
|
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(
|
|
1379
|
+
.execute(
|
|
1380
|
+
"INSERT INTO hecks_lambda_journal (verb, args) VALUES ($1, $2)",
|
|
1381
|
+
&[&"Banking::Customer.Register", ®ister("CUST-0009")],
|
|
1382
|
+
)
|
|
1103
1383
|
.await
|
|
1104
1384
|
.unwrap();
|
|
1105
|
-
let read_failure = read(&client, &wasm_path())
|
|
1106
|
-
|
|
1107
|
-
|
|
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
|
|
1114
|
-
|
|
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)
|
|
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!(
|
|
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
|
|
1126
|
-
|
|
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(
|
|
1145
|
-
|
|
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!(
|
|
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
|
-
|
|
1165
|
-
|
|
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 {
|
|
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(
|
|
1212
|
-
self
|
|
1213
|
-
|
|
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(
|
|
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(
|
|
1225
|
-
|
|
1226
|
-
.
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
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(
|
|
1238
|
-
|
|
1239
|
-
.
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
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(
|
|
1249
|
-
|
|
1250
|
-
.
|
|
1251
|
-
|
|
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"]
|
|
1254
|
-
|
|
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
|
|
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(
|
|
1285
|
-
|
|
1286
|
-
.
|
|
1287
|
-
|
|
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!(
|
|
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 {
|
|
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 {
|
|
1372
|
-
|
|
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(
|
|
1387
|
-
|
|
1388
|
-
|
|
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!(
|
|
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!(
|
|
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!(
|
|
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!(
|
|
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!(
|
|
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(
|
|
1419
|
-
|
|
1420
|
-
|
|
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 {
|
|
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!(
|
|
1427
|
-
|
|
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!(
|
|
1875
|
+
assert!(
|
|
1876
|
+
!off_wrong.accepted,
|
|
1877
|
+
"off refuses it too: {:?}",
|
|
1878
|
+
off_wrong.result
|
|
1879
|
+
);
|
|
1430
1880
|
|
|
1431
|
-
let nobody = go(
|
|
1432
|
-
|
|
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!(
|
|
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(
|
|
1450
|
-
|
|
1451
|
-
|
|
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 [
|
|
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(
|
|
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(
|
|
1473
|
-
|
|
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(
|
|
1480
|
-
|
|
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(),
|
|
1489
|
-
|
|
1490
|
-
|
|
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(
|
|
1502
|
-
|
|
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
|
|
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(
|
|
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(
|
|
1522
|
-
|
|
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(
|
|
1529
|
-
|
|
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(),
|
|
1538
|
-
|
|
1539
|
-
|
|
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(
|
|
1547
|
-
|
|
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(
|
|
1553
|
-
|
|
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
|
|
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
|
|
1567
|
-
|
|
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(
|
|
1574
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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(
|
|
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(
|
|
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(
|
|
1618
|
-
|
|
1619
|
-
.
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
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(
|
|
1631
|
-
|
|
1632
|
-
.
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
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(
|
|
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!(
|
|
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(
|
|
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(
|
|
1687
|
-
|
|
1688
|
-
.
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
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(
|
|
1700
|
-
|
|
1701
|
-
.
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
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
|
-
|
|
1712
|
-
|
|
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!(
|
|
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(
|
|
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();
|