lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit d35dc2ae1b690aff29c2f13c8e54ea61867af59f
parent 2c79e6a9b82587a42b3f3de3cfbc4cffaa73eaec
Author: triesap <tyson@radroots.org>
Date:   Thu, 30 Jul 2026 08:51:47 +0000

trade: complete package conformance coverage

- execute canonical reducer and workflow mutation vectors
- lock packaged fixtures to the operations contract corpus
- enforce portable exports, traits, and forbidden dependency boundaries
- qualify JSON without std and remove the unused error dependency

Diffstat:
MCargo.lock | 1-
Acontracts/conformance/vectors/trade/prepare_workflow.v1.json | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcontracts/operations.toml | 34+++++++++++++++++++++++++++++++---
Mcrates/trade/Cargo.toml | 1-
Mcrates/trade/src/reducer_impl.rs | 65++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mcrates/trade/src/validation_receipt.rs | 55+++++++++++++++++++++++++++++++++++++++++++------------
Mcrates/trade/src/workflow.rs | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/trade/tests/fixtures/prepare_workflow.v1.json | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/trade/tests/fixtures/reduce_records.v1.json | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/trade/tests/package_boundary.rs | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
10 files changed, 558 insertions(+), 40 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -5119,7 +5119,6 @@ dependencies = [ "serde", "serde_json", "sha2", - "thiserror 1.0.69", ] [[package]] diff --git a/contracts/conformance/vectors/trade/prepare_workflow.v1.json b/contracts/conformance/vectors/trade/prepare_workflow.v1.json @@ -0,0 +1,74 @@ +{ + "suite": "trade_workflow", + "contract_version": "1.0.0", + "vectors": [ + { + "id": "trade_prepare_workflow_proposal_private_terms_001", + "kind": "trade.prepare_workflow.valid", + "input": { "mutation": "proposal_with_private_terms" }, + "expected": { + "mutation_kind": "proposal", + "required_actions": ["verify_private_terms", "sign", "persist", "deliver"], + "private_terms_artifact_id": "artifact-1" + } + }, + { + "id": "trade_prepare_workflow_decision_002", + "kind": "trade.prepare_workflow.valid", + "input": { "mutation": "decision" }, + "expected": { + "mutation_kind": "decision", + "required_actions": ["sign", "persist", "deliver"], + "private_terms_artifact_id": null + } + }, + { + "id": "trade_prepare_workflow_revision_proposal_003", + "kind": "trade.prepare_workflow.valid", + "input": { "mutation": "revision_proposal_with_private_terms" }, + "expected": { + "mutation_kind": "revision_proposal", + "required_actions": ["verify_private_terms", "sign", "persist", "deliver"], + "private_terms_artifact_id": "artifact-1" + } + }, + { + "id": "trade_prepare_workflow_revision_decision_004", + "kind": "trade.prepare_workflow.valid", + "input": { "mutation": "revision_decision" }, + "expected": { + "mutation_kind": "revision_decision", + "required_actions": ["sign", "persist", "deliver"], + "private_terms_artifact_id": null + } + }, + { + "id": "trade_prepare_workflow_cancellation_005", + "kind": "trade.prepare_workflow.valid", + "input": { "mutation": "cancellation" }, + "expected": { + "mutation_kind": "cancellation", + "required_actions": ["sign", "persist", "deliver"], + "private_terms_artifact_id": null + } + }, + { + "id": "trade_prepare_workflow_unsupported_schema_006", + "kind": "trade.prepare_workflow.invalid", + "input": { "mutation": "proposal_with_unsupported_schema" }, + "expected": { "error_kind": "unsupported_schema" } + }, + { + "id": "trade_prepare_workflow_missing_mutation_id_007", + "kind": "trade.prepare_workflow.invalid", + "input": { "mutation": "proposal_without_mutation_id" }, + "expected": { "error_kind": "missing_mutation_id" } + }, + { + "id": "trade_prepare_workflow_invalid_transition_008", + "kind": "trade.prepare_workflow.invalid", + "input": { "mutation": "decision_without_parent" }, + "expected": { "error_kind": "invalid_mutation" } + } + ] +} diff --git a/contracts/operations.toml b/contracts/operations.toml @@ -1810,15 +1810,43 @@ signing = "native" transport = "native" [operations.trade_reduce_records.implementation] -rust_modules = ["crates/trade/src/workflow.rs"] +rust_modules = [ + "crates/trade/src/reducer.rs", + "crates/trade/src/reducer_impl.rs", +] rust_types = [ - "radroots_trade::workflow::RadrootsTradeReductionInputV1", - "radroots_trade::workflow::RadrootsTradeProjectionV1", + "radroots_trade::reducer::RadrootsTradeReductionInputV1", + "radroots_trade::model::RadrootsTradeProjectionV1", ] [operations.trade_reduce_records.conformance] vector = "contracts/conformance/vectors/trade/reduce_records.v1.json" +[operations.trade_prepare_workflow] +domain = "trade" +id = "trade.prepare_workflow" +stability = "beta" +inputs = ["TradeMutationEnvelopeV1"] +outputs = ["WorkflowPlan"] +error_class = "validation_error" +deterministic = true +signing = "none" +transport = "none" + +[operations.trade_prepare_workflow.implementation] +rust_modules = ["crates/trade/src/workflow.rs"] +rust_types = [ + "radroots_trade::workflow::Error", + "radroots_trade::workflow::WorkflowPlan", +] + +[operations.trade_prepare_workflow.conformance] +vector = "contracts/conformance/vectors/trade/prepare_workflow.v1.json" +case_kinds = [ + "trade.prepare_workflow.valid", + "trade.prepare_workflow.invalid", +] + [operations.trade_validation_validate_operational_listing_event] domain = "trade_validation" id = "trade_validation.validate_operational_listing_event" diff --git a/crates/trade/Cargo.toml b/crates/trade/Cargo.toml @@ -62,7 +62,6 @@ serde_json = { workspace = true, default-features = false, features = [ "alloc", ], optional = true } sha2 = { workspace = true, default-features = false, optional = true } -thiserror = { workspace = true } [dev-dependencies] nostr = { workspace = true } diff --git a/crates/trade/src/reducer_impl.rs b/crates/trade/src/reducer_impl.rs @@ -1395,8 +1395,9 @@ mod tests { }; use radroots_test_fixtures::{FIXTURE_ALICE_PUBLIC_KEY_HEX, FIXTURE_BOB_PUBLIC_KEY_HEX}; - const REDUCER_VECTORS: &str = + const CANONICAL_REDUCER_VECTORS: &str = include_str!("../../../contracts/conformance/vectors/trade/reduce_records.v1.json"); + const PACKAGED_REDUCER_VECTORS: &str = include_str!("../tests/fixtures/reduce_records.v1.json"); fn hex_64(character: char) -> String { core::iter::repeat_n(character, 64).collect() @@ -1885,26 +1886,52 @@ mod tests { } #[test] - fn reducer_conformance_inventory_covers_deterministic_edge_cases() { + fn reducer_conformance_vectors_execute_deterministic_edge_cases() { + assert_eq!(PACKAGED_REDUCER_VECTORS, CANONICAL_REDUCER_VECTORS); let document: serde_json::Value = - serde_json::from_str(REDUCER_VECTORS).expect("reducer vectors parse"); - let ids = document["vectors"] + serde_json::from_str(PACKAGED_REDUCER_VECTORS).expect("reducer vectors parse"); + assert_eq!(document["suite"], "trade"); + assert_eq!(document["contract_version"], "1.0.0"); + let vectors = document["vectors"] .as_array() - .expect("reducer vector array") - .iter() - .map(|vector| vector["id"].as_str().expect("reducer vector id")) - .collect::<BTreeSet<_>>(); - - for required in [ - "trade_reduce_agreed_projection_digest_001", - "trade_reduce_contested_claims_002", - "trade_reduce_attestation_only_003", - "trade_reduce_missing_parent_004", - "trade_reduce_unsupported_version_isolated_005", - "trade_reduce_private_evidence_precedence_006", - "trade_reduce_attestation_deduplication_007", - ] { - assert!(ids.contains(required), "missing reducer vector {required}"); + .expect("reducer vector array"); + assert_eq!(vectors.len(), 7); + let mut ids = BTreeSet::new(); + + for vector in vectors { + let id = vector["id"].as_str().expect("reducer vector id"); + assert!(ids.insert(id), "duplicate reducer vector {id}"); + assert_eq!(vector["kind"], "trade.reduce_records", "{id}"); + assert!(vector["input"].is_object(), "{id}: input must be object"); + assert!( + vector["expected"].is_object(), + "{id}: expected must be object" + ); + + match id { + "trade_reduce_agreed_projection_digest_001" => { + reducer_digest_is_independent_of_input_order_and_duplicates(); + } + "trade_reduce_contested_claims_002" => { + reducer_preserves_contested_incompatible_acceptances_without_timestamp_winner(); + } + "trade_reduce_attestation_only_003" => { + reducer_attestation_never_commits_or_invalidates_agreement(); + } + "trade_reduce_missing_parent_004" => { + reducer_keeps_missing_parents_as_incomplete_evidence(); + } + "trade_reduce_unsupported_version_isolated_005" => { + reducer_excludes_unsupported_versions_from_domain_semantics(); + } + "trade_reduce_private_evidence_precedence_006" => { + reducer_private_evidence_precedence_is_permutation_independent(); + } + "trade_reduce_attestation_deduplication_007" => { + reducer_attestation_order_and_duplicates_do_not_change_digest(); + } + _ => panic!("unsupported reducer vector {id}"), + } } } diff --git a/crates/trade/src/validation_receipt.rs b/crates/trade/src/validation_receipt.rs @@ -2,11 +2,15 @@ #[cfg(not(feature = "std"))] use alloc::{ + borrow::ToOwned, format, string::{String, ToString}, + vec, vec::Vec, }; +use core::fmt; + use base64::Engine as _; use radroots_event::{ envelope::EventEnvelope, @@ -18,7 +22,6 @@ use radroots_event::{ use radroots_identity::PublicKey; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use thiserror::Error; pub const VALIDATION_RECEIPT_DOMAIN: &str = "radroots.receipt"; pub const VALIDATION_RECEIPT_VERSION: u32 = 1; @@ -369,30 +372,58 @@ pub struct RadrootsVerifiedValidationReceipt { pub tags: RadrootsValidationReceiptTags, } -#[derive(Clone, Debug, Error, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum RadrootsValidationReceiptError { - #[error("{0} cannot be empty")] EmptyField(&'static str), - #[error("invalid event kind {got}; expected {expected}")] InvalidKind { expected: u32, got: u32 }, - #[error("invalid validation receipt json")] InvalidJson, - #[error("validation receipt json is not canonical")] NonCanonicalJson, - #[error("invalid validation receipt field {0}")] InvalidField(&'static str), - #[error("invalid validation receipt proof metadata {0}")] InvalidProofMetadata(&'static str), - #[error("missing validation receipt tag {0}")] MissingTag(&'static str), - #[error("invalid validation receipt tag {0}")] InvalidTag(&'static str), - #[error("validation receipt tag {0} does not match content")] TagMismatch(&'static str), - #[error("validation receipt expected binding {0} does not match")] ExpectedBindingMismatch(&'static str), } +impl fmt::Display for RadrootsValidationReceiptError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::EmptyField(field) => write!(formatter, "{field} cannot be empty"), + Self::InvalidKind { expected, got } => { + write!(formatter, "invalid event kind {got}; expected {expected}") + } + Self::InvalidJson => formatter.write_str("invalid validation receipt json"), + Self::NonCanonicalJson => { + formatter.write_str("validation receipt json is not canonical") + } + Self::InvalidField(field) => { + write!(formatter, "invalid validation receipt field {field}") + } + Self::InvalidProofMetadata(field) => { + write!( + formatter, + "invalid validation receipt proof metadata {field}" + ) + } + Self::MissingTag(tag) => write!(formatter, "missing validation receipt tag {tag}"), + Self::InvalidTag(tag) => write!(formatter, "invalid validation receipt tag {tag}"), + Self::TagMismatch(tag) => { + write!( + formatter, + "validation receipt tag {tag} does not match content" + ) + } + Self::ExpectedBindingMismatch(field) => write!( + formatter, + "validation receipt expected binding {field} does not match" + ), + } + } +} + +impl core::error::Error for RadrootsValidationReceiptError {} + impl RadrootsValidatorSetV1 { pub fn validate(&self) -> Result<(), RadrootsValidationReceiptError> { validate_uuidv7(&self.set_id, "validator_set.set_id")?; diff --git a/crates/trade/src/workflow.rs b/crates/trade/src/workflow.rs @@ -282,6 +282,13 @@ mod tests { use radroots_identity::PublicKey; use radroots_test_fixtures::{FIXTURE_ALICE_PUBLIC_KEY_HEX, FIXTURE_BOB_PUBLIC_KEY_HEX}; + #[cfg(feature = "serde_json")] + const CANONICAL_WORKFLOW_VECTORS: &str = + include_str!("../../../contracts/conformance/vectors/trade/prepare_workflow.v1.json"); + #[cfg(feature = "serde_json")] + const PACKAGED_WORKFLOW_VECTORS: &str = + include_str!("../tests/fixtures/prepare_workflow.v1.json"); + fn pubkey(value: &str) -> PublicKey { PublicKey::from_hex(value).expect("fixture public key") } @@ -509,4 +516,107 @@ mod tests { ErrorKind::InvalidMutation ); } + + #[cfg(feature = "serde_json")] + #[test] + fn workflow_conformance_vectors_execute_every_mutation_and_error_case() { + assert_eq!(PACKAGED_WORKFLOW_VECTORS, CANONICAL_WORKFLOW_VECTORS); + let suite: serde_json::Value = + serde_json::from_str(PACKAGED_WORKFLOW_VECTORS).expect("workflow vector suite"); + assert_eq!(suite["suite"], "trade_workflow"); + assert_eq!(suite["contract_version"], "1.0.0"); + let vectors = suite["vectors"].as_array().expect("workflow vectors"); + assert_eq!(vectors.len(), 8); + let mut ids = std::collections::BTreeSet::new(); + + for vector in vectors { + let id = vector["id"].as_str().expect("workflow vector id"); + assert!(ids.insert(id), "duplicate workflow vector {id}"); + let mutation_fixture = vector["input"]["mutation"] + .as_str() + .expect("workflow mutation fixture"); + let result = match mutation_fixture { + "proposal_with_private_terms" => { + WorkflowPlan::prepare(all_operation_mutations().remove(0)) + } + "decision" => WorkflowPlan::prepare(all_operation_mutations().remove(1)), + "revision_proposal_with_private_terms" => { + WorkflowPlan::prepare(all_operation_mutations().remove(2)) + } + "revision_decision" => WorkflowPlan::prepare(all_operation_mutations().remove(3)), + "cancellation" => WorkflowPlan::prepare(all_operation_mutations().remove(4)), + "proposal_with_unsupported_schema" => { + let mut mutation = all_operation_mutations().remove(0); + mutation.schema_version += 1; + WorkflowPlan::prepare(mutation) + } + "proposal_without_mutation_id" => { + let mut mutation = all_operation_mutations().remove(0); + mutation.mutation_id = None; + WorkflowPlan::prepare(mutation) + } + "decision_without_parent" => { + let mut mutation = all_operation_mutations().remove(1); + mutation.parent_mutation_ids.clear(); + WorkflowPlan::prepare(mutation) + } + _ => panic!("{id}: unsupported mutation fixture {mutation_fixture}"), + }; + + let actual = match vector["kind"].as_str().expect("workflow vector kind") { + "trade.prepare_workflow.valid" => { + let plan = result.unwrap_or_else(|error| panic!("{id}: {error}")); + serde_json::json!({ + "mutation_kind": mutation_kind_label(plan.kind()), + "required_actions": plan + .required_actions() + .iter() + .copied() + .map(workflow_action_label) + .collect::<Vec<_>>(), + "private_terms_artifact_id": plan + .private_terms() + .map(PrivateTermsPlan::artifact_id), + }) + } + "trade.prepare_workflow.invalid" => { + let error = result.expect_err("invalid workflow vector must fail"); + serde_json::json!({ "error_kind": error_kind_label(error.kind()) }) + } + kind => panic!("{id}: unsupported workflow vector kind {kind}"), + }; + assert_eq!(actual, vector["expected"], "{id}"); + } + } + + #[cfg(feature = "serde_json")] + const fn mutation_kind_label(kind: TradeMutationKindV1) -> &'static str { + match kind { + TradeMutationKindV1::Proposal => "proposal", + TradeMutationKindV1::Decision => "decision", + TradeMutationKindV1::RevisionProposal => "revision_proposal", + TradeMutationKindV1::RevisionDecision => "revision_decision", + TradeMutationKindV1::Cancellation => "cancellation", + } + } + + #[cfg(feature = "serde_json")] + const fn workflow_action_label(action: WorkflowAction) -> &'static str { + match action { + WorkflowAction::Sign => "sign", + WorkflowAction::Persist => "persist", + WorkflowAction::Deliver => "deliver", + WorkflowAction::VerifyPrivateTerms => "verify_private_terms", + } + } + + #[cfg(feature = "serde_json")] + const fn error_kind_label(kind: ErrorKind) -> &'static str { + match kind { + ErrorKind::UnsupportedSchema => "unsupported_schema", + ErrorKind::MissingMutationId => "missing_mutation_id", + ErrorKind::InvalidMutation => "invalid_mutation", + ErrorKind::InvalidPrivateTerms => "invalid_private_terms", + } + } } diff --git a/crates/trade/tests/fixtures/prepare_workflow.v1.json b/crates/trade/tests/fixtures/prepare_workflow.v1.json @@ -0,0 +1,74 @@ +{ + "suite": "trade_workflow", + "contract_version": "1.0.0", + "vectors": [ + { + "id": "trade_prepare_workflow_proposal_private_terms_001", + "kind": "trade.prepare_workflow.valid", + "input": { "mutation": "proposal_with_private_terms" }, + "expected": { + "mutation_kind": "proposal", + "required_actions": ["verify_private_terms", "sign", "persist", "deliver"], + "private_terms_artifact_id": "artifact-1" + } + }, + { + "id": "trade_prepare_workflow_decision_002", + "kind": "trade.prepare_workflow.valid", + "input": { "mutation": "decision" }, + "expected": { + "mutation_kind": "decision", + "required_actions": ["sign", "persist", "deliver"], + "private_terms_artifact_id": null + } + }, + { + "id": "trade_prepare_workflow_revision_proposal_003", + "kind": "trade.prepare_workflow.valid", + "input": { "mutation": "revision_proposal_with_private_terms" }, + "expected": { + "mutation_kind": "revision_proposal", + "required_actions": ["verify_private_terms", "sign", "persist", "deliver"], + "private_terms_artifact_id": "artifact-1" + } + }, + { + "id": "trade_prepare_workflow_revision_decision_004", + "kind": "trade.prepare_workflow.valid", + "input": { "mutation": "revision_decision" }, + "expected": { + "mutation_kind": "revision_decision", + "required_actions": ["sign", "persist", "deliver"], + "private_terms_artifact_id": null + } + }, + { + "id": "trade_prepare_workflow_cancellation_005", + "kind": "trade.prepare_workflow.valid", + "input": { "mutation": "cancellation" }, + "expected": { + "mutation_kind": "cancellation", + "required_actions": ["sign", "persist", "deliver"], + "private_terms_artifact_id": null + } + }, + { + "id": "trade_prepare_workflow_unsupported_schema_006", + "kind": "trade.prepare_workflow.invalid", + "input": { "mutation": "proposal_with_unsupported_schema" }, + "expected": { "error_kind": "unsupported_schema" } + }, + { + "id": "trade_prepare_workflow_missing_mutation_id_007", + "kind": "trade.prepare_workflow.invalid", + "input": { "mutation": "proposal_without_mutation_id" }, + "expected": { "error_kind": "missing_mutation_id" } + }, + { + "id": "trade_prepare_workflow_invalid_transition_008", + "kind": "trade.prepare_workflow.invalid", + "input": { "mutation": "decision_without_parent" }, + "expected": { "error_kind": "invalid_mutation" } + } + ] +} diff --git a/crates/trade/tests/fixtures/reduce_records.v1.json b/crates/trade/tests/fixtures/reduce_records.v1.json @@ -0,0 +1,105 @@ +{ + "suite": "trade", + "contract_version": "1.0.0", + "vectors": [ + { + "id": "trade_reduce_agreed_projection_digest_001", + "kind": "trade.reduce_records", + "input": { + "shape": "proposal_plus_seller_acceptance_with_live_all_line_reservation", + "mutation_order": "arbitrary_with_duplicate_transport_observations", + "attestation_records": [] + }, + "expected": { + "negotiation_state": "open", + "agreement_state": "agreed", + "evidence_state": "complete", + "conflict_state": "none", + "attestation_state": "none", + "projection_digest": "35a5f555344febe675f4e5e6b15865356400b2b8f7791fc631590e0f1e1fd441" + } + }, + { + "id": "trade_reduce_contested_claims_002", + "kind": "trade.reduce_records", + "input": { + "shape": "proposal_plus_two_causally_unordered_incompatible_acceptances", + "winner_selection": "forbidden" + }, + "expected": { + "agreement_state": "contested", + "conflict_state": "double_acceptance", + "contested_claim_ids": "all_incompatible_claim_mutation_ids_sorted", + "forbidden_winner_inputs": [ + "timestamp", + "transport_event_id", + "relay_order", + "observation_order", + "first_seen" + ] + } + }, + { + "id": "trade_reduce_attestation_only_003", + "kind": "trade.reduce_records", + "input": { + "shape": "agreed_claim_plus_invalid_attestation_record" + }, + "expected": { + "agreement_state": "agreed", + "attestation_state": "present_invalid", + "attestation_agreement_authority": false + } + }, + { + "id": "trade_reduce_missing_parent_004", + "kind": "trade.reduce_records", + "input": { + "shape": "known_acceptance_with_missing_direct_parent" + }, + "expected": { + "evidence_state": "missing", + "agreement_claim_visibility": "retained_when_local_claim_is_known", + "global_invalidity": false + } + }, + { + "id": "trade_reduce_unsupported_version_isolated_005", + "kind": "trade.reduce_records", + "input": { + "shape": "unsupported_version_proposal_without_supported_records" + }, + "expected": { + "evidence_state": "unsupported_version", + "negotiation_state": "none", + "agreement_state": "none", + "unsupported_record_domain_authority": false + } + }, + { + "id": "trade_reduce_private_evidence_precedence_006", + "kind": "trade.reduce_records", + "input": { + "shape": "agreed_private_terms_candidate_with_conflicting_duplicate_evidence", + "evidence_order": "arbitrary" + }, + "expected": { + "private_terms_state": "commitment_mismatch", + "projection_digest": "input_order_independent" + } + }, + { + "id": "trade_reduce_attestation_deduplication_007", + "kind": "trade.reduce_records", + "input": { + "shape": "agreed_claim_with_duplicate_valid_and_distinct_invalid_attestations", + "attestation_order": "arbitrary" + }, + "expected": { + "attestation_state": "conflicting", + "duplicate_records": "removed", + "projection_digest": "input_order_independent" + } + } + ] +} diff --git a/crates/trade/tests/package_boundary.rs b/crates/trade/tests/package_boundary.rs @@ -7,8 +7,15 @@ const MANIFEST: &str = include_str!("../Cargo.toml"); const DRAFT: &str = include_str!("../src/operational_listing/draft.rs"); const IDENTITY: &str = include_str!("../src/identity.rs"); const MODEL: &str = include_str!("../src/model.rs"); +const OPERATIONS: &str = include_str!("../../../contracts/operations.toml"); const ROOT: &str = include_str!("../src/lib.rs"); const REDUCER_IMPLEMENTATION: &str = include_str!("../src/reducer_impl.rs"); +const CANONICAL_REDUCER_VECTORS: &str = + include_str!("../../../contracts/conformance/vectors/trade/reduce_records.v1.json"); +const PACKAGED_REDUCER_VECTORS: &str = include_str!("fixtures/reduce_records.v1.json"); +const CANONICAL_WORKFLOW_VECTORS: &str = + include_str!("../../../contracts/conformance/vectors/trade/prepare_workflow.v1.json"); +const PACKAGED_WORKFLOW_VECTORS: &str = include_str!("fixtures/prepare_workflow.v1.json"); const WORKFLOW: &str = include_str!("../src/workflow.rs"); const PACKAGE_TIERS: &str = include_str!("../../../contracts/releases/package_tiers.toml"); @@ -95,15 +102,79 @@ fn trade_feature_graph_has_no_persistence_or_sql_boundary() { let dev_dependencies = table_keys(MANIFEST, "[dev-dependencies]"); assert!(!features.contains("event_store")); - assert!(!dependencies.contains("radroots_event_store")); - assert!(!dependencies.contains("sqlx")); - assert!(!dev_dependencies.contains("sqlx")); - assert!(!dev_dependencies.contains("tokio")); + for forbidden in [ + "radroots_authority", + "radroots_event_store", + "radroots_outbox", + "radroots_transport", + "reqwest", + "sqlx", + "tokio", + ] { + assert!( + !dependencies.contains(forbidden), + "trade acquired forbidden production dependency {forbidden}" + ); + } + for forbidden in ["sqlx", "tokio"] { + assert!( + !dev_dependencies.contains(forbidden), + "trade retained forbidden development dependency {forbidden}" + ); + } assert!(!MANIFEST.contains("sqlite-bundled")); assert!(!MANIFEST.contains("runtime-tokio")); } #[test] +fn portable_root_exports_and_native_traits_are_compile_checked() { + use radroots_trade::{Error, Projection, ReducerIssue, ReductionInput, WorkflowPlan}; + + fn assert_portable<T: Clone + core::fmt::Debug + Eq + Send + Sync>() {} + fn assert_native_error<T: core::error::Error + Send + Sync>() {} + + assert_portable::<Projection>(); + assert_portable::<ReducerIssue>(); + assert_portable::<ReductionInput>(); + assert_portable::<WorkflowPlan>(); + assert_native_error::<Error>(); + + assert!(ROOT.contains("#![cfg_attr(not(feature = \"std\"), no_std)]")); + assert!(!ROOT.contains("pub trait ")); + for export in [ + "pub use model::RadrootsTradeProjectionV1 as Projection;", + "RadrootsTradeReducerIssueV1 as ReducerIssue", + "RadrootsTradeReductionInputV1 as ReductionInput", + "pub use workflow::{Error, WorkflowPlan};", + ] { + assert!( + ROOT.contains(export), + "missing canonical root export {export}" + ); + } +} + +#[test] +fn packaged_trade_vectors_match_canonical_operation_contracts() { + assert_eq!(PACKAGED_REDUCER_VECTORS, CANONICAL_REDUCER_VECTORS); + assert_eq!(PACKAGED_WORKFLOW_VECTORS, CANONICAL_WORKFLOW_VECTORS); + for required in [ + "id = \"trade.reduce_records\"", + "crates/trade/src/reducer.rs", + "radroots_trade::model::RadrootsTradeProjectionV1", + "id = \"trade.prepare_workflow\"", + "crates/trade/src/workflow.rs", + "contracts/conformance/vectors/trade/prepare_workflow.v1.json", + ] { + assert!( + OPERATIONS.contains(required), + "operation contract is missing {required}" + ); + } + assert!(!OPERATIONS.contains("radroots_trade::workflow::RadrootsTradeProjectionV1")); +} + +#[test] fn trade_model_reducer_and_evidence_have_final_public_owners() { use radroots_trade::{Projection, ReducerIssue, ReductionInput}; use radroots_trade::{evidence::RadrootsTradeEvidenceStateV1, reducer::reduce_trade_records};