lib

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

tests.rs (69012B)


      1 use super::*;
      2 use crate::envelope::EventEnvelopeParts;
      3 use std::collections::BTreeSet;
      4 
      5 static AMBIGUOUS_TEST_CONTRACTS: &[EventContract] = &[
      6     event_contract!(
      7         "radroots.test.one.v1",
      8         KIND_POST,
      9         "Test One",
     10         "Test",
     11         EventClass::Regular,
     12         EventPrivacy::Public,
     13         AuthorRole::Any,
     14         ContentSchema::PlainText,
     15         EventDiscriminator::KindOnly,
     16         NO_TAGS,
     17         SOCIAL_REDUCERS,
     18     ),
     19     event_contract!(
     20         "radroots.test.two.v1",
     21         KIND_POST,
     22         "Test Two",
     23         "Test",
     24         EventClass::Regular,
     25         EventPrivacy::Public,
     26         AuthorRole::Any,
     27         ContentSchema::PlainText,
     28         EventDiscriminator::KindOnly,
     29         NO_TAGS,
     30         SOCIAL_REDUCERS,
     31     ),
     32 ];
     33 
     34 static REQUIRED_MANY_TEST_TAGS: &[TagContract] = &[tag(
     35     "test_many",
     36     TagCardinality::RequiredMany,
     37     TagSemantic::Topic,
     38     TagValueType::Text,
     39     false,
     40 )];
     41 
     42 static OPTIONAL_ONE_TEST_TAGS: &[TagContract] = &[tag(
     43     "test_optional",
     44     TagCardinality::OptionalOne,
     45     TagSemantic::Topic,
     46     TagValueType::Text,
     47     false,
     48 )];
     49 
     50 static DUPLICATE_REQUIRED_TEST_TAGS: &[TagContract] = &[
     51     tag(
     52         "test_required",
     53         TagCardinality::RequiredOne,
     54         TagSemantic::Topic,
     55         TagValueType::Text,
     56         false,
     57     ),
     58     tag(
     59         "test_required",
     60         TagCardinality::RequiredOne,
     61         TagSemantic::Category,
     62         TagValueType::Text,
     63         false,
     64     ),
     65 ];
     66 
     67 static DUPLICATE_OPTIONAL_TEST_TAGS: &[TagContract] = &[
     68     tag(
     69         "test_optional",
     70         TagCardinality::OptionalOne,
     71         TagSemantic::Topic,
     72         TagValueType::Text,
     73         false,
     74     ),
     75     tag(
     76         "test_optional",
     77         TagCardinality::OptionalOne,
     78         TagSemantic::Category,
     79         TagValueType::Text,
     80         false,
     81     ),
     82 ];
     83 
     84 fn synthetic_event_contract(id: &'static str, tags: &'static [TagContract]) -> EventContract {
     85     EventContract {
     86         id,
     87         kind: KIND_POST,
     88         name: "Test",
     89         payload_type: "Test",
     90         class: EventClass::Regular,
     91         stability: EventStability::Experimental,
     92         privacy: EventPrivacy::Public,
     93         required_author_role: AuthorRole::Any,
     94         content_schema: ContentSchema::PlainText,
     95         authoring_policy: EventAuthoringPolicy::GenericDraft,
     96         discriminator: EventDiscriminator::KindOnly,
     97         tags,
     98         reducers: SOCIAL_REDUCERS,
     99     }
    100 }
    101 
    102 fn synthetic_kind_contract(kind: u32) -> KindContract {
    103     KindContract {
    104         kind,
    105         canonical_constant: "KIND_TEST",
    106         name: "Test",
    107         class: EventClass::Regular,
    108         standard: NostrStandard::Radroots,
    109         accepted_event_contracts: &[],
    110     }
    111 }
    112 
    113 #[test]
    114 fn author_role_catalog_is_event_owned_complete_and_schema_stable() {
    115     let declared = AuthorRole::ALL.into_iter().collect::<BTreeSet<_>>();
    116     let used = all_event_contracts_registry_v7()
    117         .iter()
    118         .map(EventContract::required_author_role)
    119         .collect::<BTreeSet<_>>();
    120     let labels = AuthorRole::ALL
    121         .into_iter()
    122         .map(AuthorRole::as_str)
    123         .collect::<BTreeSet<_>>();
    124 
    125     assert_eq!(used, declared, "every event author role must be exercised");
    126     assert_eq!(labels.len(), AuthorRole::ALL.len());
    127     assert!(
    128         labels.iter().all(|label| {
    129             !label.is_empty()
    130                 && label
    131                     .bytes()
    132                     .all(|byte| byte.is_ascii_lowercase() || byte == b'_')
    133         }),
    134         "author-role manifest labels must remain canonical snake case"
    135     );
    136 }
    137 
    138 #[test]
    139 fn authoring_policy_matrix_is_centralized_and_complete() {
    140     let generic = EventAuthoringPolicy::GenericDraft;
    141     assert!(generic.permits_generic_draft());
    142     assert!(generic.permits_typed_authoring());
    143     assert!(!generic.is_read_only());
    144 
    145     let typed = EventAuthoringPolicy::TypedOnly;
    146     assert!(!typed.permits_generic_draft());
    147     assert!(typed.permits_typed_authoring());
    148     assert!(!typed.is_read_only());
    149 
    150     let read_only = EventAuthoringPolicy::ReadOnly;
    151     assert!(!read_only.permits_generic_draft());
    152     assert!(!read_only.permits_typed_authoring());
    153     assert!(read_only.is_read_only());
    154 }
    155 
    156 fn unsigned_event(kind: u32, tags: Vec<Vec<&str>>, content: &str) -> EventEnvelope {
    157     EventEnvelope::new(EventEnvelopeParts {
    158         id: "0".repeat(64),
    159         author: crate::test_valid_hex_64('1'),
    160         created_at: 1_700_000_000,
    161         kind,
    162         tags: tags
    163             .into_iter()
    164             .map(|tag| tag.into_iter().map(ToOwned::to_owned).collect())
    165             .collect(),
    166         content: content.to_owned(),
    167         sig: "2".repeat(128),
    168     })
    169     .expect("event envelope")
    170 }
    171 
    172 fn unsigned_event_owned(kind: u32, tags: Vec<Vec<String>>, content: &str) -> EventEnvelope {
    173     EventEnvelope::new(EventEnvelopeParts {
    174         id: "0".repeat(64),
    175         author: crate::test_valid_hex_64('1'),
    176         created_at: 1_700_000_000,
    177         kind,
    178         tags,
    179         content: content.to_owned(),
    180         sig: "2".repeat(128),
    181     })
    182     .expect("event envelope")
    183 }
    184 
    185 fn hex_64(character: char) -> String {
    186     crate::test_valid_hex_64(character)
    187 }
    188 
    189 fn event_ref_tag(name: &str, event_id: &str, author: &str, kind: u32) -> Vec<String> {
    190     vec![
    191         name.to_owned(),
    192         event_id.to_owned(),
    193         author.to_owned(),
    194         kind.to_string(),
    195         String::new(),
    196     ]
    197 }
    198 
    199 fn owned_tag(values: &[&str]) -> Vec<String> {
    200     values.iter().map(|value| (*value).to_owned()).collect()
    201 }
    202 
    203 #[test]
    204 fn exposes_one_kind_contract_per_supported_kind() {
    205     let mut kinds = BTreeSet::new();
    206     for contract in all_kind_contracts() {
    207         assert!(
    208             kinds.insert(contract.kind),
    209             "duplicate kind {}",
    210             contract.kind
    211         );
    212         assert!(!contract.accepted_event_contracts.is_empty());
    213     }
    214 }
    215 
    216 #[test]
    217 fn exposes_unique_event_contract_ids() {
    218     let mut ids = BTreeSet::new();
    219     for contract in all_event_contracts() {
    220         assert!(
    221             ids.insert(contract.id),
    222             "duplicate event contract {}",
    223             contract.id
    224         );
    225         assert!(kind_contract(contract.kind).is_some());
    226     }
    227 }
    228 
    229 #[test]
    230 fn every_kind_references_known_matching_event_contracts() {
    231     for kind in all_kind_contracts() {
    232         for id in kind.accepted_event_contracts {
    233             let event = event_contract(id).expect("accepted event contract");
    234             assert_eq!(event.kind, kind.kind, "{}", id);
    235         }
    236     }
    237 }
    238 
    239 #[test]
    240 fn event_contract_classes_match_kind_contracts() {
    241     for contract in all_event_contracts() {
    242         let kind = kind_contract(contract.kind).expect("event kind contract");
    243         assert_eq!(contract.class, kind.class, "{}", contract.id);
    244     }
    245 }
    246 
    247 #[test]
    248 fn every_event_contract_is_listed_by_its_kind_contract() {
    249     for contract in all_event_contracts() {
    250         let kind = kind_contract(contract.kind).expect("event kind contract");
    251         assert!(
    252             kind.accepted_event_contracts.contains(&contract.id),
    253             "{}",
    254             contract.id
    255         );
    256     }
    257 }
    258 
    259 #[test]
    260 fn calendar_contracts_expose_current_nip52_types_content_and_tags() {
    261     let date = event_contract("radroots.calendar.date_event.v1").expect("calendar date");
    262     assert_eq!(
    263         date.payload_type,
    264         "RadrootsAuthoredCalendarDateEvent / RadrootsParsedNip52CalendarDateEvent / RadrootsAdmittedCalendarDateEvent"
    265     );
    266     assert_eq!(date.content_schema, ContentSchema::PlainText);
    267     assert_eq!(date.authoring_policy(), EventAuthoringPolicy::TypedOnly);
    268     assert!(
    269         date.tags
    270             .iter()
    271             .any(|tag| { tag.name == "title" && tag.cardinality == TagCardinality::RequiredOne })
    272     );
    273     assert!(
    274         date.tags
    275             .iter()
    276             .any(|tag| { tag.name == "name" && tag.cardinality == TagCardinality::OptionalOne })
    277     );
    278     assert!(
    279         date.tags
    280             .iter()
    281             .any(|tag| tag.name == "start" && tag.value_type == TagValueType::CalendarDate)
    282     );
    283     assert!(!date.tags.iter().any(|tag| tag.name == "D"));
    284 
    285     let time = event_contract("radroots.calendar.time_event.v1").expect("calendar time");
    286     assert_eq!(
    287         time.payload_type,
    288         "RadrootsAuthoredCalendarTimeEvent / RadrootsParsedNip52CalendarTimeEvent / RadrootsAdmittedCalendarTimeEvent"
    289     );
    290     assert_eq!(time.content_schema, ContentSchema::PlainText);
    291     assert_eq!(time.authoring_policy(), EventAuthoringPolicy::TypedOnly);
    292     assert!(time.tags.iter().any(|tag| {
    293         tag.name == "D"
    294             && tag.cardinality == TagCardinality::RequiredMany
    295             && tag.value_type == TagValueType::UtcDayIndex
    296     }));
    297     assert!(
    298         time.tags
    299             .iter()
    300             .any(|tag| { tag.name == "name" && tag.cardinality == TagCardinality::OptionalOne })
    301     );
    302 
    303     let calendar = event_contract("radroots.calendar.collection.v1").expect("calendar");
    304     assert_eq!(calendar.payload_type, "RadrootsAdmittedCalendar");
    305     assert_eq!(calendar.content_schema, ContentSchema::PlainText);
    306     assert!(calendar.tags.iter().any(|tag| {
    307         tag.name == "a"
    308             && tag.cardinality == TagCardinality::OptionalMany
    309             && tag.value_type == TagValueType::CalendarEventCoordinate
    310     }));
    311 
    312     let rsvp = event_contract("radroots.calendar.rsvp.v1").expect("calendar RSVP");
    313     assert_eq!(rsvp.payload_type, "RadrootsAdmittedCalendarEventRsvp");
    314     assert_eq!(rsvp.content_schema, ContentSchema::PlainText);
    315     assert!(rsvp.tags.iter().any(|tag| {
    316         tag.name == "status"
    317             && tag.cardinality == TagCardinality::RequiredOne
    318             && tag.value_type == TagValueType::CalendarRsvpStatus
    319     }));
    320 }
    321 
    322 #[test]
    323 fn calendar_collection_and_rsvp_contracts_enforce_strict_nip52_shapes() {
    324     let author = "a".repeat(64);
    325     let event_coordinate = format!("31923:{author}:wash-pack");
    326     let second_coordinate = format!("31922:{author}:market-day");
    327     let event_id = "b".repeat(64);
    328     let blossom_image = format!("https://media.example/{}.webp", "c".repeat(64));
    329     let collection = vec![
    330         owned_tag(&["d", "AAAAAAAAAAAAAAAAAAAAAA"]),
    331         owned_tag(&["title", "Farm calendar"]),
    332         owned_tag(&["description", "Upcoming farm work"]),
    333         vec!["image".to_owned(), blossom_image],
    334         vec![
    335             "a".to_owned(),
    336             event_coordinate.clone(),
    337             "wss://relay.example/events".to_owned(),
    338         ],
    339         vec!["a".to_owned(), second_coordinate],
    340     ];
    341     assert_eq!(
    342         validate_event_contract_parts(
    343             KIND_CALENDAR,
    344             &collection,
    345             "Detailed shared schedule.",
    346             "radroots.calendar.collection.v1",
    347         ),
    348         Ok(())
    349     );
    350 
    351     let mut duplicate_collection = collection.clone();
    352     duplicate_collection.push(vec!["a".to_owned(), event_coordinate.clone()]);
    353     assert!(matches!(
    354         validate_event_contract_parts(
    355             KIND_CALENDAR,
    356             &duplicate_collection,
    357             "",
    358             "radroots.calendar.collection.v1",
    359         ),
    360         Err(ContractValidationError::TagValueMismatch { name: "a", .. })
    361     ));
    362 
    363     let rsvp = vec![
    364         owned_tag(&["d", "AAAAAAAAAAAAAAAAAAAAAQ"]),
    365         vec![
    366             "a".to_owned(),
    367             event_coordinate.clone(),
    368             "wss://relay.example/a".to_owned(),
    369         ],
    370         vec!["e".to_owned(), event_id, "wss://relay.example/e".to_owned()],
    371         owned_tag(&["status", "tentative"]),
    372         owned_tag(&["fb", "busy"]),
    373         vec![
    374             "p".to_owned(),
    375             author.clone(),
    376             "wss://relay.example/p".to_owned(),
    377         ],
    378     ];
    379     assert_eq!(
    380         validate_event_contract_parts(
    381             KIND_CALENDAR_EVENT_RSVP,
    382             &rsvp,
    383             "I expect to attend.",
    384             "radroots.calendar.rsvp.v1",
    385         ),
    386         Ok(())
    387     );
    388 
    389     let mut mismatched_author = rsvp.clone();
    390     let participant = mismatched_author
    391         .iter_mut()
    392         .find(|tag| tag.first().map(String::as_str) == Some("p"))
    393         .unwrap();
    394     participant[1] = "d".repeat(64);
    395     assert!(matches!(
    396         validate_event_contract_parts(
    397             KIND_CALENDAR_EVENT_RSVP,
    398             &mismatched_author,
    399             "",
    400             "radroots.calendar.rsvp.v1",
    401         ),
    402         Err(ContractValidationError::TagValueMismatch { name: "p", .. })
    403     ));
    404 
    405     let declined_with_observed_free_busy = vec![
    406         owned_tag(&["d", "AAAAAAAAAAAAAAAAAAAAAg"]),
    407         vec!["a".to_owned(), event_coordinate],
    408         owned_tag(&["status", "declined"]),
    409         owned_tag(&["fb", "free"]),
    410     ];
    411     assert_eq!(
    412         validate_event_contract_parts(
    413             KIND_CALENDAR_EVENT_RSVP,
    414             &declined_with_observed_free_busy,
    415             "",
    416             "radroots.calendar.rsvp.v1",
    417         ),
    418         Ok(())
    419     );
    420 }
    421 
    422 #[test]
    423 fn calendar_date_contract_validates_gregorian_exclusive_ranges_without_day_tags() {
    424     let valid = vec![
    425         owned_tag(&["d", "market-day"]),
    426         owned_tag(&["title", "Market day"]),
    427         owned_tag(&["start", "2026-06-20"]),
    428         owned_tag(&["end", "2026-06-21"]),
    429         owned_tag(&["location", "Moss Street Market"]),
    430         owned_tag(&["location", "Victoria, BC"]),
    431     ];
    432     assert_eq!(
    433         validate_event_contract_parts(
    434             KIND_CALENDAR_DATE_EVENT,
    435             &valid,
    436             "Farm stand pickup window.",
    437             "radroots.calendar.date_event.v1",
    438         ),
    439         Ok(())
    440     );
    441 
    442     let duplicate_legacy_name = vec![
    443         owned_tag(&["d", "market-day"]),
    444         owned_tag(&["title", "Market day"]),
    445         owned_tag(&["name", "Market day"]),
    446         owned_tag(&["name", "Deprecated duplicate"]),
    447         owned_tag(&["start", "2026-06-20"]),
    448     ];
    449     assert!(matches!(
    450         validate_event_contract_parts(
    451             KIND_CALENDAR_DATE_EVENT,
    452             &duplicate_legacy_name,
    453             "description",
    454             "radroots.calendar.date_event.v1",
    455         ),
    456         Err(ContractValidationError::TagCardinalityMismatch { name: "name", .. })
    457     ));
    458 
    459     for invalid in [
    460         vec![
    461             owned_tag(&["d", "market-day"]),
    462             owned_tag(&["title", "Market day"]),
    463             owned_tag(&["start", "2026-02-29"]),
    464         ],
    465         vec![
    466             owned_tag(&["d", "market-day"]),
    467             owned_tag(&["title", "Market day"]),
    468             owned_tag(&["start", "2026-06-20"]),
    469             owned_tag(&["end", "2026-06-20"]),
    470         ],
    471         vec![
    472             owned_tag(&["d", "market-day"]),
    473             owned_tag(&["title", "Market day"]),
    474             owned_tag(&["start", "2026-06-20"]),
    475             owned_tag(&["D", "20624"]),
    476         ],
    477     ] {
    478         assert!(matches!(
    479             validate_event_contract_parts(
    480                 KIND_CALENDAR_DATE_EVENT,
    481                 &invalid,
    482                 "description",
    483                 "radroots.calendar.date_event.v1",
    484             ),
    485             Err(ContractValidationError::TagValueMismatch { .. })
    486         ));
    487     }
    488 }
    489 
    490 #[test]
    491 fn calendar_time_contract_requires_exact_derived_bounded_day_coverage() {
    492     let valid = vec![
    493         owned_tag(&["d", "wash-pack"]),
    494         owned_tag(&["title", "Wash and pack"]),
    495         owned_tag(&["start", "86399"]),
    496         owned_tag(&["end", "86401"]),
    497         owned_tag(&["D", "0"]),
    498         owned_tag(&["D", "1"]),
    499     ];
    500     assert_eq!(
    501         validate_event_contract_parts(
    502             KIND_CALENDAR_TIME_EVENT,
    503             &valid,
    504             "Pack CSA shares.",
    505             "radroots.calendar.time_event.v1",
    506         ),
    507         Ok(())
    508     );
    509 
    510     for invalid in [
    511         vec![
    512             owned_tag(&["d", "wash-pack"]),
    513             owned_tag(&["title", "Wash and pack"]),
    514             owned_tag(&["start", "086399"]),
    515             owned_tag(&["D", "0"]),
    516         ],
    517         vec![
    518             owned_tag(&["d", "wash-pack"]),
    519             owned_tag(&["title", "Wash and pack"]),
    520             owned_tag(&["start", "86399"]),
    521             owned_tag(&["end", "86401"]),
    522             owned_tag(&["D", "1"]),
    523             owned_tag(&["D", "0"]),
    524         ],
    525         vec![
    526             owned_tag(&["d", "wash-pack"]),
    527             owned_tag(&["title", "Wash and pack"]),
    528             owned_tag(&["start", "0"]),
    529             owned_tag(&["end", "31708800"]),
    530             owned_tag(&["D", "0"]),
    531         ],
    532     ] {
    533         assert!(matches!(
    534             validate_event_contract_parts(
    535                 KIND_CALENDAR_TIME_EVENT,
    536                 &invalid,
    537                 "description",
    538                 "radroots.calendar.time_event.v1",
    539             ),
    540             Err(ContractValidationError::TagValueMismatch { .. })
    541         ));
    542     }
    543 }
    544 
    545 #[test]
    546 fn trade_mutation_contract_requires_exact_contract_tag() {
    547     for contract_id in [
    548         "radroots.trade.proposal.v1",
    549         "radroots.trade.decision.v1",
    550         "radroots.trade.revision_proposal.v1",
    551         "radroots.trade.revision_decision.v1",
    552         "radroots.trade.cancellation.v1",
    553     ] {
    554         let contract = event_contract(contract_id).expect("trade mutation contract");
    555         assert_eq!(contract.authoring_policy(), EventAuthoringPolicy::TypedOnly);
    556         assert_eq!(
    557             contract.tags.iter().map(|tag| tag.name).collect::<Vec<_>>(),
    558             ["contract", "d", "x", "p"]
    559         );
    560         let contract_tag = &contract.tags[0];
    561         assert_eq!(contract_tag.semantic, TagSemantic::Contract);
    562         assert_eq!(contract_tag.value_type, TagValueType::ContractId);
    563         assert!(!contract_tag.relay_indexed);
    564         let mutation = &contract.tags[2];
    565         assert_eq!(mutation.cardinality, TagCardinality::RequiredMany);
    566         assert_eq!(mutation.semantic, TagSemantic::TradeMutation);
    567         assert_eq!(mutation.value_type, TagValueType::MutationId);
    568         assert!(mutation.relay_indexed);
    569         let parties = &contract.tags[3];
    570         assert_eq!(parties.cardinality, TagCardinality::RequiredMany);
    571         assert_eq!(parties.semantic, TagSemantic::Participant);
    572         assert_eq!(parties.value_type, TagValueType::PublicKey);
    573     }
    574 }
    575 
    576 #[test]
    577 fn covers_public_kind_arrays() {
    578     for kind in COMMERCIAL_EVENT_KINDS
    579         .iter()
    580         .chain(PUBLIC_SOCIAL_KINDS.iter())
    581         .chain(PRIVATE_FARM_OPS_KINDS.iter())
    582         .chain(NIP29_GROUP_KINDS.iter())
    583         .chain(KNOWLEDGE_EVENT_KINDS.iter())
    584     {
    585         assert!(kind_contract(*kind).is_some(), "missing kind {kind}");
    586     }
    587 }
    588 
    589 #[test]
    590 fn classified_listing_kind_profiles_are_partitioned_and_explicit() {
    591     let kind = kind_contract(KIND_CLASSIFIED_LISTING).expect("classified listing kind");
    592     assert_eq!(kind.canonical_constant, "KIND_CLASSIFIED_LISTING");
    593     assert_eq!(kind.name, "Classified Listing");
    594     assert_eq!(kind.class, EventClass::Addressable);
    595     assert_eq!(kind.standard, NostrStandard::Nip99);
    596     assert_eq!(
    597         kind.accepted_event_contracts,
    598         &[
    599             "radroots.operational_listing.published.v1",
    600             "radroots.food.availability.v1",
    601         ]
    602     );
    603 
    604     let operational = event_contract("radroots.operational_listing.published.v1")
    605         .expect("operational listing profile");
    606     assert_eq!(operational.name, "Operational Listing");
    607     assert_eq!(operational.payload_type, "RadrootsOperationalListing");
    608     assert_eq!(operational.content_schema, ContentSchema::Markdown);
    609     assert_eq!(
    610         operational.discriminator,
    611         EventDiscriminator::ClassifiedListingPartition(
    612             ClassifiedListingPartition::OperationalListing,
    613         )
    614     );
    615 
    616     let food =
    617         event_contract("radroots.food.availability.v1").expect("focused food availability profile");
    618     assert_eq!(food.name, "Food Availability");
    619     assert_eq!(
    620         food.payload_type,
    621         "RadrootsFoodAvailabilityDetails / RadrootsInboundFoodAvailabilityProjection"
    622     );
    623     assert_eq!(food.class, EventClass::Addressable);
    624     assert_eq!(food.privacy, EventPrivacy::Public);
    625     assert_eq!(food.required_author_role(), AuthorRole::Seller);
    626     assert_eq!(food.content_schema, ContentSchema::Markdown);
    627     assert_eq!(food.authoring_policy(), EventAuthoringPolicy::TypedOnly);
    628     assert_eq!(food.discriminator, EventDiscriminator::AdmissionOnly);
    629     assert_eq!(food.reducers, &[Reducer::MarketProjection]);
    630     assert_eq!(event_contract_family(food), Some(ContractFamily::Market));
    631     assert_eq!(
    632         food.tags
    633             .iter()
    634             .map(|tag| (tag.name, tag.cardinality))
    635             .collect::<Vec<_>>(),
    636         vec![
    637             ("d", TagCardinality::RequiredOne),
    638             ("title", TagCardinality::RequiredOne),
    639             ("summary", TagCardinality::RequiredOne),
    640             ("published_at", TagCardinality::RequiredOne),
    641             ("location", TagCardinality::RequiredOne),
    642             ("price", TagCardinality::RequiredOne),
    643             ("radroots:price_unit", TagCardinality::RequiredOne),
    644             ("radroots:quantity", TagCardinality::OptionalOne),
    645             ("status", TagCardinality::RequiredOne),
    646             ("image", TagCardinality::OptionalMany),
    647         ]
    648     );
    649     assert!(event_contract("radroots.listing.published.v1").is_none());
    650 
    651     let seller = "a".repeat(64);
    652     let tags = vec![
    653         owned_tag(&["d", "carrots"]),
    654         owned_tag(&["p", seller.as_str()]),
    655         owned_tag(&["a", format!("30340:{seller}:victoria-farm").as_str()]),
    656         owned_tag(&["key", "carrots"]),
    657         owned_tag(&["title", "Carrots"]),
    658         owned_tag(&["category", "produce"]),
    659         owned_tag(&["radroots:primary_bin", "field-bin"]),
    660         owned_tag(&["radroots:bin", "field-bin", "1000", "g"]),
    661         owned_tag(&["radroots:price", "field-bin", "3", "CAD", "1", "lb"]),
    662     ];
    663     assert_eq!(
    664         validate_event_contract_parts(
    665             KIND_CLASSIFIED_LISTING,
    666             &tags,
    667             "# Carrots",
    668             "radroots.operational_listing.published.v1",
    669         ),
    670         Ok(())
    671     );
    672     assert_eq!(
    673         identify_event_contract(KIND_CLASSIFIED_LISTING, &tags, "# Carrots")
    674             .expect("operational discriminator")
    675             .id,
    676         operational.id
    677     );
    678 
    679     let malformed_operational = vec![owned_tag(&["radroots:bin"])];
    680     assert_eq!(
    681         identify_event_contract(KIND_CLASSIFIED_LISTING, &malformed_operational, "Carrots",)
    682             .expect("raw operational marker partitions before shape validation")
    683             .id,
    684         operational.id
    685     );
    686     assert_eq!(
    687         validate_event_contract_parts(
    688             KIND_CLASSIFIED_LISTING,
    689             &malformed_operational,
    690             "Carrots",
    691             operational.id,
    692         ),
    693         Err(ContractValidationError::MissingTag {
    694             contract_id: "radroots.operational_listing.published.v1",
    695             name: "d",
    696         })
    697     );
    698 
    699     let focused = vec![owned_tag(&["radroots:price_unit", "lb"])];
    700     assert_eq!(
    701         identify_event_contract(KIND_CLASSIFIED_LISTING, &focused, "Carrots"),
    702         Err(ContractMatchError::UnsupportedShape(
    703             KIND_CLASSIFIED_LISTING
    704         ))
    705     );
    706     assert_eq!(
    707         validate_event_contract_parts(KIND_CLASSIFIED_LISTING, &focused, "Carrots", food.id,),
    708         Err(ContractValidationError::AdmissionRequired {
    709             contract_id: "radroots.food.availability.v1",
    710         })
    711     );
    712     assert_eq!(
    713         validate_event_contract_parts(KIND_CLASSIFIED_LISTING, &focused, "Carrots", operational.id,),
    714         Err(ContractValidationError::ContractMatch {
    715             error: ContractMatchError::UnsupportedShape(KIND_CLASSIFIED_LISTING),
    716         })
    717     );
    718 
    719     let generic = vec![owned_tag(&["d", "carrots"])];
    720     assert_eq!(
    721         identify_event_contract(KIND_CLASSIFIED_LISTING, &generic, "Carrots"),
    722         Err(ContractMatchError::UnsupportedShape(
    723             KIND_CLASSIFIED_LISTING
    724         ))
    725     );
    726 
    727     let ambiguous = vec![
    728         owned_tag(&["radroots:price_unit"]),
    729         owned_tag(&["radroots:primary_bin"]),
    730     ];
    731     assert_eq!(
    732         identify_event_contract(KIND_CLASSIFIED_LISTING, &ambiguous, "Carrots"),
    733         Err(ContractMatchError::UnsupportedShape(
    734             KIND_CLASSIFIED_LISTING
    735         ))
    736     );
    737 }
    738 
    739 #[test]
    740 fn event_contract_lookup_supports_many_contracts_per_kind() {
    741     let contracts = event_contracts_for_kind(KIND_LIST_SET_GENERIC).collect::<Vec<_>>();
    742     assert_eq!(contracts.len(), 6);
    743     assert!(
    744         contracts
    745             .iter()
    746             .any(|contract| contract.id == "radroots.list_set.farm.members.v1")
    747     );
    748     assert_eq!(
    749         event_contract("radroots.list_set.member_of.farms.v1").map(|contract| contract.kind),
    750         Some(KIND_LIST_SET_GENERIC)
    751     );
    752     assert!(event_contracts_for_kind(999_999).next().is_none());
    753 }
    754 
    755 #[test]
    756 fn event_contract_lookup_supports_knowledge_contract_kinds() {
    757     let contracts = event_contracts_for_kind(KIND_WIKI_ARTICLE).collect::<Vec<_>>();
    758     assert_eq!(contracts.len(), 1);
    759     assert_eq!(contracts[0].id, "radroots.wiki.article.v1");
    760     assert_eq!(
    761         identify_event_contract(KIND_WIKI_ARTICLE, &[], "# Soil")
    762             .expect("wiki article contract")
    763             .id,
    764         "radroots.wiki.article.v1"
    765     );
    766 }
    767 
    768 #[test]
    769 fn exposes_contract_family_metadata() {
    770     assert!(
    771         contract_families()
    772             .iter()
    773             .any(|family| family.family == ContractFamily::Knowledge && family.id == "knowledge")
    774     );
    775     assert_eq!(
    776         event_contract_family(event_contract("radroots.wiki.article.v1").expect("wiki")),
    777         Some(ContractFamily::Knowledge)
    778     );
    779     assert_eq!(
    780         kind_contract_family(kind_contract(KIND_KNOWLEDGE_CLAIM).expect("claim kind")),
    781         Some(ContractFamily::Knowledge)
    782     );
    783     assert_eq!(
    784         kind_contract_family(kind_contract(KIND_LIST_SET_GENERIC).expect("list kind")),
    785         Some(ContractFamily::List)
    786     );
    787 }
    788 
    789 #[test]
    790 fn contract_family_helpers_cover_prefixes_and_kind_branches() {
    791     for (id, family) in [
    792         ("radroots.account.test.v1", Some(ContractFamily::Account)),
    793         (
    794             "radroots.application.test.v1",
    795             Some(ContractFamily::Application),
    796         ),
    797         ("radroots.calendar.test.v1", Some(ContractFamily::Calendar)),
    798         ("radroots.farm.test.v1", Some(ContractFamily::Farm)),
    799         ("radroots.group.test.v1", Some(ContractFamily::Group)),
    800         ("radroots.http.test.v1", Some(ContractFamily::Http)),
    801         ("radroots.job.test.v1", Some(ContractFamily::Job)),
    802         (
    803             "radroots.knowledge.test.v1",
    804             Some(ContractFamily::Knowledge),
    805         ),
    806         ("radroots.wiki.test.v1", Some(ContractFamily::Knowledge)),
    807         ("radroots.list.test.v1", Some(ContractFamily::List)),
    808         ("radroots.list_set.test.v1", Some(ContractFamily::List)),
    809         (
    810             "radroots.operational_listing.test.v1",
    811             Some(ContractFamily::Market),
    812         ),
    813         ("radroots.food.test.v1", Some(ContractFamily::Market)),
    814         ("radroots.message.test.v1", Some(ContractFamily::Message)),
    815         ("radroots.profile.test.v1", Some(ContractFamily::Profile)),
    816         ("radroots.relay.test.v1", Some(ContractFamily::Relay)),
    817         ("radroots.social.test.v1", Some(ContractFamily::Social)),
    818         ("radroots.trade.test.v1", Some(ContractFamily::Trade)),
    819         ("radroots.order.test.v1", None),
    820         ("radroots.test.unknown.v1", None),
    821     ] {
    822         assert_eq!(contract_family_for_id(id), family, "{id}");
    823     }
    824 
    825     for (kind, family) in [
    826         (KIND_PROFILE, ContractFamily::Profile),
    827         (KIND_MESSAGE, ContractFamily::Message),
    828         (KIND_POST, ContractFamily::Social),
    829         (KIND_RELAY_AUTH, ContractFamily::Relay),
    830         (KIND_GROUP_ROLES, ContractFamily::Group),
    831         (KIND_LIST_SET_GENERIC, ContractFamily::List),
    832         (KIND_CALENDAR_EVENT_RSVP, ContractFamily::Calendar),
    833         (KIND_FARM_CRDT_CHANGE, ContractFamily::Farm),
    834         (KIND_CLASSIFIED_LISTING, ContractFamily::Market),
    835         (KIND_TRADE_CANCELLATION, ContractFamily::Trade),
    836         (KIND_KNOWLEDGE_CLAIM, ContractFamily::Knowledge),
    837         (KIND_JOB_FEEDBACK, ContractFamily::Job),
    838         (KIND_JOB_REQUEST_MIN, ContractFamily::Job),
    839         (KIND_JOB_RESULT_MIN, ContractFamily::Job),
    840     ] {
    841         assert_eq!(
    842             kind_contract_family(&synthetic_kind_contract(kind)),
    843             Some(family),
    844             "{kind}"
    845         );
    846     }
    847 
    848     assert_eq!(
    849         kind_contract_family(&synthetic_kind_contract(999_999)),
    850         None
    851     );
    852 }
    853 
    854 #[test]
    855 fn scalar_contract_validators_cover_canonical_boundaries() {
    856     assert_eq!(canonical_u64("0"), Some(0));
    857     assert_eq!(canonical_u64(u64::MAX.to_string().as_str()), Some(u64::MAX));
    858     for invalid in ["", "00", "01", "+1", "18446744073709551616"] {
    859         assert_eq!(canonical_u64(invalid), None, "{invalid}");
    860     }
    861 
    862     for valid in ["0", "u4pruydqqvj", "U4PRUYDQQVJ"] {
    863         assert!(geohash_is_valid(valid), "{valid}");
    864     }
    865     for invalid in ["", "u4pruydqqvjz0x", "a", "u4pruydqqv-i"] {
    866         assert!(!geohash_is_valid(invalid), "{invalid}");
    867     }
    868 }
    869 
    870 #[test]
    871 fn exposes_knowledge_contracts() {
    872     let wiki_article = event_contract("radroots.wiki.article.v1").expect("wiki article");
    873     assert_eq!(wiki_article.kind, KIND_WIKI_ARTICLE);
    874     assert_eq!(wiki_article.stability, EventStability::Experimental);
    875     assert_eq!(
    876         kind_contract(KIND_WIKI_ARTICLE)
    877             .expect("wiki kind")
    878             .standard,
    879         NostrStandard::Nip54
    880     );
    881     assert_eq!(wiki_article.content_schema, ContentSchema::Djot);
    882 
    883     let wiki_merge_request =
    884         event_contract("radroots.wiki.merge_request.v1").expect("wiki merge request");
    885     assert_eq!(wiki_merge_request.stability, EventStability::Experimental);
    886     assert_eq!(wiki_merge_request.content_schema, ContentSchema::PlainText);
    887 
    888     let wiki_redirect = event_contract("radroots.wiki.redirect.v1").expect("wiki redirect");
    889     assert_eq!(wiki_redirect.kind, KIND_WIKI_REDIRECT);
    890     assert_eq!(wiki_redirect.stability, EventStability::Experimental);
    891     assert_eq!(wiki_redirect.content_schema, ContentSchema::Empty);
    892 
    893     for id in [
    894         "radroots.knowledge.source.v1",
    895         "radroots.knowledge.evidence_bounty.v1",
    896         "radroots.knowledge.claim.v1",
    897         "radroots.knowledge.relation.v1",
    898         "radroots.knowledge.review.v1",
    899         "radroots.knowledge.field_report.v1",
    900         "radroots.knowledge.change_proposal.v1",
    901         "radroots.knowledge.contribution_attestation.v1",
    902     ] {
    903         let contract = event_contract(id).expect(id);
    904         assert_eq!(contract.stability, EventStability::Experimental);
    905         assert_eq!(
    906             event_contract_family(contract),
    907             Some(ContractFamily::Knowledge)
    908         );
    909         let contract_tag = contract
    910             .tags
    911             .iter()
    912             .find(|tag| tag.name == "contract")
    913             .expect("contract tag");
    914         assert_eq!(contract_tag.semantic, TagSemantic::Contract);
    915         assert_eq!(contract_tag.value_type, TagValueType::ContractId);
    916     }
    917 }
    918 
    919 #[test]
    920 fn custom_knowledge_schema_lookup_covers_registered_ids() {
    921     for id in [
    922         "radroots.knowledge.source.v1",
    923         "radroots.knowledge.evidence_bounty.v1",
    924         "radroots.knowledge.claim.v1",
    925         "radroots.knowledge.relation.v1",
    926         "radroots.knowledge.review.v1",
    927         "radroots.knowledge.field_report.v1",
    928         "radroots.knowledge.change_proposal.v1",
    929         "radroots.knowledge.contribution_attestation.v1",
    930     ] {
    931         assert_eq!(custom_knowledge_schema(id), Some(id), "{id}");
    932     }
    933     assert_eq!(custom_knowledge_schema("radroots.wiki.article.v1"), None);
    934 }
    935 
    936 #[test]
    937 fn post_subtype_contracts_require_verified_admission() {
    938     let tags = vec![vec!["t".to_owned(), "radroots-ask".to_owned()]];
    939     let generic = identify_event_contract(KIND_POST, &tags, "Question")
    940         .expect("unsigned kind-1 identification remains generic");
    941     assert_eq!(generic.id, "radroots.social.post.v1");
    942     assert_eq!(generic.authoring_policy(), EventAuthoringPolicy::ReadOnly);
    943 
    944     for id in [
    945         "radroots.social.update.v1",
    946         "radroots.social.photo_update.v1",
    947         "radroots.social.ask.v1",
    948         "radroots.social.reply.v1",
    949     ] {
    950         let contract = event_contract(id).expect(id);
    951         assert_eq!(
    952             event_contract_family(contract),
    953             Some(ContractFamily::Social)
    954         );
    955         assert_eq!(contract.authoring_policy(), EventAuthoringPolicy::TypedOnly);
    956         assert_eq!(
    957             validate_event_contract_parts(KIND_POST, &tags, "Question", id),
    958             Err(ContractValidationError::AdmissionRequired { contract_id: id })
    959         );
    960     }
    961 
    962     assert_eq!(
    963         event_contract("radroots.profile.metadata.v1")
    964             .expect("strict authored profile contract")
    965             .authoring_policy(),
    966         EventAuthoringPolicy::TypedOnly
    967     );
    968     assert_eq!(
    969         event_contract("radroots.social.geochat.v1")
    970             .expect("generic-draft control contract")
    971             .authoring_policy(),
    972         EventAuthoringPolicy::GenericDraft
    973     );
    974 }
    975 
    976 #[test]
    977 fn nip22_comment_contract_is_typed_and_admission_only() {
    978     assert_eq!(RADROOTS_EVENT_CONTRACT_REGISTRY_VERSION, 7);
    979 
    980     let kind = kind_contract(KIND_COMMENT).expect("Comment kind");
    981     assert_eq!(kind.canonical_constant, "KIND_COMMENT");
    982     assert_eq!(kind.kind, 1111);
    983     assert_eq!(kind.class, EventClass::Regular);
    984     assert_eq!(kind.standard, NostrStandard::Nip22);
    985     assert_eq!(
    986         kind.accepted_event_contracts,
    987         &["radroots.social.comment.v1"]
    988     );
    989 
    990     let contract = event_contract("radroots.social.comment.v1").expect("NIP-22 Comment contract");
    991     assert_eq!(
    992         contract.payload_type,
    993         "RadrootsAuthoredNip22Comment / RadrootsInboundNip22CommentProjection"
    994     );
    995     assert_eq!(contract.kind, KIND_COMMENT);
    996     assert_eq!(contract.class, EventClass::Regular);
    997     assert_eq!(contract.privacy, EventPrivacy::Public);
    998     assert_eq!(contract.required_author_role(), AuthorRole::Any);
    999     assert_eq!(contract.content_schema, ContentSchema::PlainText);
   1000     assert_eq!(contract.authoring_policy(), EventAuthoringPolicy::TypedOnly);
   1001     assert_eq!(contract.discriminator, EventDiscriminator::AdmissionOnly);
   1002     assert_eq!(contract.reducers, &[Reducer::SocialProjection]);
   1003     assert_eq!(
   1004         event_contract_family(contract),
   1005         Some(ContractFamily::Social)
   1006     );
   1007     assert_eq!(
   1008         contract.tags.iter().map(|tag| tag.name).collect::<Vec<_>>(),
   1009         vec!["E", "A", "K", "P", "a", "e", "k", "p"]
   1010     );
   1011     assert!(
   1012         contract.tags.iter().all(|tag| tag.relay_indexed),
   1013         "every single-letter NIP-22 tag must remain relay-indexed"
   1014     );
   1015     assert_eq!(
   1016         validate_event_contract_parts(KIND_COMMENT, &[], "Comment", contract.id),
   1017         Err(ContractValidationError::AdmissionRequired {
   1018             contract_id: "radroots.social.comment.v1",
   1019         })
   1020     );
   1021 }
   1022 
   1023 #[test]
   1024 fn nip09_deletion_request_contract_is_typed_and_admission_only() {
   1025     use crate::post::deletion::{
   1026         RADROOTS_NIP09_DELETION_CONTENT_MAX_BYTES, RADROOTS_NIP09_DELETION_EVENT_WIRE_MAX_BYTES,
   1027         RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES, RADROOTS_NIP09_DELETION_TAG_MAX_COUNT,
   1028         RADROOTS_NIP09_DELETION_TAG_TOTAL_ELEMENT_MAX_COUNT,
   1029         RADROOTS_NIP09_DELETION_TAG_TOTAL_MAX_BYTES, RADROOTS_NIP09_DELETION_TARGET_KIND_MAX,
   1030     };
   1031 
   1032     assert_eq!(RADROOTS_EVENT_CONTRACT_REGISTRY_VERSION, 7);
   1033 
   1034     let kind = kind_contract(KIND_DELETION_REQUEST).expect("deletion request kind");
   1035     assert_eq!(kind.canonical_constant, "KIND_DELETION_REQUEST");
   1036     assert_eq!(kind.kind, 5);
   1037     assert_eq!(kind.class, EventClass::Regular);
   1038     assert_eq!(kind.standard, NostrStandard::Nip09);
   1039     assert_eq!(
   1040         kind.accepted_event_contracts,
   1041         &["radroots.social.deletion_request.v1"]
   1042     );
   1043 
   1044     let contract = event_contract("radroots.social.deletion_request.v1")
   1045         .expect("NIP-09 deletion request contract");
   1046     assert_eq!(
   1047         contract.payload_type,
   1048         "RadrootsAuthoredNip09DeletionRequest / RadrootsInboundNip09DeletionProjection"
   1049     );
   1050     assert_eq!(contract.kind, KIND_DELETION_REQUEST);
   1051     assert_eq!(contract.class, EventClass::Regular);
   1052     assert_eq!(contract.privacy, EventPrivacy::Public);
   1053     assert_eq!(contract.required_author_role(), AuthorRole::Any);
   1054     assert_eq!(contract.content_schema, ContentSchema::PlainText);
   1055     assert_eq!(contract.authoring_policy(), EventAuthoringPolicy::TypedOnly);
   1056     assert_eq!(contract.discriminator, EventDiscriminator::AdmissionOnly);
   1057     assert_eq!(contract.reducers, &[Reducer::SocialProjection]);
   1058     assert_eq!(
   1059         event_contract_family(contract),
   1060         Some(ContractFamily::Social)
   1061     );
   1062     assert_eq!(
   1063         contract
   1064             .tags
   1065             .iter()
   1066             .map(|tag| (
   1067                 tag.name,
   1068                 tag.cardinality,
   1069                 tag.semantic,
   1070                 tag.value_type,
   1071                 tag.relay_indexed,
   1072             ))
   1073             .collect::<Vec<_>>(),
   1074         vec![
   1075             (
   1076                 "e",
   1077                 TagCardinality::OptionalMany,
   1078                 TagSemantic::EventPointer,
   1079                 TagValueType::EventId,
   1080                 true,
   1081             ),
   1082             (
   1083                 "a",
   1084                 TagCardinality::OptionalMany,
   1085                 TagSemantic::Nip01Coordinate,
   1086                 TagValueType::Nip01Coordinate,
   1087                 true,
   1088             ),
   1089             (
   1090                 "k",
   1091                 TagCardinality::OptionalMany,
   1092                 TagSemantic::Kind,
   1093                 TagValueType::Kind,
   1094                 true,
   1095             ),
   1096         ]
   1097     );
   1098     assert_eq!(
   1099         (
   1100             RADROOTS_NIP09_DELETION_CONTENT_MAX_BYTES,
   1101             RADROOTS_NIP09_DELETION_TAG_MAX_COUNT,
   1102             RADROOTS_NIP09_DELETION_TAG_TOTAL_ELEMENT_MAX_COUNT,
   1103             RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES,
   1104             RADROOTS_NIP09_DELETION_TAG_TOTAL_MAX_BYTES,
   1105             RADROOTS_NIP09_DELETION_EVENT_WIRE_MAX_BYTES,
   1106             RADROOTS_NIP09_DELETION_TARGET_KIND_MAX,
   1107         ),
   1108         (131_072, 1_024, 4_096, 4_096, 131_072, 262_144, 65_535)
   1109     );
   1110     assert_eq!(
   1111         validate_event_contract_parts(KIND_DELETION_REQUEST, &[], "superseded", contract.id,),
   1112         Err(ContractValidationError::AdmissionRequired {
   1113             contract_id: "radroots.social.deletion_request.v1",
   1114         })
   1115     );
   1116     let selected = unsigned_event(
   1117         KIND_DELETION_REQUEST,
   1118         vec![vec![
   1119             "e",
   1120             "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
   1121         ]],
   1122         "superseded",
   1123     );
   1124     assert_eq!(
   1125         validate_event_contract_for_admission(&selected, contract.id),
   1126         Ok(contract)
   1127     );
   1128     assert_eq!(
   1129         validate_event_contract_for_admission(
   1130             &unsigned_event(KIND_DELETION_REQUEST, vec![], "superseded"),
   1131             contract.id,
   1132         ),
   1133         Err(ContractValidationError::ContractMatch {
   1134             error: ContractMatchError::UnsupportedShape(KIND_DELETION_REQUEST),
   1135         })
   1136     );
   1137 }
   1138 
   1139 #[test]
   1140 fn identifies_exact_list_set_shape() {
   1141     let tags = vec![vec!["d".to_owned(), "member_of.farms".to_owned()]];
   1142     let contract = identify_event_contract(KIND_LIST_SET_GENERIC, &tags, "{}")
   1143         .expect("member_of farms contract");
   1144     assert_eq!(contract.id, "radroots.list_set.member_of.farms.v1");
   1145 }
   1146 
   1147 #[test]
   1148 fn identifies_composite_list_set_shape() {
   1149     let tags = vec![vec![
   1150         "d".to_owned(),
   1151         "farm:farm_01:members.workers".to_owned(),
   1152     ]];
   1153     let contract =
   1154         identify_event_contract(KIND_LIST_SET_GENERIC, &tags, "{}").expect("farm workers contract");
   1155     assert_eq!(contract.id, "radroots.list_set.farm.members.workers.v1");
   1156 }
   1157 
   1158 #[test]
   1159 fn rejects_unknown_or_unsupported_shapes() {
   1160     assert_eq!(
   1161         identify_event_contract(999_999, &[], "{}"),
   1162         Err(ContractMatchError::UnsupportedKind(999_999))
   1163     );
   1164     assert_eq!(
   1165         validate_event_contract(&unsigned_event(999_999, Vec::new(), "{}")),
   1166         Err(ContractValidationError::ContractMatch {
   1167             error: ContractMatchError::UnsupportedKind(999_999),
   1168         })
   1169     );
   1170 
   1171     let tags = vec![vec!["d".to_owned(), "unknown".to_owned()]];
   1172     assert_eq!(
   1173         identify_event_contract(KIND_LIST_SET_GENERIC, &tags, "{}"),
   1174         Err(ContractMatchError::UnsupportedShape(KIND_LIST_SET_GENERIC))
   1175     );
   1176 }
   1177 
   1178 #[test]
   1179 fn rejects_ambiguous_shapes() {
   1180     assert_eq!(
   1181         identify_from_contracts(AMBIGUOUS_TEST_CONTRACTS.iter(), KIND_POST, &[], ""),
   1182         Err(ContractMatchError::AmbiguousShape(KIND_POST))
   1183     );
   1184 }
   1185 
   1186 #[test]
   1187 fn supports_content_field_discriminators() {
   1188     assert!(discriminator_matches(
   1189         &EventDiscriminator::EnvelopeType("proposal"),
   1190         &[],
   1191         r#"{"domain":"radroots.trade","type":"proposal"}"#
   1192     ));
   1193     assert!(discriminator_matches(
   1194         &EventDiscriminator::ContentJsonFieldEquals {
   1195             field: "domain",
   1196             value: "radroots.trade"
   1197         },
   1198         &[],
   1199         r#"{"domain": "radroots.trade", "type": "proposal"}"#
   1200     ));
   1201 
   1202     let base = *event_contract("radroots.trade.proposal.v1").expect("trade proposal");
   1203     for discriminator in [
   1204         EventDiscriminator::ContentJsonFieldEquals {
   1205             field: "type",
   1206             value: "proposal",
   1207         },
   1208         EventDiscriminator::EnvelopeType("proposal"),
   1209     ] {
   1210         let contract = EventContract {
   1211             discriminator,
   1212             ..base
   1213         };
   1214         assert!(validate_discriminator_parts(r#"{"type":"proposal"}"#, &contract, false).is_ok());
   1215         assert!(matches!(
   1216             validate_discriminator_parts(r#"{"type":"decision"}"#, &contract, false),
   1217             Err(ContractValidationError::ContentFieldMismatch { .. })
   1218         ));
   1219         assert!(matches!(
   1220             validate_discriminator_parts("{}", &contract, false),
   1221             Err(ContractValidationError::MissingContentField { .. })
   1222         ));
   1223     }
   1224     let contract = EventContract {
   1225         discriminator: EventDiscriminator::KindOnly,
   1226         ..base
   1227     };
   1228     assert!(validate_discriminator_parts("not-json", &contract, false).is_ok());
   1229 }
   1230 
   1231 #[test]
   1232 fn supports_tag_equals_discriminators() {
   1233     let tags = vec![vec!["status".to_owned(), "accepted".to_owned()]];
   1234 
   1235     assert!(discriminator_matches(
   1236         &EventDiscriminator::TagEquals {
   1237             name: "status",
   1238             value: "accepted",
   1239         },
   1240         &tags,
   1241         "{}"
   1242     ));
   1243     assert!(!discriminator_matches(
   1244         &EventDiscriminator::TagEquals {
   1245             name: "status",
   1246             value: "declined",
   1247         },
   1248         &tags,
   1249         "{}"
   1250     ));
   1251 }
   1252 
   1253 #[test]
   1254 fn validates_custom_knowledge_contract_shape() {
   1255     let event = unsigned_event(
   1256         KIND_KNOWLEDGE_CLAIM,
   1257         vec![vec!["contract", "radroots.knowledge.claim.v1"]],
   1258         r#"{"schema":"radroots.knowledge.claim.v1","schema_version":1,"text":"soil improves with cover crops"}"#,
   1259     );
   1260 
   1261     assert_eq!(
   1262         validate_event_contract_shape(&event, "radroots.knowledge.claim.v1"),
   1263         Ok(())
   1264     );
   1265     assert_eq!(
   1266         validate_event_contract(&event).expect("validated").id,
   1267         "radroots.knowledge.claim.v1"
   1268     );
   1269 }
   1270 
   1271 #[test]
   1272 fn rejects_custom_knowledge_contract_tag_mismatch() {
   1273     let event = unsigned_event(
   1274         KIND_KNOWLEDGE_CLAIM,
   1275         vec![vec!["contract", "radroots.knowledge.relation.v1"]],
   1276         r#"{"schema":"radroots.knowledge.claim.v1","schema_version":1}"#,
   1277     );
   1278 
   1279     assert_eq!(
   1280         validate_event_contract_shape(&event, "radroots.knowledge.claim.v1"),
   1281         Err(ContractValidationError::TagValueMismatch {
   1282             contract_id: "radroots.knowledge.claim.v1",
   1283             name: "contract",
   1284             expected: "radroots.knowledge.claim.v1".to_owned(),
   1285             actual: Some("radroots.knowledge.relation.v1".to_owned()),
   1286         })
   1287     );
   1288 }
   1289 
   1290 #[test]
   1291 fn rejects_custom_knowledge_schema_mismatch() {
   1292     let event = unsigned_event(
   1293         KIND_KNOWLEDGE_CLAIM,
   1294         vec![vec!["contract", "radroots.knowledge.claim.v1"]],
   1295         r#"{"schema":"radroots.knowledge.relation.v1","schema_version":1}"#,
   1296     );
   1297 
   1298     assert_eq!(
   1299         validate_event_contract_shape(&event, "radroots.knowledge.claim.v1"),
   1300         Err(ContractValidationError::ContentFieldMismatch {
   1301             contract_id: "radroots.knowledge.claim.v1",
   1302             field: "schema",
   1303             expected: "radroots.knowledge.claim.v1".to_owned(),
   1304         })
   1305     );
   1306 }
   1307 
   1308 #[test]
   1309 fn rejects_custom_knowledge_missing_schema_version() {
   1310     let event = unsigned_event(
   1311         KIND_KNOWLEDGE_CLAIM,
   1312         vec![vec!["contract", "radroots.knowledge.claim.v1"]],
   1313         r#"{"schema":"radroots.knowledge.claim.v1"}"#,
   1314     );
   1315 
   1316     assert_eq!(
   1317         validate_event_contract_shape(&event, "radroots.knowledge.claim.v1"),
   1318         Err(ContractValidationError::MissingContentField {
   1319             contract_id: "radroots.knowledge.claim.v1",
   1320             field: "schema_version",
   1321         })
   1322     );
   1323 }
   1324 
   1325 #[test]
   1326 fn rejects_authoritative_knowledge_status_fields() {
   1327     let event = unsigned_event_owned(
   1328         KIND_KNOWLEDGE_REVIEW,
   1329         vec![
   1330             vec![
   1331                 "contract".to_owned(),
   1332                 "radroots.knowledge.review.v1".to_owned(),
   1333             ],
   1334             vec![
   1335                 "review_target".to_owned(),
   1336                 "0".repeat(64),
   1337                 crate::test_valid_hex_64('1'),
   1338                 "30818".to_owned(),
   1339                 "soil".to_owned(),
   1340             ],
   1341         ],
   1342         r#"{"schema":"radroots.knowledge.review.v1","schema_version":1,"canon_status":"approved"}"#,
   1343     );
   1344 
   1345     assert_eq!(
   1346         validate_event_contract_shape(&event, "radroots.knowledge.review.v1"),
   1347         Err(ContractValidationError::ForbiddenContentField {
   1348             contract_id: "radroots.knowledge.review.v1",
   1349             field: "canon_status",
   1350         })
   1351     );
   1352 }
   1353 
   1354 #[test]
   1355 fn validate_event_contract_shape_reports_registry_kind_and_content_errors() {
   1356     let event = unsigned_event(KIND_POST, Vec::new(), "hello");
   1357     assert_eq!(
   1358         validate_event_contract_shape(&event, "missing.contract.v1"),
   1359         Err(ContractValidationError::UnknownContract {
   1360             contract_id: "missing.contract.v1".to_owned(),
   1361         })
   1362     );
   1363     assert_eq!(
   1364         validate_event_contract_shape(&event, "radroots.profile.metadata.v1"),
   1365         Err(ContractValidationError::KindMismatch {
   1366             expected: KIND_PROFILE,
   1367             actual: KIND_POST,
   1368         })
   1369     );
   1370 
   1371     let invalid_json = unsigned_event(
   1372         KIND_KNOWLEDGE_CLAIM,
   1373         vec![vec!["contract", "radroots.knowledge.claim.v1"]],
   1374         "not-json",
   1375     );
   1376     assert_eq!(
   1377         validate_event_contract_shape(&invalid_json, "radroots.knowledge.claim.v1"),
   1378         Err(ContractValidationError::InvalidJsonContent {
   1379             contract_id: "radroots.knowledge.claim.v1",
   1380         })
   1381     );
   1382 
   1383     assert_eq!(
   1384         validate_event_contract_shape(
   1385             &unsigned_event(KIND_POST, Vec::new(), "plain text"),
   1386             "radroots.social.post.v1",
   1387         ),
   1388         Ok(())
   1389     );
   1390 }
   1391 
   1392 #[test]
   1393 fn validate_contract_tags_reports_cardinality_errors() {
   1394     let missing_required_one = unsigned_event(
   1395         KIND_KNOWLEDGE_CLAIM,
   1396         Vec::new(),
   1397         r#"{"schema":"radroots.knowledge.claim.v1","schema_version":1}"#,
   1398     );
   1399     assert_eq!(
   1400         validate_event_contract_shape(&missing_required_one, "radroots.knowledge.claim.v1"),
   1401         Err(ContractValidationError::MissingTag {
   1402             contract_id: "radroots.knowledge.claim.v1",
   1403             name: "contract",
   1404         })
   1405     );
   1406 
   1407     let duplicate_required_one = unsigned_event(
   1408         KIND_KNOWLEDGE_CLAIM,
   1409         vec![
   1410             vec!["contract", "radroots.knowledge.claim.v1"],
   1411             vec!["contract", "radroots.knowledge.claim.v1"],
   1412         ],
   1413         r#"{"schema":"radroots.knowledge.claim.v1","schema_version":1}"#,
   1414     );
   1415     assert_eq!(
   1416         validate_event_contract_shape(&duplicate_required_one, "radroots.knowledge.claim.v1"),
   1417         Err(ContractValidationError::TagCardinalityMismatch {
   1418             contract_id: "radroots.knowledge.claim.v1",
   1419             name: "contract",
   1420         })
   1421     );
   1422 
   1423     let required_many =
   1424         synthetic_event_contract("radroots.test.required_many.v1", REQUIRED_MANY_TEST_TAGS);
   1425     assert_eq!(
   1426         validate_contract_tags_parts(&[], &required_many),
   1427         Err(ContractValidationError::MissingTag {
   1428             contract_id: "radroots.test.required_many.v1",
   1429             name: "test_many",
   1430         })
   1431     );
   1432     assert_eq!(
   1433         validate_contract_tags_parts(
   1434             &[vec!["test_many".to_owned(), "one".to_owned()]],
   1435             &required_many
   1436         ),
   1437         Ok(())
   1438     );
   1439 
   1440     let optional_one =
   1441         synthetic_event_contract("radroots.test.optional_one.v1", OPTIONAL_ONE_TEST_TAGS);
   1442     assert_eq!(validate_contract_tags_parts(&[], &optional_one), Ok(()));
   1443     assert_eq!(
   1444         validate_contract_tags_parts(
   1445             &[
   1446                 vec!["test_optional".to_owned(), "one".to_owned()],
   1447                 vec!["test_optional".to_owned(), "two".to_owned()],
   1448             ],
   1449             &optional_one,
   1450         ),
   1451         Err(ContractValidationError::TagCardinalityMismatch {
   1452             contract_id: "radroots.test.optional_one.v1",
   1453             name: "test_optional",
   1454         })
   1455     );
   1456 
   1457     let duplicate_required = synthetic_event_contract(
   1458         "radroots.test.duplicate_required.v1",
   1459         DUPLICATE_REQUIRED_TEST_TAGS,
   1460     );
   1461     assert_eq!(
   1462         validate_contract_tags_parts(
   1463             &[
   1464                 vec!["test_required".to_owned(), "one".to_owned()],
   1465                 vec!["test_required".to_owned(), "two".to_owned()],
   1466             ],
   1467             &duplicate_required,
   1468         ),
   1469         Ok(())
   1470     );
   1471 
   1472     let duplicate_optional = synthetic_event_contract(
   1473         "radroots.test.duplicate_optional.v1",
   1474         DUPLICATE_OPTIONAL_TEST_TAGS,
   1475     );
   1476     assert_eq!(
   1477         validate_contract_tags_parts(
   1478             &[
   1479                 vec!["test_optional".to_owned(), "one".to_owned()],
   1480                 vec!["test_optional".to_owned(), "two".to_owned()],
   1481             ],
   1482             &duplicate_optional,
   1483         ),
   1484         Ok(())
   1485     );
   1486 }
   1487 
   1488 #[test]
   1489 fn validate_contract_tags_enforces_declared_value_types() {
   1490     let claim_content = r#"{"schema":"radroots.knowledge.claim.v1","schema_version":1}"#;
   1491     let invalid_source = unsigned_event_owned(
   1492         KIND_KNOWLEDGE_CLAIM,
   1493         vec![
   1494             vec![
   1495                 "contract".to_owned(),
   1496                 "radroots.knowledge.claim.v1".to_owned(),
   1497             ],
   1498             vec!["source".to_owned(), "not-an-event-id".to_owned()],
   1499         ],
   1500         claim_content,
   1501     );
   1502     assert_eq!(
   1503         validate_event_contract_shape(&invalid_source, "radroots.knowledge.claim.v1"),
   1504         Err(ContractValidationError::TagValueMismatch {
   1505             contract_id: "radroots.knowledge.claim.v1",
   1506             name: "source",
   1507             expected: "event_pointer".to_owned(),
   1508             actual: Some("not-an-event-id".to_owned()),
   1509         })
   1510     );
   1511 
   1512     let invalid_citation = unsigned_event(
   1513         KIND_KNOWLEDGE_CLAIM,
   1514         vec![
   1515             vec!["contract", "radroots.knowledge.claim.v1"],
   1516             vec!["citation", "not-hex"],
   1517         ],
   1518         claim_content,
   1519     );
   1520     assert_eq!(
   1521         validate_event_contract_shape(&invalid_citation, "radroots.knowledge.claim.v1"),
   1522         Err(ContractValidationError::TagValueMismatch {
   1523             contract_id: "radroots.knowledge.claim.v1",
   1524             name: "citation",
   1525             expected: "sha256".to_owned(),
   1526             actual: Some("not-hex".to_owned()),
   1527         })
   1528     );
   1529 
   1530     let invalid_review = unsigned_event(
   1531         KIND_KNOWLEDGE_REVIEW,
   1532         vec![
   1533             vec!["contract", "radroots.knowledge.review.v1"],
   1534             vec!["review_target", "not-an-event-id"],
   1535         ],
   1536         r#"{"schema":"radroots.knowledge.review.v1","schema_version":1}"#,
   1537     );
   1538     assert_eq!(
   1539         validate_event_contract_shape(&invalid_review, "radroots.knowledge.review.v1"),
   1540         Err(ContractValidationError::TagValueMismatch {
   1541             contract_id: "radroots.knowledge.review.v1",
   1542             name: "review_target",
   1543             expected: "event_pointer".to_owned(),
   1544             actual: Some("not-an-event-id".to_owned()),
   1545         })
   1546     );
   1547 
   1548     let invalid_geohash = unsigned_event(
   1549         KIND_KNOWLEDGE_FIELD_REPORT,
   1550         vec![
   1551             vec!["contract", "radroots.knowledge.field_report.v1"],
   1552             vec!["g", "invalid-a"],
   1553         ],
   1554         r#"{"schema":"radroots.knowledge.field_report.v1","schema_version":1}"#,
   1555     );
   1556     assert_eq!(
   1557         validate_event_contract_shape(&invalid_geohash, "radroots.knowledge.field_report.v1"),
   1558         Err(ContractValidationError::TagValueMismatch {
   1559             contract_id: "radroots.knowledge.field_report.v1",
   1560             name: "g",
   1561             expected: "geohash".to_owned(),
   1562             actual: Some("invalid-a".to_owned()),
   1563         })
   1564     );
   1565 
   1566     let invalid_address = unsigned_event(
   1567         KIND_WIKI_REDIRECT,
   1568         vec![vec!["d", "soil"], vec!["a", "30818:not-hex:soil"]],
   1569         "",
   1570     );
   1571     assert_eq!(
   1572         validate_event_contract_shape(&invalid_address, "radroots.wiki.redirect.v1"),
   1573         Err(ContractValidationError::TagValueMismatch {
   1574             contract_id: "radroots.wiki.redirect.v1",
   1575             name: "a",
   1576             expected: "addressable_coordinate".to_owned(),
   1577             actual: Some("30818:not-hex:soil".to_owned()),
   1578         })
   1579     );
   1580 
   1581     let invalid_event_id = unsigned_event_owned(
   1582         KIND_WIKI_MERGE_REQUEST,
   1583         vec![
   1584             vec![
   1585                 "a".to_owned(),
   1586                 format!("30818:{}:soil", crate::test_valid_hex_64('0')),
   1587             ],
   1588             vec!["p".to_owned(), crate::test_valid_hex_64('1')],
   1589             vec!["e".to_owned(), "not-hex".to_owned()],
   1590         ],
   1591         "",
   1592     );
   1593     assert_eq!(
   1594         validate_event_contract_shape(&invalid_event_id, "radroots.wiki.merge_request.v1"),
   1595         Err(ContractValidationError::TagValueMismatch {
   1596             contract_id: "radroots.wiki.merge_request.v1",
   1597             name: "e",
   1598             expected: "event_id".to_owned(),
   1599             actual: Some("not-hex".to_owned()),
   1600         })
   1601     );
   1602 
   1603     let valid_source = unsigned_event_owned(
   1604         KIND_KNOWLEDGE_CLAIM,
   1605         vec![
   1606             vec![
   1607                 "contract".to_owned(),
   1608                 "radroots.knowledge.claim.v1".to_owned(),
   1609             ],
   1610             event_ref_tag(
   1611                 "source",
   1612                 hex_64('a').as_str(),
   1613                 hex_64('b').as_str(),
   1614                 KIND_KNOWLEDGE_SOURCE,
   1615             ),
   1616             vec!["citation".to_owned(), hex_64('c')],
   1617         ],
   1618         claim_content,
   1619     );
   1620     assert_eq!(
   1621         validate_event_contract_shape(&valid_source, "radroots.knowledge.claim.v1"),
   1622         Ok(())
   1623     );
   1624 }
   1625 
   1626 #[test]
   1627 fn tag_value_shape_helpers_cover_contract_registry_value_types() {
   1628     let event_id = hex_64('a');
   1629     let public_key = hex_64('b');
   1630     let coordinate = format!("{KIND_WIKI_ARTICLE}:{public_key}:soil");
   1631     let replaceable_coordinate = format!("0:{public_key}:");
   1632     let valid_pointer = vec![
   1633         "source".to_owned(),
   1634         event_id.clone(),
   1635         public_key.clone(),
   1636         KIND_KNOWLEDGE_SOURCE.to_string(),
   1637         "soil".to_owned(),
   1638         "ws://relay.example.com".to_owned(),
   1639         "wss://relay.example.net".to_owned(),
   1640     ];
   1641     let empty_d_pointer = vec![
   1642         "source".to_owned(),
   1643         event_id.clone(),
   1644         public_key.clone(),
   1645         KIND_KNOWLEDGE_SOURCE.to_string(),
   1646         String::new(),
   1647     ];
   1648 
   1649     assert!(!tag_value_is_valid(
   1650         &owned_tag(&["source"]),
   1651         TagValueType::EventPointer
   1652     ));
   1653     assert!(tag_value_is_valid(
   1654         &owned_tag(&["a", coordinate.as_str()]),
   1655         TagValueType::AddressableCoordinate
   1656     ));
   1657     assert!(!tag_value_is_valid(
   1658         &owned_tag(&["a", "30818:not-hex:soil"]),
   1659         TagValueType::AddressableCoordinate
   1660     ));
   1661     assert!(tag_value_is_valid(
   1662         &owned_tag(&["a", replaceable_coordinate.as_str()]),
   1663         TagValueType::Nip01Coordinate
   1664     ));
   1665     assert!(!tag_value_is_valid(
   1666         &owned_tag(&["a", format!("0:{public_key}:profile").as_str()]),
   1667         TagValueType::Nip01Coordinate
   1668     ));
   1669     assert!(tag_value_is_valid(
   1670         &owned_tag(&["contract", "radroots.knowledge.claim.v1"]),
   1671         TagValueType::ContractId
   1672     ));
   1673     assert!(!tag_value_is_valid(
   1674         &owned_tag(&["contract", "radroots.unknown.v1"]),
   1675         TagValueType::ContractId
   1676     ));
   1677     assert!(tag_value_is_valid(
   1678         &owned_tag(&["d", "soil"]),
   1679         TagValueType::DTag
   1680     ));
   1681     assert!(!tag_value_is_valid(
   1682         &owned_tag(&["d", ""]),
   1683         TagValueType::DTag
   1684     ));
   1685     assert!(tag_value_is_valid(
   1686         &owned_tag(&["e", event_id.as_str()]),
   1687         TagValueType::EventId
   1688     ));
   1689     assert!(tag_value_is_valid(
   1690         &owned_tag(&["citation", event_id.as_str()]),
   1691         TagValueType::Sha256
   1692     ));
   1693     assert!(!tag_value_is_valid(
   1694         &owned_tag(&["e", "not-hex"]),
   1695         TagValueType::EventId
   1696     ));
   1697     assert!(tag_value_is_valid(
   1698         &valid_pointer,
   1699         TagValueType::EventPointer
   1700     ));
   1701     assert!(tag_value_is_valid(
   1702         &empty_d_pointer,
   1703         TagValueType::EventPointer
   1704     ));
   1705     assert!(!event_pointer_tag_is_valid(&owned_tag(&[
   1706         "source",
   1707         "not-hex",
   1708         public_key.as_str(),
   1709         "1",
   1710         ""
   1711     ])));
   1712     assert!(!event_pointer_tag_is_valid(&owned_tag(&[
   1713         "source",
   1714         event_id.as_str(),
   1715         "not-hex",
   1716         "1",
   1717         ""
   1718     ])));
   1719     assert!(!event_pointer_tag_is_valid(&owned_tag(&[
   1720         "source",
   1721         event_id.as_str(),
   1722         public_key.as_str(),
   1723         "not-a-kind",
   1724         ""
   1725     ])));
   1726     assert!(!event_pointer_tag_is_valid(&owned_tag(&[
   1727         "source",
   1728         event_id.as_str(),
   1729         public_key.as_str(),
   1730         "1"
   1731     ])));
   1732     assert!(!event_pointer_tag_is_valid(&owned_tag(&[
   1733         "source",
   1734         event_id.as_str(),
   1735         public_key.as_str(),
   1736         "1",
   1737         "bad tag"
   1738     ])));
   1739     assert!(!event_pointer_tag_is_valid(&owned_tag(&[
   1740         "source",
   1741         event_id.as_str(),
   1742         public_key.as_str(),
   1743         "1",
   1744         "",
   1745         "https://relay.example.com"
   1746     ])));
   1747     assert!(tag_value_is_valid(
   1748         &owned_tag(&["g", "9q8yy"]),
   1749         TagValueType::Geohash
   1750     ));
   1751     assert!(tag_value_is_valid(
   1752         &owned_tag(&["g", "9Q8YY"]),
   1753         TagValueType::Geohash
   1754     ));
   1755     assert!(!tag_value_is_valid(
   1756         &owned_tag(&["g", ""]),
   1757         TagValueType::Geohash
   1758     ));
   1759     assert!(!tag_value_is_valid(
   1760         &owned_tag(&["g", "1234567890123"]),
   1761         TagValueType::Geohash
   1762     ));
   1763     assert!(!tag_value_is_valid(
   1764         &owned_tag(&["g", "aaaaa"]),
   1765         TagValueType::Geohash
   1766     ));
   1767     assert!(tag_value_is_valid(
   1768         &owned_tag(&["k", "30818"]),
   1769         TagValueType::Kind
   1770     ));
   1771     assert!(!tag_value_is_valid(
   1772         &owned_tag(&["k", "not-a-kind"]),
   1773         TagValueType::Kind
   1774     ));
   1775     assert!(tag_value_is_valid(
   1776         &owned_tag(&["p", public_key.as_str()]),
   1777         TagValueType::PublicKey
   1778     ));
   1779     assert!(!tag_value_is_valid(
   1780         &owned_tag(&["p", "not-hex"]),
   1781         TagValueType::PublicKey
   1782     ));
   1783     assert!(tag_value_is_valid(
   1784         &owned_tag(&["relay", "ws://relay.example.com"]),
   1785         TagValueType::RelayUrl
   1786     ));
   1787     assert!(tag_value_is_valid(
   1788         &owned_tag(&["relay", "wss://relay.example.com"]),
   1789         TagValueType::RelayUrl
   1790     ));
   1791     assert!(!tag_value_is_valid(
   1792         &owned_tag(&["relay", "http://relay.example.com"]),
   1793         TagValueType::RelayUrl
   1794     ));
   1795     assert!(relay_url_is_valid("ws://relay.example.com"));
   1796     assert!(relay_url_is_valid("wss://relay.example.com"));
   1797     assert!(!relay_url_is_valid("ws://"));
   1798     assert!(!relay_url_is_valid("http://relay.example.com"));
   1799     assert!(!relay_url_is_valid(" wss://relay.example.com"));
   1800     assert!(!relay_url_is_valid("wss://relay.example.com "));
   1801     assert!(!relay_url_is_valid("wss://relay.example.com\nmiddle"));
   1802     assert!(tag_value_is_valid(
   1803         &owned_tag(&["title", "Soil Guide"]),
   1804         TagValueType::Text
   1805     ));
   1806     assert!(!tag_value_is_valid(
   1807         &owned_tag(&["title", "   "]),
   1808         TagValueType::Text
   1809     ));
   1810     assert!(!tag_value_is_valid(
   1811         &owned_tag(&["title", "Soil\nGuide"]),
   1812         TagValueType::Text
   1813     ));
   1814     assert!(tag_value_is_valid(
   1815         &owned_tag(&["expiration", "1700000000"]),
   1816         TagValueType::UnixTimestamp
   1817     ));
   1818     assert!(!tag_value_is_valid(
   1819         &owned_tag(&["expiration", "not-time"]),
   1820         TagValueType::UnixTimestamp
   1821     ));
   1822     assert!(tag_value_is_valid(
   1823         &owned_tag(&["image", "https://example.com"]),
   1824         TagValueType::Url
   1825     ));
   1826     assert!(!tag_value_is_valid(
   1827         &owned_tag(&["image", "wss://example.com"]),
   1828         TagValueType::Url
   1829     ));
   1830     assert!(url_is_valid("http://example.com"));
   1831     assert!(url_is_valid("https://example.com"));
   1832     assert!(!url_is_valid("http://"));
   1833     assert!(!url_is_valid("wss://example.com"));
   1834     assert!(!url_is_valid(" https://example.com"));
   1835     assert!(!url_is_valid("https://example.com "));
   1836     assert!(!url_is_valid("https://example.com\nmiddle"));
   1837     assert!(tag_value_is_valid(
   1838         &owned_tag(&["uuid", "123e4567-e89b-12d3-a456-426614174000"]),
   1839         TagValueType::Uuid
   1840     ));
   1841     assert!(!tag_value_is_valid(
   1842         &owned_tag(&["uuid", "123e4567-e89b-12d3-a456-42661417400"]),
   1843         TagValueType::Uuid
   1844     ));
   1845     assert!(uuid_is_valid("123e4567-e89b-12d3-a456-426614174000"));
   1846     assert!(!uuid_is_valid("123e4567-e89b-12d3-a456-42661417400"));
   1847     assert!(!uuid_is_valid("123e4567xe89b-12d3-a456-426614174000"));
   1848     assert!(!uuid_is_valid("123e4567-e89b-12d3-a456-42661417400x"));
   1849 
   1850     let expectations = [
   1851         (
   1852             TagValueType::AddressableCoordinate,
   1853             "addressable_coordinate",
   1854         ),
   1855         (TagValueType::ContractId, "contract_id"),
   1856         (TagValueType::DTag, "d_tag"),
   1857         (TagValueType::EventId, "event_id"),
   1858         (TagValueType::EventPointer, "event_pointer"),
   1859         (TagValueType::Geohash, "geohash"),
   1860         (TagValueType::Kind, "kind"),
   1861         (TagValueType::Nip01Coordinate, "nip01_coordinate"),
   1862         (TagValueType::PublicKey, "public_key"),
   1863         (TagValueType::RelayUrl, "relay_url"),
   1864         (TagValueType::Sha256, "sha256"),
   1865         (TagValueType::Text, "text"),
   1866         (TagValueType::UnixTimestamp, "unix_timestamp"),
   1867         (TagValueType::Url, "url"),
   1868         (TagValueType::Uuid, "uuid"),
   1869     ];
   1870     for (value_type, expected) in expectations {
   1871         assert_eq!(tag_value_type_expectation(value_type), expected);
   1872     }
   1873 }
   1874 
   1875 #[test]
   1876 fn validate_custom_knowledge_contract_rejects_missing_schema_and_bad_version() {
   1877     let missing_schema = unsigned_event(
   1878         KIND_KNOWLEDGE_CLAIM,
   1879         vec![vec!["contract", "radroots.knowledge.claim.v1"]],
   1880         r#"{"schema_version":1}"#,
   1881     );
   1882     assert_eq!(
   1883         validate_event_contract_shape(&missing_schema, "radroots.knowledge.claim.v1"),
   1884         Err(ContractValidationError::MissingContentField {
   1885             contract_id: "radroots.knowledge.claim.v1",
   1886             field: "schema",
   1887         })
   1888     );
   1889 
   1890     let bad_version = unsigned_event(
   1891         KIND_KNOWLEDGE_CLAIM,
   1892         vec![vec!["contract", "radroots.knowledge.claim.v1"]],
   1893         r#"{"schema":"radroots.knowledge.claim.v1","schema_version":2}"#,
   1894     );
   1895     assert_eq!(
   1896         validate_event_contract_shape(&bad_version, "radroots.knowledge.claim.v1"),
   1897         Err(ContractValidationError::ContentFieldMismatch {
   1898             contract_id: "radroots.knowledge.claim.v1",
   1899             field: "schema_version",
   1900             expected: "1".to_owned(),
   1901         })
   1902     );
   1903 }
   1904 
   1905 #[test]
   1906 fn validates_nip54_empty_redirect_content() {
   1907     let event = unsigned_event_owned(
   1908         KIND_WIKI_REDIRECT,
   1909         vec![
   1910             vec!["d".to_owned(), "soil".to_owned()],
   1911             vec![
   1912                 "a".to_owned(),
   1913                 format!("30818:{}:soil", crate::test_valid_hex_64('0')),
   1914             ],
   1915         ],
   1916         "",
   1917     );
   1918 
   1919     assert_eq!(
   1920         validate_event_contract_shape(&event, "radroots.wiki.redirect.v1"),
   1921         Ok(())
   1922     );
   1923 
   1924     let invalid = unsigned_event_owned(
   1925         KIND_WIKI_REDIRECT,
   1926         vec![
   1927             vec!["d".to_owned(), "soil".to_owned()],
   1928             vec![
   1929                 "a".to_owned(),
   1930                 format!("30818:{}:soil", crate::test_valid_hex_64('0')),
   1931             ],
   1932         ],
   1933         "{}",
   1934     );
   1935     assert_eq!(
   1936         validate_event_contract_shape(&invalid, "radroots.wiki.redirect.v1"),
   1937         Err(ContractValidationError::ContentMustBeEmpty {
   1938             contract_id: "radroots.wiki.redirect.v1",
   1939         })
   1940     );
   1941 }
   1942 
   1943 #[test]
   1944 fn exposes_validation_error_codes() {
   1945     for (error, code) in [
   1946         (
   1947             ContractValidationError::UnknownContract {
   1948                 contract_id: "missing".to_owned(),
   1949             },
   1950             "unknown_contract",
   1951         ),
   1952         (
   1953             ContractValidationError::AdmissionRequired {
   1954                 contract_id: "radroots.social.ask.v1",
   1955             },
   1956             "admission_required",
   1957         ),
   1958         (
   1959             ContractValidationError::ContractMatch {
   1960                 error: ContractMatchError::UnsupportedKind(999_999),
   1961             },
   1962             "contract_match",
   1963         ),
   1964         (
   1965             ContractValidationError::KindMismatch {
   1966                 expected: KIND_PROFILE,
   1967                 actual: KIND_POST,
   1968             },
   1969             "kind_mismatch",
   1970         ),
   1971         (
   1972             ContractValidationError::ContentMustBeEmpty {
   1973                 contract_id: "radroots.wiki.redirect.v1",
   1974             },
   1975             "content_must_be_empty",
   1976         ),
   1977         (
   1978             ContractValidationError::InvalidJsonContent {
   1979                 contract_id: "radroots.knowledge.claim.v1",
   1980             },
   1981             "invalid_json_content",
   1982         ),
   1983         (
   1984             ContractValidationError::MissingTag {
   1985                 contract_id: "radroots.knowledge.claim.v1",
   1986                 name: "contract",
   1987             },
   1988             "missing_tag",
   1989         ),
   1990         (
   1991             ContractValidationError::TagCardinalityMismatch {
   1992                 contract_id: "radroots.knowledge.claim.v1",
   1993                 name: "contract",
   1994             },
   1995             "tag_cardinality_mismatch",
   1996         ),
   1997         (
   1998             ContractValidationError::TagValueMismatch {
   1999                 contract_id: "radroots.knowledge.claim.v1",
   2000                 name: "contract",
   2001                 expected: "radroots.knowledge.claim.v1".to_owned(),
   2002                 actual: None,
   2003             },
   2004             "tag_value_mismatch",
   2005         ),
   2006         (
   2007             ContractValidationError::MissingContentField {
   2008                 contract_id: "radroots.knowledge.claim.v1",
   2009                 field: "schema",
   2010             },
   2011             "missing_content_field",
   2012         ),
   2013         (
   2014             ContractValidationError::ContentFieldMismatch {
   2015                 contract_id: "radroots.knowledge.claim.v1",
   2016                 field: "schema",
   2017                 expected: "radroots.knowledge.claim.v1".to_owned(),
   2018             },
   2019             "content_field_mismatch",
   2020         ),
   2021         (
   2022             ContractValidationError::ForbiddenContentField {
   2023                 contract_id: "radroots.knowledge.claim.v1",
   2024                 field: "review_status",
   2025             },
   2026             "forbidden_content_field",
   2027         ),
   2028     ] {
   2029         assert_eq!(error.code(), code);
   2030     }
   2031 }
   2032 
   2033 #[test]
   2034 fn tag_helpers_cover_missing_names_and_cardinality_mismatches() {
   2035     let tags = vec![
   2036         vec!["p".to_owned(), "counterparty".to_owned()],
   2037         vec!["d".to_owned()],
   2038     ];
   2039 
   2040     assert_eq!(tag_value(&tags, "d"), None);
   2041     assert_eq!(tag_value(&tags, "p"), Some("counterparty"));
   2042 
   2043     let malformed = [
   2044         tag(
   2045             "d",
   2046             TagCardinality::OptionalOne,
   2047             TagSemantic::Identifier,
   2048             TagValueType::DTag,
   2049             true,
   2050         ),
   2051         tag(
   2052             "p",
   2053             TagCardinality::RequiredOne,
   2054             TagSemantic::Counterparty,
   2055             TagValueType::PublicKey,
   2056             true,
   2057         ),
   2058     ];
   2059 
   2060     assert!(
   2061         !malformed
   2062             .iter()
   2063             .any(|tag| tag.name == "d" && tag.cardinality == TagCardinality::RequiredOne)
   2064     );
   2065 }
   2066 
   2067 #[test]
   2068 fn relay_indexed_tags_are_single_letter() {
   2069     for contract in all_event_contracts() {
   2070         for tag in contract.tags {
   2071             if tag.relay_indexed {
   2072                 assert_eq!(tag.name.len(), 1, "{}:{}", contract.id, tag.name);
   2073             }
   2074         }
   2075     }
   2076 }
   2077 
   2078 #[test]
   2079 fn addressable_event_contracts_require_d_tags() {
   2080     for contract in all_event_contracts() {
   2081         if contract.class == EventClass::Addressable {
   2082             let d_tag_cardinality = contract
   2083                 .tags
   2084                 .iter()
   2085                 .find(|tag| tag.name == "d")
   2086                 .map(|tag| tag.cardinality);
   2087             assert_eq!(
   2088                 d_tag_cardinality,
   2089                 Some(TagCardinality::RequiredOne),
   2090                 "{}",
   2091                 contract.id
   2092             );
   2093         }
   2094     }
   2095 }