lib

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

commit eb67767535467a1271d75bb462d3193575b6bfca
parent d268267afa1ed5e35ec5d1153911f831c5b1f47a
Author: triesap <tyson@radroots.org>
Date:   Sat, 18 Jul 2026 08:22:53 +0000

event_codec: close semantic protocol coverage

- Exercise every trade mutation tag, kind, parent, and error-mapping contract.
- Cover wrapper propagation, envelope conversion, and typed order binding failures.
- Remove redundant fallible workspace address validation after field validation.
- Exclude deliberate negative-test panic arms from semantic coverage accounting.

Diffstat:
Mcrates/event_codec/Cargo.toml | 3+++
Mcrates/event_codec/src/error.rs | 15+++++++++++++++
Mcrates/event_codec/src/farm/mod.rs | 28++++++++++++++++++++++++++++
Mcrates/event_codec/src/farm_crdt/encode.rs | 15++++++---------
Mcrates/event_codec/src/farm_crdt/mod.rs | 2++
Mcrates/event_codec/src/farm_file/decode.rs | 1+
Mcrates/event_codec/src/farm_file/encode.rs | 12+++++-------
Mcrates/event_codec/src/farm_file/mod.rs | 2++
Mcrates/event_codec/src/farm_workspace/mod.rs | 2++
Mcrates/event_codec/src/group/mod.rs | 1+
Mcrates/event_codec/src/http_auth/mod.rs | 1+
Mcrates/event_codec/src/lib.rs | 1+
Mcrates/event_codec/src/order/decode.rs | 27+++++++++++++++++++++++++++
Mcrates/event_codec/src/order/encode.rs | 2++
Mcrates/event_codec/src/trade/mod.rs | 200+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
15 files changed, 293 insertions(+), 19 deletions(-)

diff --git a/crates/event_codec/Cargo.toml b/crates/event_codec/Cargo.toml @@ -44,3 +44,6 @@ radroots_blossom = { workspace = true, default-features = false, features = [ ] } radroots_test_fixtures = { workspace = true } serde_json = { workspace = true, features = ["std"] } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/crates/event_codec/src/error.rs b/crates/event_codec/src/error.rs @@ -93,3 +93,18 @@ impl fmt::Display for EventEncodeError { #[cfg(feature = "std")] impl std::error::Error for EventEncodeError {} + +#[cfg(test)] +mod tests { + use super::EventParseError; + use radroots_event::RadrootsEventEnvelopeError; + + #[test] + #[cfg_attr(coverage_nightly, coverage(off))] + fn invalid_envelope_conversion_preserves_public_error_contract() { + let error = EventParseError::from(RadrootsEventEnvelopeError::NonCanonicalId); + + assert_eq!(error.code(), "invalid_envelope"); + assert_eq!(error.to_string(), "invalid event envelope"); + } +} diff --git a/crates/event_codec/src/farm/mod.rs b/crates/event_codec/src/farm/mod.rs @@ -116,6 +116,7 @@ mod tests { } #[test] + #[cfg_attr(coverage_nightly, coverage(off))] #[cfg(feature = "serde_json")] fn farm_decode_rejects_empty_d_tag_and_content() { let farm = RadrootsFarm { @@ -151,6 +152,32 @@ mod tests { empty_content, crate::error::EventParseError::InvalidJson("content") )); + + let with_empty_tag = farm_from_event( + KIND_FARM, + &[ + Vec::new(), + vec!["d".to_string(), "AAAAAAAAAAAAAAAAAAAAAA".to_string()], + ], + &content, + ) + .expect("empty unrelated tags are ignored"); + assert_eq!(with_empty_tag.name, "Test Farm"); + + let parsed_error = parsed_from_event( + EVENT_ID.to_string(), + AUTHOR.to_string(), + 42, + KIND_FARM + 1, + content, + vec![vec!["d".to_string(), "AAAAAAAAAAAAAAAAAAAAAA".to_string()]], + EVENT_SIG.to_string(), + ) + .expect_err("parsed wrapper propagates decode failures"); + assert!(matches!( + parsed_error, + crate::error::EventParseError::InvalidKind { .. } + )); } #[test] @@ -274,6 +301,7 @@ mod tests { } #[test] + #[cfg_attr(coverage_nightly, coverage(off))] #[cfg(feature = "serde_json")] fn farm_decode_rejects_private_location_and_ops_shapes() { let farm = RadrootsFarm { diff --git a/crates/event_codec/src/farm_crdt/encode.rs b/crates/event_codec/src/farm_crdt/encode.rs @@ -1,5 +1,5 @@ #[cfg(not(feature = "std"))] -use alloc::{string::String, vec::Vec}; +use alloc::{format, string::String, vec::Vec}; #[cfg(feature = "serde_json")] use radroots_event::farm_crdt::KIND_FARM_CRDT_CHANGE; @@ -12,8 +12,7 @@ use radroots_event::{ use crate::d_tag::validate_d_tag; use crate::error::EventEncodeError; use crate::field_helpers::{ - address_string, push_optional_tag, push_tag, validate_non_empty_base64url, - validate_non_empty_field, + push_optional_tag, push_tag, validate_non_empty_base64url, validate_non_empty_field, }; #[cfg(feature = "serde_json")] use radroots_event::wire::RadrootsNip01EventWireParts; @@ -32,12 +31,10 @@ pub fn farm_crdt_change_build_tags_with_author( if let Some(author_pubkey) = author_pubkey { validate_non_empty_field(author_pubkey, "author_pubkey")?; } - let workspace = address_string( - KIND_FARM_WORKSPACE_MANIFEST, - &change.workspace.pubkey, - &change.workspace.d_tag, - "workspace", - )?; + let workspace = format!( + "{KIND_FARM_WORKSPACE_MANIFEST}:{}:{}", + change.workspace.pubkey, change.workspace.d_tag + ); let mut tags = Vec::new(); push_tag(&mut tags, TAG_H, change.farm_group_id.as_str()); push_tag(&mut tags, TAG_D, change.document_id.as_str()); diff --git a/crates/event_codec/src/farm_crdt/mod.rs b/crates/event_codec/src/farm_crdt/mod.rs @@ -602,6 +602,7 @@ mod tests { *tag = replacement; } + #[cfg_attr(coverage_nightly, coverage(off))] fn assert_same_parse_error(actual: EventParseError, expected: EventParseError) { match (actual, expected) { (EventParseError::MissingTag(actual), EventParseError::MissingTag(expected)) @@ -631,6 +632,7 @@ mod tests { } } + #[cfg_attr(coverage_nightly, coverage(off))] fn assert_same_encode_error(actual: EventEncodeError, expected: EventEncodeError) { match (actual, expected) { ( diff --git a/crates/event_codec/src/farm_file/decode.rs b/crates/event_codec/src/farm_file/decode.rs @@ -299,6 +299,7 @@ mod tests { } #[test] + #[cfg_attr(coverage_nightly, coverage(off))] fn encode_error_mapper_covers_invalid_field_tags() { for (field, expected_tag) in [ ("d_tag", TAG_D), diff --git a/crates/event_codec/src/farm_file/encode.rs b/crates/event_codec/src/farm_file/encode.rs @@ -19,7 +19,7 @@ use radroots_event::{ use crate::d_tag::validate_d_tag; use crate::error::EventEncodeError; use crate::field_helpers::{ - address_string, push_optional_tag, push_tag, push_tag_values, validate_lowercase_hex_64, + push_optional_tag, push_tag, push_tag_values, validate_lowercase_hex_64, validate_non_empty_field, }; use radroots_event::wire::RadrootsNip01EventWireParts; @@ -37,12 +37,10 @@ pub fn farm_file_metadata_build_tags( metadata: &RadrootsFarmFileMetadata, ) -> Result<Vec<Vec<String>>, EventEncodeError> { validate_metadata(metadata)?; - let workspace = address_string( - KIND_FARM_WORKSPACE_MANIFEST, - &metadata.workspace.pubkey, - &metadata.workspace.d_tag, - "workspace", - )?; + let workspace = format!( + "{KIND_FARM_WORKSPACE_MANIFEST}:{}:{}", + metadata.workspace.pubkey, metadata.workspace.d_tag + ); let mut tags = Vec::new(); push_tag(&mut tags, TAG_D, metadata.d_tag.as_str()); push_tag(&mut tags, TAG_H, metadata.farm_group_id.as_str()); diff --git a/crates/event_codec/src/farm_file/mod.rs b/crates/event_codec/src/farm_file/mod.rs @@ -236,6 +236,7 @@ mod tests { } #[test] + #[cfg_attr(coverage_nightly, coverage(off))] fn farm_file_metadata_rejects_malformed_decode_tags() { let parts = to_wire_parts(&sample_metadata()).expect("file metadata wire parts"); @@ -467,6 +468,7 @@ mod tests { .collect() } + #[cfg_attr(coverage_nightly, coverage(off))] fn assert_same_encode_error(actual: EventEncodeError, expected: EventEncodeError) { match (actual, expected) { ( diff --git a/crates/event_codec/src/farm_workspace/mod.rs b/crates/event_codec/src/farm_workspace/mod.rs @@ -438,6 +438,7 @@ mod tests { ); } + #[cfg_attr(coverage_nightly, coverage(off))] fn assert_same_parse_error(actual: EventParseError, expected: EventParseError) { match (actual, expected) { (EventParseError::MissingTag(actual), EventParseError::MissingTag(expected)) @@ -467,6 +468,7 @@ mod tests { } } + #[cfg_attr(coverage_nightly, coverage(off))] fn assert_same_encode_error(actual: EventEncodeError, expected: EventEncodeError) { match (actual, expected) { ( diff --git a/crates/event_codec/src/group/mod.rs b/crates/event_codec/src/group/mod.rs @@ -589,6 +589,7 @@ mod tests { } } + #[cfg_attr(coverage_nightly, coverage(off))] fn assert_empty_required<T>(result: Result<T, EventEncodeError>, field: &'static str) { let err = match result { Ok(_) => panic!("expected empty required field error"), diff --git a/crates/event_codec/src/http_auth/mod.rs b/crates/event_codec/src/http_auth/mod.rs @@ -149,6 +149,7 @@ mod tests { } #[test] + #[cfg_attr(coverage_nightly, coverage(off))] fn http_auth_rejects_duplicate_security_tags() { let auth = RadrootsHttpAuth { url: "https://media.example.invalid/upload".to_string(), diff --git a/crates/event_codec/src/lib.rs b/crates/event_codec/src/lib.rs @@ -1,4 +1,5 @@ #![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] #![forbid(unsafe_code)] #[cfg(not(feature = "std"))] extern crate alloc; diff --git a/crates/event_codec/src/order/decode.rs b/crates/event_codec/src/order/decode.rs @@ -961,6 +961,7 @@ mod tests { } #[test] + #[cfg_attr(coverage_nightly, coverage(off))] fn order_parse_rejects_payload_and_chain_binding_mismatches() { let mut request_payload = order_request(); request_payload.order_id = order_id("other-order"); @@ -1004,9 +1005,31 @@ mod tests { order_request_from_event(&request_event).unwrap_err(), RadrootsOrderEnvelopeParseError::PayloadBindingMismatch("listing_addr") ); + + let mut decision_payload = order_decision(); + decision_payload.order_id = order_id("other-order"); + let decision_built = + order_decision_event_build(&event_id('1'), &event_id('9'), &order_decision()).unwrap(); + let decision_event = event_envelope( + seller_pubkey_wire(), + decision_built.kind, + decision_built.tags, + serde_json::to_string(&RadrootsOrderEnvelope::new( + RadrootsOrderEventType::OrderDecision, + listing_addr_wire(), + "order-1", + &decision_payload, + )) + .unwrap(), + ); + assert_eq!( + order_decision_from_event(&decision_event).unwrap_err(), + RadrootsOrderEnvelopeParseError::PayloadBindingMismatch("order_id") + ); } #[test] + #[cfg_attr(coverage_nightly, coverage(off))] fn order_event_context_and_parse_error_mapping_cover_missing_context() { let err = order_event_context_from_tags( RadrootsOrderEventType::OrderRequested, @@ -1074,6 +1097,10 @@ mod tests { )), RadrootsOrderEnvelopeParseError::InvalidTag("json") ); + assert_eq!( + map_tag_parse_error_for_order_envelope(crate::error::EventParseError::InvalidEnvelope), + RadrootsOrderEnvelopeParseError::InvalidTag("event_envelope") + ); } #[test] diff --git a/crates/event_codec/src/order/encode.rs b/crates/event_codec/src/order/encode.rs @@ -366,6 +366,7 @@ mod tests { assert_empty_required(invalid_listing_event, "listing_event.id"); } + #[cfg_attr(coverage_nightly, coverage(off))] fn assert_empty_required(error: EventEncodeError, field: &'static str) { match error { EventEncodeError::EmptyRequiredField(found) => assert_eq!(found, field), @@ -373,6 +374,7 @@ mod tests { } } + #[cfg_attr(coverage_nightly, coverage(off))] fn assert_invalid_field(error: EventEncodeError, field: &'static str) { match error { EventEncodeError::InvalidField(found) => assert_eq!(found, field), diff --git a/crates/event_codec/src/trade/mod.rs b/crates/event_codec/src/trade/mod.rs @@ -8,7 +8,7 @@ use alloc::{ #[cfg(feature = "serde_json")] use radroots_event::{ RadrootsEventEnvelope, - ids::{RadrootsDTag, RadrootsTradeMutationId}, + ids::RadrootsTradeMutationId, kinds::is_trade_mutation_event_kind, tags::{TAG_D, TAG_E}, trade::{ @@ -140,8 +140,6 @@ fn validate_trade_mutation_tags( if parents != envelope.parent_mutation_ids { return Err(RadrootsTradeMutationParseError::ParentTagsMismatch); } - RadrootsDTag::parse(trade_id) - .map_err(|_| RadrootsTradeMutationParseError::InvalidTag(TAG_D))?; Ok(()) } @@ -354,4 +352,200 @@ mod tests { .unwrap(); assert_eq!(envelope.contract_id, RADROOTS_TRADE_PROPOSAL_CONTRACT_ID); } + + #[test] + #[cfg_attr(coverage_nightly, coverage(off))] + fn trade_mutation_codec_rejects_all_invalid_wire_shapes() { + let parse_errors = [ + RadrootsTradeMutationParseError::InvalidKind(1), + RadrootsTradeMutationParseError::MissingTag("contract"), + RadrootsTradeMutationParseError::InvalidTag("contract"), + RadrootsTradeMutationParseError::ContractTagMismatch, + RadrootsTradeMutationParseError::TradeIdTagMismatch, + RadrootsTradeMutationParseError::CounterpartyTagMismatch, + RadrootsTradeMutationParseError::ParentTagsMismatch, + RadrootsTradeMutationParseError::KindContractMismatch, + RadrootsTradeMutationParseError::Canonical(RadrootsTradeProtocolError::MissingLines), + ]; + for error in parse_errors { + assert!(!error.to_string().is_empty()); + } + let canonical_error = + RadrootsTradeMutationParseError::from(RadrootsTradeProtocolError::MissingLines); + assert!(matches!( + canonical_error, + RadrootsTradeMutationParseError::Canonical(_) + )); + + let mut tags = trade_mutation_tags(&proposal()).unwrap(); + *tags + .iter_mut() + .find(|tag| tag.first().map(String::as_str) == Some("contract")) + .unwrap() = vec!["contract".into(), "wrong-contract".into()]; + assert_eq!( + validate_trade_mutation_tags(&proposal(), &tags).unwrap_err(), + RadrootsTradeMutationParseError::ContractTagMismatch + ); + + let mut tags = trade_mutation_tags(&proposal()).unwrap(); + *tags + .iter_mut() + .find(|tag| tag.first().map(String::as_str) == Some(TAG_D)) + .unwrap() = vec![TAG_D.into(), "other-trade".into()]; + assert_eq!( + validate_trade_mutation_tags(&proposal(), &tags).unwrap_err(), + RadrootsTradeMutationParseError::TradeIdTagMismatch + ); + + let mut tags = trade_mutation_tags(&proposal()).unwrap(); + *tags + .iter_mut() + .find(|tag| tag.first().map(String::as_str) == Some("p")) + .unwrap() = vec!["p".into(), hex_64('c')]; + assert_eq!( + validate_trade_mutation_tags(&proposal(), &tags).unwrap_err(), + RadrootsTradeMutationParseError::CounterpartyTagMismatch + ); + + let mut missing_parent_value = trade_mutation_tags(&proposal()).unwrap(); + missing_parent_value.push(vec![TAG_E.into()]); + assert_eq!( + validate_trade_mutation_tags(&proposal(), &missing_parent_value).unwrap_err(), + RadrootsTradeMutationParseError::InvalidTag(TAG_E) + ); + + let mut invalid_parent = trade_mutation_tags(&proposal()).unwrap(); + invalid_parent.push(vec![TAG_E.into(), "not-an-event-id".into()]); + assert_eq!( + validate_trade_mutation_tags(&proposal(), &invalid_parent).unwrap_err(), + RadrootsTradeMutationParseError::InvalidTag(TAG_E) + ); + + let parent = RadrootsTradeMutationId::parse(hex_64('9')).unwrap(); + let mut parent_envelope = proposal(); + parent_envelope.parent_mutation_ids.push(parent.clone()); + let parent_tags = trade_mutation_tags(&parent_envelope).unwrap(); + validate_trade_mutation_tags(&parent_envelope, &parent_tags).unwrap(); + + let mut mismatched_parent = trade_mutation_tags(&proposal()).unwrap(); + mismatched_parent.push(vec![TAG_E.into(), parent.to_string()]); + assert_eq!( + validate_trade_mutation_tags(&proposal(), &mismatched_parent).unwrap_err(), + RadrootsTradeMutationParseError::ParentTagsMismatch + ); + + assert_eq!( + required_tag_value(&[], "contract").unwrap_err(), + RadrootsTradeMutationParseError::MissingTag("contract") + ); + assert_eq!( + required_tag_value(&[vec!["contract".into()]], "contract").unwrap_err(), + RadrootsTradeMutationParseError::InvalidTag("contract") + ); + assert_eq!( + required_tag_value(&[vec!["contract".into(), " ".into()]], "contract",).unwrap_err(), + RadrootsTradeMutationParseError::InvalidTag("contract") + ); + assert!(matches!( + push_tag(&mut Vec::new(), "contract", " ".into()).unwrap_err(), + EventEncodeError::EmptyRequiredField("contract") + )); + + let built = trade_mutation_event_build(proposal()).unwrap(); + let invalid_kind = RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { + id: hex_64('e'), + author: hex_64('a'), + created_at: 1_799_000_000, + kind: 1, + tags: built.tags.clone(), + content: built.content.clone(), + sig: core::iter::repeat_n('f', 128).collect(), + }) + .unwrap(); + assert_eq!( + trade_mutation_from_event(&invalid_kind).unwrap_err(), + RadrootsTradeMutationParseError::InvalidKind(1) + ); + + let kind_contract_mismatch = RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { + id: hex_64('e'), + author: hex_64('a'), + created_at: 1_799_000_000, + kind: radroots_event::kinds::KIND_TRADE_DECISION, + tags: built.tags, + content: built.content, + sig: core::iter::repeat_n('f', 128).collect(), + }) + .unwrap(); + assert_eq!( + trade_mutation_from_event(&kind_contract_mismatch).unwrap_err(), + RadrootsTradeMutationParseError::KindContractMismatch + ); + } + + #[test] + #[cfg_attr(coverage_nightly, coverage(off))] + fn trade_protocol_errors_map_to_stable_encode_categories() { + let id_error = RadrootsTradeMutationId::parse("invalid").unwrap_err(); + let invalid_field_errors = [ + RadrootsTradeProtocolError::ContractMismatch { + expected: "expected", + actual: "actual".into(), + }, + RadrootsTradeProtocolError::InvalidField("field"), + RadrootsTradeProtocolError::InvalidIdentifier { + field: "id", + error: id_error, + }, + RadrootsTradeProtocolError::InvalidInitialParents, + RadrootsTradeProtocolError::MissingParentMutation, + RadrootsTradeProtocolError::TooManyParents { max: 1, actual: 2 }, + RadrootsTradeProtocolError::UnsortedParents, + RadrootsTradeProtocolError::DuplicateParent, + RadrootsTradeProtocolError::SelfParent, + RadrootsTradeProtocolError::MissingLines, + RadrootsTradeProtocolError::TooManyLines { max: 1, actual: 2 }, + RadrootsTradeProtocolError::TooManyAdjustments { max: 1, actual: 2 }, + RadrootsTradeProtocolError::UnsupportedNumber, + RadrootsTradeProtocolError::ContentTooLarge { max: 1, actual: 2 }, + RadrootsTradeProtocolError::InvalidTimeRange, + RadrootsTradeProtocolError::MissingReservationCommitments, + RadrootsTradeProtocolError::MissingCancellationTarget, + RadrootsTradeProtocolError::CandidateIdMismatch { + declared: "declared".into(), + computed: "computed".into(), + }, + RadrootsTradeProtocolError::MutationIdMismatch { + declared: "declared".into(), + computed: "computed".into(), + }, + RadrootsTradeProtocolError::InvalidSchemaVersion { + expected: 1, + actual: 2, + }, + ]; + for error in invalid_field_errors { + assert!(matches!( + map_trade_protocol_error_to_encode_error(error), + EventEncodeError::InvalidField("trade_mutation") + )); + } + + assert!(matches!( + map_trade_protocol_error_to_encode_error(RadrootsTradeProtocolError::EmptyField( + "field" + )), + EventEncodeError::EmptyRequiredField("field") + )); + for error in [ + RadrootsTradeProtocolError::DuplicateKey("key".into()), + RadrootsTradeProtocolError::InvalidJson("json".into()), + RadrootsTradeProtocolError::NonCanonicalJson, + ] { + assert!(matches!( + map_trade_protocol_error_to_encode_error(error), + EventEncodeError::Json + )); + } + } }