knowledge.rs (53093B)
1 #![cfg(feature = "knowledge")] 2 3 use radroots_event::contract::validate_event_contract_shape; 4 use radroots_event::envelope::kind::{ 5 KIND_KNOWLEDGE_CLAIM, KIND_KNOWLEDGE_REVIEW, KIND_KNOWLEDGE_SOURCE, KIND_WIKI_ARTICLE, 6 }; 7 use radroots_event::knowledge::{ 8 AddressableRef, ContributionAttestation, EvidenceBounty, KnowledgeChangeProposal, 9 KnowledgeCitationSpan, KnowledgeClaim, KnowledgeFieldContext, KnowledgeFieldReport, 10 KnowledgeLocation, KnowledgeLocationPrecision, KnowledgeNodeRef, KnowledgeObservation, 11 KnowledgeObservationValue, KnowledgeRelation, KnowledgeReview, KnowledgeReviewScope, 12 KnowledgeReviewScore, KnowledgeReviewTarget, KnowledgeSource, 13 RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA, RADROOTS_EVIDENCE_BOUNTY_SCHEMA, 14 RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, 15 RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, RADROOTS_KNOWLEDGE_RELATION_SCHEMA, 16 RADROOTS_KNOWLEDGE_REVIEW_SCHEMA, RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 17 RADROOTS_KNOWLEDGE_SOURCE_SCHEMA, WikiArticle, WikiArticleVersionRef, WikiMergeRequest, 18 WikiRedirect, 19 }; 20 use radroots_event::wire::Nip01EventWireParts; 21 use radroots_event::{envelope::EventEnvelope, envelope::EventEnvelopeParts}; 22 use radroots_event_codec::decode::EventParseError; 23 use radroots_event_codec::decode::knowledge::{ 24 contribution_attestation_from_event, evidence_bounty_from_event, 25 knowledge_change_proposal_from_event, knowledge_claim_from_event, 26 knowledge_field_report_from_event, knowledge_relation_from_event, knowledge_review_from_event, 27 knowledge_source_from_event, wiki_article_from_event, wiki_merge_request_from_event, 28 wiki_redirect_from_event, 29 }; 30 use radroots_event_codec::encode::EventEncodeError; 31 use radroots_event_codec::encode::knowledge::{ 32 contribution_attestation_to_wire_parts, evidence_bounty_to_wire_parts, 33 knowledge_change_proposal_to_wire_parts, knowledge_claim_to_wire_parts, 34 knowledge_field_report_to_wire_parts, knowledge_relation_to_wire_parts, 35 knowledge_review_to_wire_parts, knowledge_source_to_wire_parts, wiki_article_to_wire_parts, 36 wiki_merge_request_to_wire_parts, wiki_redirect_to_wire_parts, 37 }; 38 use radroots_event_codec::verify::{ 39 RadrootsDecodeError, RadrootsDecodedEvent, RadrootsNip01VerificationError, 40 verify_and_decode_radroots_event, 41 }; 42 43 fn hex_64(character: char) -> String { 44 core::iter::repeat_n(character, 64).collect() 45 } 46 47 fn event_ref(character: char, kind: u32) -> radroots_event::tag::EventRef { 48 radroots_event::tag::EventRef { 49 id: hex_64(character), 50 author: radroots_identity::PublicKey::from_hex( 51 "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df", 52 ) 53 .expect("fixture public key"), 54 kind, 55 d_tag: None, 56 relays: Some(vec!["wss://relay.radroots.example".to_string()]), 57 } 58 } 59 60 fn address_ref() -> AddressableRef { 61 AddressableRef { 62 kind: KIND_WIKI_ARTICLE, 63 pubkey: hex_64('a'), 64 d_tag: "soil-health".to_string(), 65 relays: vec!["wss://relay.radroots.example".to_string()], 66 } 67 } 68 69 fn article_version_ref() -> WikiArticleVersionRef { 70 article_version_ref_for('b', "soil-health") 71 } 72 73 fn article_version_ref_for(event_id_character: char, d_tag: &str) -> WikiArticleVersionRef { 74 let mut address_ref = address_ref(); 75 address_ref.d_tag = d_tag.to_string(); 76 WikiArticleVersionRef { 77 event_id: hex_64(event_id_character), 78 address_ref, 79 } 80 } 81 82 fn event_from_parts(parts: Nip01EventWireParts) -> EventEnvelope { 83 EventEnvelope::new(EventEnvelopeParts { 84 id: hex_64('0'), 85 author: hex_64('a'), 86 created_at: 1_800_000_000, 87 kind: parts.kind, 88 tags: parts.tags, 89 content: parts.content, 90 sig: "1".repeat(128), 91 }) 92 .unwrap() 93 } 94 95 fn event_with_parts( 96 event: &EventEnvelope, 97 tags: Vec<Vec<String>>, 98 content: String, 99 ) -> EventEnvelope { 100 EventEnvelope::new(EventEnvelopeParts { 101 id: event.id_hex().to_string(), 102 author: event.author().to_hex().to_string(), 103 created_at: event.created_at_u64(), 104 kind: event.kind_u32(), 105 tags, 106 content, 107 sig: event.signature_hex().to_string(), 108 }) 109 .unwrap() 110 } 111 112 fn mutate_tags(event: &mut EventEnvelope, update: impl FnOnce(&mut Vec<Vec<String>>)) { 113 let mut tags = event.tags_as_vec(); 114 update(&mut tags); 115 *event = event_with_parts(event, tags, event.content().to_string()); 116 } 117 118 fn replace_content(event: &mut EventEnvelope, content: String) { 119 *event = event_with_parts(event, event.tags_as_vec(), content); 120 } 121 122 fn replace_first_tag_value(event: &mut EventEnvelope, name: &str, value: String) { 123 mutate_tags(event, |tags| { 124 let tag = tags 125 .iter_mut() 126 .find(|tag| tag.first().map(String::as_str) == Some(name)) 127 .expect("tag"); 128 let tag_value = tag.get_mut(1).expect("tag value"); 129 *tag_value = value; 130 }); 131 } 132 133 fn marked_tag(tag: &[String], name: &str, marker: &str) -> bool { 134 tag.first().map(String::as_str) == Some(name) && tag.last().map(String::as_str) == Some(marker) 135 } 136 137 fn marked_tag_index(event: &EventEnvelope, name: &str, marker: &str) -> usize { 138 event 139 .tags_as_vec() 140 .iter() 141 .position(|tag| marked_tag(tag, name, marker)) 142 .expect("marked tag") 143 } 144 145 fn assert_parse_error(actual: EventParseError, expected: EventParseError) { 146 match (actual, expected) { 147 (EventParseError::MissingTag(actual), EventParseError::MissingTag(expected)) 148 | (EventParseError::InvalidTag(actual), EventParseError::InvalidTag(expected)) 149 | (EventParseError::InvalidJson(actual), EventParseError::InvalidJson(expected)) => { 150 assert_eq!(actual, expected); 151 } 152 ( 153 EventParseError::InvalidKind { 154 expected: actual_expected, 155 got: actual_got, 156 }, 157 EventParseError::InvalidKind { expected, got }, 158 ) => { 159 assert_eq!(actual_expected, expected); 160 assert_eq!(actual_got, got); 161 } 162 (actual, expected) => panic!("expected {expected:?}, got {actual:?}"), 163 } 164 } 165 166 fn assert_encode_error(actual: EventEncodeError, expected: EventEncodeError) { 167 match (actual, expected) { 168 ( 169 EventEncodeError::EmptyRequiredField(actual), 170 EventEncodeError::EmptyRequiredField(expected), 171 ) 172 | (EventEncodeError::InvalidField(actual), EventEncodeError::InvalidField(expected)) => { 173 assert_eq!(actual, expected); 174 } 175 (actual, expected) => panic!("expected {expected:?}, got {actual:?}"), 176 } 177 } 178 179 fn invalid_relay() -> String { 180 "http://relay.radroots.example".to_string() 181 } 182 183 fn sign_parts(parts: Nip01EventWireParts) -> EventEnvelope { 184 let tags = parts 185 .tags 186 .into_iter() 187 .map(nostr::Tag::parse) 188 .collect::<Result<Vec<_>, _>>() 189 .expect("tags"); 190 let keys = 191 nostr::Keys::parse("0101010101010101010101010101010101010101010101010101010101010101") 192 .expect("keys"); 193 let kind = u16::try_from(parts.kind).expect("knowledge event kind must fit NIP-01"); 194 let event = nostr::EventBuilder::new(nostr::Kind::Custom(kind), parts.content) 195 .tags(tags) 196 .custom_created_at(nostr::Timestamp::from_secs(1_800_000_000)) 197 .sign_with_keys(&keys) 198 .expect("signed event"); 199 EventEnvelope::new(EventEnvelopeParts { 200 id: event.id.to_hex(), 201 author: event.pubkey.to_hex(), 202 created_at: event.created_at.as_secs(), 203 kind: u32::from(event.kind.as_u16()), 204 tags: event 205 .tags 206 .as_slice() 207 .iter() 208 .map(|tag| tag.as_slice().to_vec()) 209 .collect(), 210 content: event.content, 211 sig: event.sig.to_string(), 212 }) 213 .unwrap() 214 } 215 216 fn wiki_article() -> WikiArticle { 217 WikiArticle { 218 d_tag: "soil-health".to_string(), 219 title: Some("Soil health".to_string()), 220 content_djot: "# Soil health".to_string(), 221 summary: Some("Living soil basics".to_string()), 222 topics: vec!["soil".to_string(), "health".to_string()], 223 references: vec![event_ref('2', KIND_KNOWLEDGE_SOURCE)], 224 forked_from: vec![article_version_ref()], 225 deferred_to: Some(article_version_ref()), 226 } 227 } 228 229 fn source() -> KnowledgeSource { 230 KnowledgeSource { 231 schema: RADROOTS_KNOWLEDGE_SOURCE_SCHEMA.to_string(), 232 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 233 d_tag: "soil-source".to_string(), 234 title: "Soil Source".to_string(), 235 source_type: "book".to_string(), 236 authors: vec!["A. Example".to_string()], 237 publisher: Some("Radroots Synthetic Press".to_string()), 238 publication_year: Some(2026), 239 edition: None, 240 canonical_url: None, 241 artifact_refs: vec![event_ref('3', 1063)], 242 author_asserted_rights: None, 243 topics: vec!["soil".to_string()], 244 summary: Some("Synthetic source".to_string()), 245 } 246 } 247 248 fn claim() -> KnowledgeClaim { 249 KnowledgeClaim { 250 schema: RADROOTS_KNOWLEDGE_CLAIM_SCHEMA.to_string(), 251 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 252 claim_type: "practice_effect".to_string(), 253 text: "Cover crops improve soil structure.".to_string(), 254 citation_spans: vec![KnowledgeCitationSpan { 255 source_ref: event_ref('4', KIND_KNOWLEDGE_SOURCE), 256 artifact_ref: None, 257 page_start: Some(12), 258 page_end: Some(13), 259 section_path: vec!["chapter-1".to_string()], 260 quote_hash: Some(hex_64('5')), 261 chunk_id: Some("chunk-1".to_string()), 262 }], 263 topics: vec!["cover-crops".to_string()], 264 applies_to: vec!["local-food".to_string()], 265 author_asserted_confidence: Some("medium".to_string()), 266 supersedes: Vec::new(), 267 } 268 } 269 270 fn node_ref(label: &str) -> KnowledgeNodeRef { 271 KnowledgeNodeRef { 272 node_type: "event".to_string(), 273 event_ref: Some(event_ref('6', KIND_KNOWLEDGE_CLAIM)), 274 address_ref: None, 275 external_id: None, 276 label: Some(label.to_string()), 277 } 278 } 279 280 fn relation() -> KnowledgeRelation { 281 KnowledgeRelation { 282 schema: RADROOTS_KNOWLEDGE_RELATION_SCHEMA.to_string(), 283 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 284 subject: node_ref("cover crops"), 285 predicate: "supports".to_string(), 286 object: node_ref("soil structure"), 287 support_refs: vec![event_ref('7', KIND_KNOWLEDGE_CLAIM)], 288 author_asserted_confidence: Some("medium".to_string()), 289 supersedes: Vec::new(), 290 } 291 } 292 293 fn review() -> KnowledgeReview { 294 KnowledgeReview { 295 schema: RADROOTS_KNOWLEDGE_REVIEW_SCHEMA.to_string(), 296 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 297 target: KnowledgeReviewTarget { 298 event_id: hex_64('8'), 299 author_pubkey: hex_64('a'), 300 kind: KIND_KNOWLEDGE_CLAIM, 301 address: None, 302 relays: vec!["wss://relay.radroots.example".to_string()], 303 review_scope: KnowledgeReviewScope::SpecificVersion, 304 }, 305 reviewer_role: "peer".to_string(), 306 verdict: "needs_more_evidence".to_string(), 307 scores: vec![KnowledgeReviewScore { 308 dimension: "evidence".to_string(), 309 value: "partial".to_string(), 310 note: None, 311 }], 312 notes: Some("Synthetic review".to_string()), 313 evidence_refs: vec![event_ref('9', KIND_KNOWLEDGE_SOURCE)], 314 } 315 } 316 317 fn field_report() -> KnowledgeFieldReport { 318 KnowledgeFieldReport { 319 schema: RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA.to_string(), 320 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 321 report_type: "observation".to_string(), 322 title: "Field observation".to_string(), 323 summary: Some("Observed cover crop residue.".to_string()), 324 context: KnowledgeFieldContext { 325 location_precision: KnowledgeLocationPrecision::CoarseGeohash, 326 public_location: Some(KnowledgeLocation { 327 label: Some("watershed".to_string()), 328 region: Some("synthetic-region".to_string()), 329 locality: None, 330 geohash: Some("c23".to_string()), 331 }), 332 private_location_ref: None, 333 topics: vec!["field".to_string()], 334 context_tags: vec!["observation".to_string()], 335 }, 336 observations: vec![KnowledgeObservation { 337 observation_type: "residue".to_string(), 338 text: "Residue was visible across beds.".to_string(), 339 observed_at: Some("2026-07-05".to_string()), 340 values: vec![KnowledgeObservationValue { 341 key: "coverage".to_string(), 342 value: "medium".to_string(), 343 unit: None, 344 }], 345 }], 346 artifact_refs: vec![event_ref('c', 1063)], 347 related_refs: vec![event_ref('d', KIND_KNOWLEDGE_CLAIM)], 348 limitations: vec!["single observer".to_string()], 349 } 350 } 351 352 fn bounty() -> EvidenceBounty { 353 EvidenceBounty { 354 schema: RADROOTS_EVIDENCE_BOUNTY_SCHEMA.to_string(), 355 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 356 d_tag: "soil-bounty".to_string(), 357 title: "Soil bounty".to_string(), 358 summary: None, 359 topics: vec!["soil".to_string()], 360 target_refs: vec![event_ref('a', KIND_KNOWLEDGE_CLAIM)], 361 reward_note: None, 362 closes_at: None, 363 } 364 } 365 366 fn proposal() -> KnowledgeChangeProposal { 367 KnowledgeChangeProposal { 368 schema: RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA.to_string(), 369 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 370 target: event_ref('b', KIND_KNOWLEDGE_CLAIM), 371 proposal_type: "amend".to_string(), 372 summary: "Clarify scope".to_string(), 373 rationale: None, 374 evidence_refs: vec![event_ref('c', KIND_KNOWLEDGE_SOURCE)], 375 supersedes: Vec::new(), 376 } 377 } 378 379 fn attestation() -> ContributionAttestation { 380 ContributionAttestation { 381 schema: RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA.to_string(), 382 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 383 contributor_pubkey: hex_64('a'), 384 contribution_type: "review".to_string(), 385 subject_refs: vec![event_ref('d', KIND_KNOWLEDGE_REVIEW)], 386 summary: "Reviewed synthetic claim".to_string(), 387 evidence_refs: vec![event_ref('e', KIND_KNOWLEDGE_REVIEW)], 388 } 389 } 390 391 #[test] 392 fn knowledge_codecs_roundtrip_all_contracts() { 393 let article_event = event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); 394 validate_event_contract_shape(&article_event, "radroots.wiki.article.v1").unwrap(); 395 assert!(article_event.tags_as_vec().iter().any(|tag| tag 396 == &vec![ 397 "a".to_string(), 398 format!("30818:{}:soil-health", hex_64('a')), 399 "wss://relay.radroots.example".to_string(), 400 "fork".to_string() 401 ])); 402 assert!(article_event.tags_as_vec().iter().any(|tag| tag 403 == &vec![ 404 "e".to_string(), 405 hex_64('b'), 406 "wss://relay.radroots.example".to_string(), 407 "fork".to_string() 408 ])); 409 assert!(article_event.tags_as_vec().iter().any(|tag| tag 410 == &vec![ 411 "a".to_string(), 412 format!("30818:{}:soil-health", hex_64('a')), 413 "wss://relay.radroots.example".to_string(), 414 "defer".to_string() 415 ])); 416 assert!(article_event.tags_as_vec().iter().any(|tag| tag 417 == &vec![ 418 "e".to_string(), 419 hex_64('b'), 420 "wss://relay.radroots.example".to_string(), 421 "defer".to_string() 422 ])); 423 assert_eq!( 424 wiki_article_from_event(article_event) 425 .unwrap() 426 .data 427 .data 428 .title, 429 Some("Soil health".to_string()) 430 ); 431 432 let redirect = WikiRedirect { 433 d_tag: "soil".to_string(), 434 target: address_ref(), 435 }; 436 let redirect_event = event_from_parts(wiki_redirect_to_wire_parts(&redirect).unwrap()); 437 validate_event_contract_shape(&redirect_event, "radroots.wiki.redirect.v1").unwrap(); 438 assert!(redirect_event.tags_as_vec().iter().any(|tag| tag 439 == &vec![ 440 "a".to_string(), 441 format!("30818:{}:soil-health", hex_64('a')), 442 "wss://relay.radroots.example".to_string() 443 ])); 444 assert_eq!( 445 wiki_redirect_from_event(redirect_event) 446 .unwrap() 447 .data 448 .data 449 .target 450 .d_tag, 451 "soil-health" 452 ); 453 454 let merge = WikiMergeRequest { 455 target_article: address_ref(), 456 destination_pubkey: hex_64('a'), 457 base_version_event_id: Some(hex_64('e')), 458 source_version_event_id: hex_64('f'), 459 explanation: Some("Merge synthetic source".to_string()), 460 }; 461 let merge_event = event_from_parts(wiki_merge_request_to_wire_parts(&merge).unwrap()); 462 validate_event_contract_shape(&merge_event, "radroots.wiki.merge_request.v1").unwrap(); 463 assert_eq!(merge_event.content(), "Merge synthetic source"); 464 assert!( 465 merge_event 466 .tags_as_vec() 467 .iter() 468 .any(|tag| tag == &vec!["e".to_string(), hex_64('e'), String::new()]) 469 ); 470 assert!(merge_event.tags_as_vec().iter().any(|tag| tag 471 == &vec![ 472 "e".to_string(), 473 hex_64('f'), 474 String::new(), 475 "source".to_string() 476 ])); 477 assert_eq!( 478 wiki_merge_request_from_event(merge_event) 479 .unwrap() 480 .data 481 .data 482 .source_version_event_id, 483 hex_64('f') 484 ); 485 486 let source_event = event_from_parts(knowledge_source_to_wire_parts(&source()).unwrap()); 487 validate_event_contract_shape(&source_event, RADROOTS_KNOWLEDGE_SOURCE_SCHEMA).unwrap(); 488 assert_eq!( 489 knowledge_source_from_event(source_event) 490 .unwrap() 491 .data 492 .data 493 .source_type, 494 "book" 495 ); 496 497 let claim_event = event_from_parts(knowledge_claim_to_wire_parts(&claim()).unwrap()); 498 validate_event_contract_shape(&claim_event, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA).unwrap(); 499 assert_eq!( 500 knowledge_claim_from_event(claim_event) 501 .unwrap() 502 .data 503 .data 504 .claim_type, 505 "practice_effect" 506 ); 507 508 let relation_event = event_from_parts(knowledge_relation_to_wire_parts(&relation()).unwrap()); 509 validate_event_contract_shape(&relation_event, RADROOTS_KNOWLEDGE_RELATION_SCHEMA).unwrap(); 510 assert_eq!( 511 knowledge_relation_from_event(relation_event) 512 .unwrap() 513 .data 514 .data 515 .predicate, 516 "supports" 517 ); 518 519 let review_event = event_from_parts(knowledge_review_to_wire_parts(&review()).unwrap()); 520 validate_event_contract_shape(&review_event, RADROOTS_KNOWLEDGE_REVIEW_SCHEMA).unwrap(); 521 assert_eq!( 522 knowledge_review_from_event(review_event) 523 .unwrap() 524 .data 525 .data 526 .verdict, 527 "needs_more_evidence" 528 ); 529 530 let report_event = 531 event_from_parts(knowledge_field_report_to_wire_parts(&field_report()).unwrap()); 532 validate_event_contract_shape(&report_event, RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA).unwrap(); 533 assert_eq!( 534 knowledge_field_report_from_event(report_event) 535 .unwrap() 536 .data 537 .data 538 .report_type, 539 "observation" 540 ); 541 542 let bounty = EvidenceBounty { 543 schema: RADROOTS_EVIDENCE_BOUNTY_SCHEMA.to_string(), 544 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 545 d_tag: "soil-bounty".to_string(), 546 title: "Soil bounty".to_string(), 547 summary: None, 548 topics: vec!["soil".to_string()], 549 target_refs: vec![event_ref('a', KIND_KNOWLEDGE_CLAIM)], 550 reward_note: None, 551 closes_at: None, 552 }; 553 let bounty_event = event_from_parts(evidence_bounty_to_wire_parts(&bounty).unwrap()); 554 validate_event_contract_shape(&bounty_event, RADROOTS_EVIDENCE_BOUNTY_SCHEMA).unwrap(); 555 assert_eq!( 556 evidence_bounty_from_event(bounty_event) 557 .unwrap() 558 .data 559 .data 560 .title, 561 "Soil bounty" 562 ); 563 564 let proposal = KnowledgeChangeProposal { 565 schema: RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA.to_string(), 566 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 567 target: event_ref('b', KIND_KNOWLEDGE_CLAIM), 568 proposal_type: "amend".to_string(), 569 summary: "Clarify scope".to_string(), 570 rationale: None, 571 evidence_refs: vec![event_ref('c', KIND_KNOWLEDGE_SOURCE)], 572 supersedes: Vec::new(), 573 }; 574 let proposal_event = 575 event_from_parts(knowledge_change_proposal_to_wire_parts(&proposal).unwrap()); 576 validate_event_contract_shape(&proposal_event, RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA) 577 .unwrap(); 578 assert_eq!( 579 knowledge_change_proposal_from_event(proposal_event) 580 .unwrap() 581 .data 582 .data 583 .proposal_type, 584 "amend" 585 ); 586 587 let attestation = ContributionAttestation { 588 schema: RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA.to_string(), 589 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 590 contributor_pubkey: hex_64('a'), 591 contribution_type: "review".to_string(), 592 subject_refs: vec![event_ref('d', KIND_KNOWLEDGE_REVIEW)], 593 summary: "Reviewed synthetic claim".to_string(), 594 evidence_refs: vec![event_ref('e', KIND_KNOWLEDGE_REVIEW)], 595 }; 596 let attestation_event = 597 event_from_parts(contribution_attestation_to_wire_parts(&attestation).unwrap()); 598 validate_event_contract_shape(&attestation_event, RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA) 599 .unwrap(); 600 assert_eq!( 601 contribution_attestation_from_event(attestation_event) 602 .unwrap() 603 .data 604 .data 605 .contribution_type, 606 "review" 607 ); 608 } 609 610 #[test] 611 fn knowledge_decode_rejects_mirrored_tag_content_drift() { 612 let mut source_event = event_from_parts(knowledge_source_to_wire_parts(&source()).unwrap()); 613 replace_first_tag_value(&mut source_event, "source", hex_64('4')); 614 assert_parse_error( 615 knowledge_source_from_event(source_event).unwrap_err(), 616 EventParseError::InvalidTag("source"), 617 ); 618 619 let mut bounty_event = event_from_parts(evidence_bounty_to_wire_parts(&bounty()).unwrap()); 620 replace_first_tag_value(&mut bounty_event, "evidence", hex_64('b')); 621 assert_parse_error( 622 evidence_bounty_from_event(bounty_event).unwrap_err(), 623 EventParseError::InvalidTag("evidence"), 624 ); 625 626 let mut claim_source_event = event_from_parts(knowledge_claim_to_wire_parts(&claim()).unwrap()); 627 replace_first_tag_value(&mut claim_source_event, "source", hex_64('6')); 628 assert_parse_error( 629 knowledge_claim_from_event(claim_source_event).unwrap_err(), 630 EventParseError::InvalidTag("source"), 631 ); 632 633 let mut claim_citation_event = 634 event_from_parts(knowledge_claim_to_wire_parts(&claim()).unwrap()); 635 replace_first_tag_value(&mut claim_citation_event, "citation", hex_64('6')); 636 assert_parse_error( 637 knowledge_claim_from_event(claim_citation_event).unwrap_err(), 638 EventParseError::InvalidTag("citation"), 639 ); 640 641 let mut relation_event = 642 event_from_parts(knowledge_relation_to_wire_parts(&relation()).unwrap()); 643 replace_first_tag_value(&mut relation_event, "source", hex_64('8')); 644 assert_parse_error( 645 knowledge_relation_from_event(relation_event).unwrap_err(), 646 EventParseError::InvalidTag("source"), 647 ); 648 649 let mut review_target_event = 650 event_from_parts(knowledge_review_to_wire_parts(&review()).unwrap()); 651 replace_first_tag_value(&mut review_target_event, "review_target", hex_64('9')); 652 assert_parse_error( 653 knowledge_review_from_event(review_target_event).unwrap_err(), 654 EventParseError::InvalidTag("review_target"), 655 ); 656 657 let mut report_geohash_event = 658 event_from_parts(knowledge_field_report_to_wire_parts(&field_report()).unwrap()); 659 replace_first_tag_value(&mut report_geohash_event, "g", "c24".to_string()); 660 assert_parse_error( 661 knowledge_field_report_from_event(report_geohash_event).unwrap_err(), 662 EventParseError::InvalidTag("g"), 663 ); 664 665 let mut report_topic_event = 666 event_from_parts(knowledge_field_report_to_wire_parts(&field_report()).unwrap()); 667 replace_first_tag_value(&mut report_topic_event, "t", "soil".to_string()); 668 assert_parse_error( 669 knowledge_field_report_from_event(report_topic_event).unwrap_err(), 670 EventParseError::InvalidTag("t"), 671 ); 672 673 let mut report_evidence_event = 674 event_from_parts(knowledge_field_report_to_wire_parts(&field_report()).unwrap()); 675 replace_first_tag_value(&mut report_evidence_event, "evidence", hex_64('e')); 676 assert_parse_error( 677 knowledge_field_report_from_event(report_evidence_event).unwrap_err(), 678 EventParseError::InvalidTag("evidence"), 679 ); 680 681 let mut proposal_event = 682 event_from_parts(knowledge_change_proposal_to_wire_parts(&proposal()).unwrap()); 683 replace_first_tag_value(&mut proposal_event, "evidence", hex_64('d')); 684 assert_parse_error( 685 knowledge_change_proposal_from_event(proposal_event).unwrap_err(), 686 EventParseError::InvalidTag("evidence"), 687 ); 688 689 let mut attestation_event = 690 event_from_parts(contribution_attestation_to_wire_parts(&attestation()).unwrap()); 691 replace_first_tag_value(&mut attestation_event, "evidence", hex_64('f')); 692 assert_parse_error( 693 contribution_attestation_from_event(attestation_event).unwrap_err(), 694 EventParseError::InvalidTag("evidence"), 695 ); 696 } 697 698 #[test] 699 fn verified_decode_accepts_signed_claim_and_rejects_mutation() { 700 let signed = sign_parts(knowledge_claim_to_wire_parts(&claim()).unwrap()); 701 let decoded = verify_and_decode_radroots_event(signed.clone()).unwrap(); 702 assert!(matches!(decoded, RadrootsDecodedEvent::KnowledgeClaim(_))); 703 704 let mut mutated = signed; 705 let mutated_content = mutated.content().replace("Cover crops", "Compost"); 706 replace_content(&mut mutated, mutated_content); 707 let err = verify_and_decode_radroots_event(mutated).unwrap_err(); 708 assert_eq!(err.code(), "nip01_verification"); 709 assert!(matches!( 710 err, 711 RadrootsDecodeError::Nip01Verification(RadrootsNip01VerificationError::IdMismatch { .. }) 712 )); 713 } 714 715 #[test] 716 fn malformed_knowledge_events_return_stable_decode_codes() { 717 let mut missing_contract = event_from_parts(knowledge_claim_to_wire_parts(&claim()).unwrap()); 718 mutate_tags(&mut missing_contract, |tags| { 719 tags.retain(|tag| tag.first().map(|value| value.as_str()) != Some("contract")); 720 }); 721 let signed = sign_parts(Nip01EventWireParts { 722 kind: missing_contract.kind_u32(), 723 content: missing_contract.content().to_string(), 724 tags: missing_contract.tags_as_vec(), 725 }); 726 let error = verify_and_decode_radroots_event(signed).unwrap_err(); 727 assert_eq!(error.code(), "contract_validation"); 728 729 let mut report = 730 event_from_parts(knowledge_field_report_to_wire_parts(&field_report()).unwrap()); 731 let mut value: serde_json::Value = serde_json::from_str(report.content()).unwrap(); 732 value["context"]["latitude"] = serde_json::Value::from("45.0000"); 733 replace_content(&mut report, serde_json::to_string(&value).unwrap()); 734 let parsed_error = knowledge_field_report_from_event(report).unwrap_err(); 735 assert_eq!(parsed_error.code(), "invalid_json"); 736 } 737 738 #[test] 739 fn malformed_nip54_wiki_shapes_are_rejected() { 740 let mut redirect = event_from_parts( 741 wiki_redirect_to_wire_parts(&WikiRedirect { 742 d_tag: "soil".to_string(), 743 target: address_ref(), 744 }) 745 .unwrap(), 746 ); 747 mutate_tags(&mut redirect, |tags| { 748 for tag in tags { 749 if tag.first().map(|value| value.as_str()) == Some("a") { 750 tag[1] = format!("30023:{}:soil-health", hex_64('a')); 751 } 752 } 753 }); 754 assert_parse_error( 755 wiki_redirect_from_event(redirect).unwrap_err(), 756 EventParseError::InvalidTag("a"), 757 ); 758 759 let merge = WikiMergeRequest { 760 target_article: address_ref(), 761 destination_pubkey: hex_64('a'), 762 base_version_event_id: Some(hex_64('e')), 763 source_version_event_id: hex_64('f'), 764 explanation: Some("Merge synthetic source".to_string()), 765 }; 766 let mut missing_target = event_from_parts(wiki_merge_request_to_wire_parts(&merge).unwrap()); 767 mutate_tags(&mut missing_target, |tags| { 768 tags.retain(|tag| tag.first().map(|value| value.as_str()) != Some("a")); 769 }); 770 assert_parse_error( 771 wiki_merge_request_from_event(missing_target).unwrap_err(), 772 EventParseError::MissingTag("a"), 773 ); 774 775 let mut missing_destination = 776 event_from_parts(wiki_merge_request_to_wire_parts(&merge).unwrap()); 777 mutate_tags(&mut missing_destination, |tags| { 778 tags.retain(|tag| tag.first().map(|value| value.as_str()) != Some("p")); 779 }); 780 assert_parse_error( 781 wiki_merge_request_from_event(missing_destination).unwrap_err(), 782 EventParseError::MissingTag("p"), 783 ); 784 785 let mut missing_source = event_from_parts(wiki_merge_request_to_wire_parts(&merge).unwrap()); 786 mutate_tags(&mut missing_source, |tags| { 787 tags.retain(|tag| { 788 !(tag.first().map(|value| value.as_str()) == Some("e") 789 && tag.last().map(|value| value.as_str()) == Some("source")) 790 }); 791 }); 792 assert_parse_error( 793 wiki_merge_request_from_event(missing_source).unwrap_err(), 794 EventParseError::InvalidTag("e"), 795 ); 796 797 let mut duplicate_source = event_from_parts(wiki_merge_request_to_wire_parts(&merge).unwrap()); 798 mutate_tags(&mut duplicate_source, |tags| { 799 tags.push(vec![ 800 "e".to_string(), 801 hex_64('a'), 802 String::new(), 803 "source".to_string(), 804 ]); 805 }); 806 assert_parse_error( 807 wiki_merge_request_from_event(duplicate_source).unwrap_err(), 808 EventParseError::InvalidTag("e"), 809 ); 810 811 let mut wrong_merge_target = 812 event_from_parts(wiki_merge_request_to_wire_parts(&merge).unwrap()); 813 mutate_tags(&mut wrong_merge_target, |tags| { 814 for tag in tags { 815 if tag.first().map(|value| value.as_str()) == Some("a") { 816 tag[1] = format!("30023:{}:soil-health", hex_64('a')); 817 } 818 } 819 }); 820 assert_parse_error( 821 wiki_merge_request_from_event(wrong_merge_target).unwrap_err(), 822 EventParseError::InvalidTag("a"), 823 ); 824 825 let mut orphan_fork = event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); 826 let mut removed_fork_event = false; 827 mutate_tags(&mut orphan_fork, |tags| { 828 tags.retain(|tag| { 829 if !removed_fork_event 830 && tag.first().map(|value| value.as_str()) == Some("e") 831 && tag.last().map(|value| value.as_str()) == Some("fork") 832 { 833 removed_fork_event = true; 834 false 835 } else { 836 true 837 } 838 }); 839 }); 840 assert_parse_error( 841 wiki_article_from_event(orphan_fork).unwrap_err(), 842 EventParseError::InvalidTag("a"), 843 ); 844 845 let mut duplicate_defer = 846 event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); 847 mutate_tags(&mut duplicate_defer, |tags| { 848 tags.extend([ 849 vec![ 850 "a".to_string(), 851 format!("30818:{}:compost", hex_64('a')), 852 String::new(), 853 "defer".to_string(), 854 ], 855 vec![ 856 "e".to_string(), 857 hex_64('c'), 858 String::new(), 859 "defer".to_string(), 860 ], 861 ]); 862 }); 863 assert_parse_error( 864 wiki_article_from_event(duplicate_defer).unwrap_err(), 865 EventParseError::InvalidTag("a"), 866 ); 867 } 868 869 #[test] 870 fn wiki_article_version_refs_require_adjacent_marked_pairs() { 871 let article_event = event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); 872 let decoded = wiki_article_from_event(article_event).unwrap(); 873 assert_eq!(decoded.data.data.forked_from[0], article_version_ref()); 874 assert_eq!(decoded.data.data.deferred_to, Some(article_version_ref())); 875 876 let mut two_forks = wiki_article(); 877 two_forks.forked_from = vec![ 878 article_version_ref_for('b', "soil-health"), 879 article_version_ref_for('c', "compost"), 880 ]; 881 two_forks.deferred_to = None; 882 let two_forks_event = event_from_parts(wiki_article_to_wire_parts(&two_forks).unwrap()); 883 let decoded = wiki_article_from_event(two_forks_event.clone()).unwrap(); 884 assert_eq!( 885 decoded.data.data.forked_from, 886 vec![ 887 article_version_ref_for('b', "soil-health"), 888 article_version_ref_for('c', "compost") 889 ] 890 ); 891 892 let mut grouped = two_forks_event; 893 let fork_addresses = grouped 894 .tags_as_vec() 895 .iter() 896 .filter(|tag| marked_tag(tag, "a", "fork")) 897 .cloned() 898 .collect::<Vec<_>>(); 899 let fork_events = grouped 900 .tags_as_vec() 901 .iter() 902 .filter(|tag| marked_tag(tag, "e", "fork")) 903 .cloned() 904 .collect::<Vec<_>>(); 905 mutate_tags(&mut grouped, |tags| { 906 tags.retain(|tag| !marked_tag(tag, "a", "fork") && !marked_tag(tag, "e", "fork")); 907 tags.extend(fork_addresses); 908 tags.extend(fork_events); 909 }); 910 assert_parse_error( 911 wiki_article_from_event(grouped).unwrap_err(), 912 EventParseError::InvalidTag("a"), 913 ); 914 915 let mut reversed = event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); 916 let fork_address_index = marked_tag_index(&reversed, "a", "fork"); 917 let fork_event_index = marked_tag_index(&reversed, "e", "fork"); 918 mutate_tags(&mut reversed, |tags| { 919 tags.swap(fork_address_index, fork_event_index); 920 }); 921 assert_parse_error( 922 wiki_article_from_event(reversed).unwrap_err(), 923 EventParseError::InvalidTag("e"), 924 ); 925 926 let mut relay_mismatch = event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); 927 let fork_event_index = marked_tag_index(&relay_mismatch, "e", "fork"); 928 mutate_tags(&mut relay_mismatch, |tags| { 929 tags[fork_event_index][2] = "wss://other.radroots.example".to_string(); 930 }); 931 assert_parse_error( 932 wiki_article_from_event(relay_mismatch).unwrap_err(), 933 EventParseError::InvalidTag("e"), 934 ); 935 936 let mut missing_partner = 937 event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); 938 let fork_event_index = marked_tag_index(&missing_partner, "e", "fork"); 939 mutate_tags(&mut missing_partner, |tags| { 940 tags.remove(fork_event_index); 941 }); 942 assert_parse_error( 943 wiki_article_from_event(missing_partner).unwrap_err(), 944 EventParseError::InvalidTag("a"), 945 ); 946 947 let mut misplaced_marker = 948 event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); 949 let fork_address_index = marked_tag_index(&misplaced_marker, "a", "fork"); 950 mutate_tags(&mut misplaced_marker, |tags| { 951 tags[fork_address_index].insert(2, "fork".to_string()); 952 }); 953 assert_parse_error( 954 wiki_article_from_event(misplaced_marker).unwrap_err(), 955 EventParseError::InvalidTag("a"), 956 ); 957 958 let mut wrong_kind = event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); 959 let fork_address_index = marked_tag_index(&wrong_kind, "a", "fork"); 960 mutate_tags(&mut wrong_kind, |tags| { 961 tags[fork_address_index][1] = format!("30023:{}:soil-health", hex_64('a')); 962 }); 963 assert_parse_error( 964 wiki_article_from_event(wrong_kind).unwrap_err(), 965 EventParseError::InvalidTag("a"), 966 ); 967 } 968 969 #[test] 970 fn wiki_article_codec_accepts_missing_title_tag() { 971 let mut article = wiki_article(); 972 article.title = None; 973 let article_event = event_from_parts(wiki_article_to_wire_parts(&article).unwrap()); 974 assert!( 975 !article_event 976 .tags_as_vec() 977 .iter() 978 .any(|tag| tag.first().map(String::as_str) == Some("title")) 979 ); 980 let decoded = wiki_article_from_event(article_event).unwrap(); 981 assert_eq!(decoded.data.data.title, None); 982 } 983 984 #[test] 985 fn semantic_validation_rejects_invalid_encode_models() { 986 let mut article = wiki_article(); 987 article.content_djot = String::new(); 988 assert_encode_error( 989 wiki_article_to_wire_parts(&article).unwrap_err(), 990 EventEncodeError::EmptyRequiredField("content_djot"), 991 ); 992 993 let mut redirect = WikiRedirect { 994 d_tag: "soil".to_string(), 995 target: address_ref(), 996 }; 997 redirect.target.kind = 30023; 998 assert_encode_error( 999 wiki_redirect_to_wire_parts(&redirect).unwrap_err(), 1000 EventEncodeError::InvalidField("wiki_redirect.target"), 1001 ); 1002 1003 let mut merge = WikiMergeRequest { 1004 target_article: address_ref(), 1005 destination_pubkey: "bad".to_string(), 1006 base_version_event_id: Some(hex_64('e')), 1007 source_version_event_id: hex_64('f'), 1008 explanation: None, 1009 }; 1010 assert_encode_error( 1011 wiki_merge_request_to_wire_parts(&merge).unwrap_err(), 1012 EventEncodeError::InvalidField("destination_pubkey"), 1013 ); 1014 merge.destination_pubkey = hex_64('a'); 1015 merge.base_version_event_id = Some("bad".to_string()); 1016 assert_encode_error( 1017 wiki_merge_request_to_wire_parts(&merge).unwrap_err(), 1018 EventEncodeError::InvalidField("base_version_event_id"), 1019 ); 1020 1021 let mut source = source(); 1022 source.title = " ".to_string(); 1023 assert_encode_error( 1024 knowledge_source_to_wire_parts(&source).unwrap_err(), 1025 EventEncodeError::EmptyRequiredField("title"), 1026 ); 1027 1028 let mut claim = claim(); 1029 claim.citation_spans[0].quote_hash = Some("bad".to_string()); 1030 assert_encode_error( 1031 knowledge_claim_to_wire_parts(&claim).unwrap_err(), 1032 EventEncodeError::InvalidField("citation_spans"), 1033 ); 1034 1035 let mut relation = relation(); 1036 relation.subject.external_id = Some("cover-crops".to_string()); 1037 assert_encode_error( 1038 knowledge_relation_to_wire_parts(&relation).unwrap_err(), 1039 EventEncodeError::InvalidField("subject"), 1040 ); 1041 1042 let mut review = review(); 1043 review.scores[0].value = String::new(); 1044 assert_encode_error( 1045 knowledge_review_to_wire_parts(&review).unwrap_err(), 1046 EventEncodeError::EmptyRequiredField("scores"), 1047 ); 1048 1049 let mut report = field_report(); 1050 report.observations.clear(); 1051 assert_encode_error( 1052 knowledge_field_report_to_wire_parts(&report).unwrap_err(), 1053 EventEncodeError::EmptyRequiredField("observations"), 1054 ); 1055 1056 let bounty = EvidenceBounty { 1057 schema: RADROOTS_EVIDENCE_BOUNTY_SCHEMA.to_string(), 1058 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 1059 d_tag: "soil-bounty".to_string(), 1060 title: "Soil bounty".to_string(), 1061 summary: None, 1062 topics: vec!["soil".to_string()], 1063 target_refs: Vec::new(), 1064 reward_note: None, 1065 closes_at: None, 1066 }; 1067 assert_encode_error( 1068 evidence_bounty_to_wire_parts(&bounty).unwrap_err(), 1069 EventEncodeError::EmptyRequiredField("target_refs"), 1070 ); 1071 1072 let proposal = KnowledgeChangeProposal { 1073 schema: RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA.to_string(), 1074 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 1075 target: event_ref('b', KIND_KNOWLEDGE_CLAIM), 1076 proposal_type: "amend".to_string(), 1077 summary: String::new(), 1078 rationale: None, 1079 evidence_refs: vec![event_ref('c', KIND_KNOWLEDGE_SOURCE)], 1080 supersedes: Vec::new(), 1081 }; 1082 assert_encode_error( 1083 knowledge_change_proposal_to_wire_parts(&proposal).unwrap_err(), 1084 EventEncodeError::EmptyRequiredField("summary"), 1085 ); 1086 1087 let attestation = ContributionAttestation { 1088 schema: RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA.to_string(), 1089 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 1090 contributor_pubkey: hex_64('a'), 1091 contribution_type: "review".to_string(), 1092 subject_refs: Vec::new(), 1093 summary: "Reviewed synthetic claim".to_string(), 1094 evidence_refs: vec![event_ref('e', KIND_KNOWLEDGE_REVIEW)], 1095 }; 1096 assert_encode_error( 1097 contribution_attestation_to_wire_parts(&attestation).unwrap_err(), 1098 EventEncodeError::EmptyRequiredField("subject_refs"), 1099 ); 1100 } 1101 1102 #[test] 1103 fn semantic_validation_rejects_invalid_relay_models() { 1104 let mut source = source(); 1105 source.artifact_refs[0].relays = Some(vec![invalid_relay()]); 1106 assert_encode_error( 1107 knowledge_source_to_wire_parts(&source).unwrap_err(), 1108 EventEncodeError::InvalidField("artifact_refs"), 1109 ); 1110 1111 let mut redirect = WikiRedirect { 1112 d_tag: "soil".to_string(), 1113 target: address_ref(), 1114 }; 1115 redirect.target.relays = vec![invalid_relay()]; 1116 assert_encode_error( 1117 wiki_redirect_to_wire_parts(&redirect).unwrap_err(), 1118 EventEncodeError::InvalidField("wiki_redirect.target"), 1119 ); 1120 1121 let mut review = review(); 1122 review.target.relays = vec![invalid_relay()]; 1123 assert_encode_error( 1124 knowledge_review_to_wire_parts(&review).unwrap_err(), 1125 EventEncodeError::InvalidField("review_target"), 1126 ); 1127 1128 let mut report = field_report(); 1129 report.context.location_precision = KnowledgeLocationPrecision::ExactPrivateReference; 1130 let mut private_location_ref = event_ref('f', KIND_KNOWLEDGE_SOURCE); 1131 private_location_ref.relays = Some(vec![invalid_relay()]); 1132 report.context.private_location_ref = Some(private_location_ref); 1133 assert_encode_error( 1134 knowledge_field_report_to_wire_parts(&report).unwrap_err(), 1135 EventEncodeError::InvalidField("private_location_ref"), 1136 ); 1137 } 1138 1139 #[test] 1140 fn knowledge_claim_encode_enforces_citation_rules() { 1141 let mut model = claim(); 1142 model.citation_spans.clear(); 1143 assert_encode_error( 1144 knowledge_claim_to_wire_parts(&model).unwrap_err(), 1145 EventEncodeError::EmptyRequiredField("citation_spans"), 1146 ); 1147 1148 assert!(knowledge_claim_to_wire_parts(&claim()).is_ok()); 1149 1150 for claim_type in ["hypothesis", "observation", "question"] { 1151 let mut uncited = claim(); 1152 uncited.claim_type = claim_type.to_string(); 1153 uncited.citation_spans.clear(); 1154 assert!(knowledge_claim_to_wire_parts(&uncited).is_ok()); 1155 } 1156 1157 let mut capitalized = claim(); 1158 capitalized.claim_type = "Hypothesis".to_string(); 1159 capitalized.citation_spans.clear(); 1160 assert_encode_error( 1161 knowledge_claim_to_wire_parts(&capitalized).unwrap_err(), 1162 EventEncodeError::EmptyRequiredField("citation_spans"), 1163 ); 1164 } 1165 1166 #[test] 1167 fn semantic_validation_rejects_invalid_decoded_content() { 1168 let mut article = event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); 1169 replace_content(&mut article, String::new()); 1170 assert_parse_error( 1171 wiki_article_from_event(article).unwrap_err(), 1172 EventParseError::InvalidJson("content_djot"), 1173 ); 1174 1175 let mut source_event = event_from_parts(knowledge_source_to_wire_parts(&source()).unwrap()); 1176 let mut value: serde_json::Value = serde_json::from_str(source_event.content()).unwrap(); 1177 value["title"] = serde_json::Value::String(String::new()); 1178 replace_content(&mut source_event, serde_json::to_string(&value).unwrap()); 1179 assert_parse_error( 1180 knowledge_source_from_event(source_event).unwrap_err(), 1181 EventParseError::InvalidJson("title"), 1182 ); 1183 1184 let mut claim_event = event_from_parts(knowledge_claim_to_wire_parts(&claim()).unwrap()); 1185 let mut value: serde_json::Value = serde_json::from_str(claim_event.content()).unwrap(); 1186 value["citation_spans"][0]["quote_hash"] = serde_json::Value::String("bad".to_string()); 1187 replace_content(&mut claim_event, serde_json::to_string(&value).unwrap()); 1188 assert_parse_error( 1189 knowledge_claim_from_event(claim_event).unwrap_err(), 1190 EventParseError::InvalidJson("citation_spans"), 1191 ); 1192 1193 let mut relation_event = 1194 event_from_parts(knowledge_relation_to_wire_parts(&relation()).unwrap()); 1195 let mut value: serde_json::Value = serde_json::from_str(relation_event.content()).unwrap(); 1196 value["subject"]["external_id"] = serde_json::Value::String("cover-crops".to_string()); 1197 replace_content(&mut relation_event, serde_json::to_string(&value).unwrap()); 1198 assert_parse_error( 1199 knowledge_relation_from_event(relation_event).unwrap_err(), 1200 EventParseError::InvalidJson("subject"), 1201 ); 1202 1203 let mut review_event = event_from_parts(knowledge_review_to_wire_parts(&review()).unwrap()); 1204 let mut value: serde_json::Value = serde_json::from_str(review_event.content()).unwrap(); 1205 value["target"]["kind"] = serde_json::Value::from(0); 1206 replace_content(&mut review_event, serde_json::to_string(&value).unwrap()); 1207 assert_parse_error( 1208 knowledge_review_from_event(review_event).unwrap_err(), 1209 EventParseError::InvalidJson("review_target"), 1210 ); 1211 1212 let mut report_event = 1213 event_from_parts(knowledge_field_report_to_wire_parts(&field_report()).unwrap()); 1214 let mut value: serde_json::Value = serde_json::from_str(report_event.content()).unwrap(); 1215 value["observations"] = serde_json::Value::Array(Vec::new()); 1216 replace_content(&mut report_event, serde_json::to_string(&value).unwrap()); 1217 assert_parse_error( 1218 knowledge_field_report_from_event(report_event).unwrap_err(), 1219 EventParseError::InvalidJson("observations"), 1220 ); 1221 1222 let bounty = EvidenceBounty { 1223 schema: RADROOTS_EVIDENCE_BOUNTY_SCHEMA.to_string(), 1224 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 1225 d_tag: "soil-bounty".to_string(), 1226 title: "Soil bounty".to_string(), 1227 summary: None, 1228 topics: vec!["soil".to_string()], 1229 target_refs: vec![event_ref('a', KIND_KNOWLEDGE_CLAIM)], 1230 reward_note: None, 1231 closes_at: None, 1232 }; 1233 let mut bounty_event = event_from_parts(evidence_bounty_to_wire_parts(&bounty).unwrap()); 1234 let mut value: serde_json::Value = serde_json::from_str(bounty_event.content()).unwrap(); 1235 value["target_refs"] = serde_json::Value::Array(Vec::new()); 1236 replace_content(&mut bounty_event, serde_json::to_string(&value).unwrap()); 1237 assert_parse_error( 1238 evidence_bounty_from_event(bounty_event).unwrap_err(), 1239 EventParseError::InvalidJson("target_refs"), 1240 ); 1241 1242 let proposal = KnowledgeChangeProposal { 1243 schema: RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA.to_string(), 1244 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 1245 target: event_ref('b', KIND_KNOWLEDGE_CLAIM), 1246 proposal_type: "amend".to_string(), 1247 summary: "Clarify scope".to_string(), 1248 rationale: None, 1249 evidence_refs: vec![event_ref('c', KIND_KNOWLEDGE_SOURCE)], 1250 supersedes: Vec::new(), 1251 }; 1252 let mut proposal_event = 1253 event_from_parts(knowledge_change_proposal_to_wire_parts(&proposal).unwrap()); 1254 let mut value: serde_json::Value = serde_json::from_str(proposal_event.content()).unwrap(); 1255 value["summary"] = serde_json::Value::String(String::new()); 1256 replace_content(&mut proposal_event, serde_json::to_string(&value).unwrap()); 1257 assert_parse_error( 1258 knowledge_change_proposal_from_event(proposal_event).unwrap_err(), 1259 EventParseError::InvalidJson("summary"), 1260 ); 1261 1262 let attestation = ContributionAttestation { 1263 schema: RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA.to_string(), 1264 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 1265 contributor_pubkey: hex_64('a'), 1266 contribution_type: "review".to_string(), 1267 subject_refs: vec![event_ref('d', KIND_KNOWLEDGE_REVIEW)], 1268 summary: "Reviewed synthetic claim".to_string(), 1269 evidence_refs: vec![event_ref('e', KIND_KNOWLEDGE_REVIEW)], 1270 }; 1271 let mut attestation_event = 1272 event_from_parts(contribution_attestation_to_wire_parts(&attestation).unwrap()); 1273 let mut value: serde_json::Value = serde_json::from_str(attestation_event.content()).unwrap(); 1274 value["subject_refs"] = serde_json::Value::Array(Vec::new()); 1275 replace_content( 1276 &mut attestation_event, 1277 serde_json::to_string(&value).unwrap(), 1278 ); 1279 assert_parse_error( 1280 contribution_attestation_from_event(attestation_event).unwrap_err(), 1281 EventParseError::InvalidJson("subject_refs"), 1282 ); 1283 } 1284 1285 #[test] 1286 fn knowledge_decode_rejects_invalid_relay_values() { 1287 let mut article_event = event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); 1288 mutate_tags(&mut article_event, |tags| { 1289 let source_tag = tags 1290 .iter_mut() 1291 .find(|tag| tag.first().map(String::as_str) == Some("source")) 1292 .expect("source tag"); 1293 source_tag[5] = invalid_relay(); 1294 }); 1295 assert_parse_error( 1296 wiki_article_from_event(article_event).unwrap_err(), 1297 EventParseError::InvalidTag("source"), 1298 ); 1299 1300 let mut redirect_event = event_from_parts( 1301 wiki_redirect_to_wire_parts(&WikiRedirect { 1302 d_tag: "soil".to_string(), 1303 target: address_ref(), 1304 }) 1305 .unwrap(), 1306 ); 1307 mutate_tags(&mut redirect_event, |tags| { 1308 let target_tag = tags 1309 .iter_mut() 1310 .find(|tag| tag.first().map(String::as_str) == Some("a")) 1311 .expect("target tag"); 1312 target_tag[2] = invalid_relay(); 1313 }); 1314 assert_parse_error( 1315 wiki_redirect_from_event(redirect_event).unwrap_err(), 1316 EventParseError::InvalidTag("a"), 1317 ); 1318 1319 let mut source_event = event_from_parts(knowledge_source_to_wire_parts(&source()).unwrap()); 1320 let mut value: serde_json::Value = serde_json::from_str(source_event.content()).unwrap(); 1321 value["artifact_refs"][0]["relays"] = 1322 serde_json::Value::Array(vec![serde_json::Value::String(invalid_relay())]); 1323 replace_content(&mut source_event, serde_json::to_string(&value).unwrap()); 1324 assert_parse_error( 1325 knowledge_source_from_event(source_event).unwrap_err(), 1326 EventParseError::InvalidJson("artifact_refs"), 1327 ); 1328 1329 let mut review_event = event_from_parts(knowledge_review_to_wire_parts(&review()).unwrap()); 1330 let mut value: serde_json::Value = serde_json::from_str(review_event.content()).unwrap(); 1331 value["target"]["relays"] = 1332 serde_json::Value::Array(vec![serde_json::Value::String(invalid_relay())]); 1333 replace_content(&mut review_event, serde_json::to_string(&value).unwrap()); 1334 assert_parse_error( 1335 knowledge_review_from_event(review_event).unwrap_err(), 1336 EventParseError::InvalidJson("review_target"), 1337 ); 1338 1339 let mut report = field_report(); 1340 report.context.location_precision = KnowledgeLocationPrecision::ExactPrivateReference; 1341 report.context.private_location_ref = Some(event_ref('f', KIND_KNOWLEDGE_SOURCE)); 1342 let mut report_event = event_from_parts(knowledge_field_report_to_wire_parts(&report).unwrap()); 1343 let mut value: serde_json::Value = serde_json::from_str(report_event.content()).unwrap(); 1344 value["context"]["private_location_ref"]["relays"] = 1345 serde_json::Value::Array(vec![serde_json::Value::String(invalid_relay())]); 1346 replace_content(&mut report_event, serde_json::to_string(&value).unwrap()); 1347 assert_parse_error( 1348 knowledge_field_report_from_event(report_event).unwrap_err(), 1349 EventParseError::InvalidJson("private_location_ref"), 1350 ); 1351 } 1352 1353 #[test] 1354 fn knowledge_claim_decode_enforces_citation_rules() { 1355 let mut claim_event = event_from_parts(knowledge_claim_to_wire_parts(&claim()).unwrap()); 1356 let mut value: serde_json::Value = serde_json::from_str(claim_event.content()).unwrap(); 1357 value["citation_spans"] = serde_json::Value::Array(Vec::new()); 1358 replace_content(&mut claim_event, serde_json::to_string(&value).unwrap()); 1359 assert_parse_error( 1360 knowledge_claim_from_event(claim_event).unwrap_err(), 1361 EventParseError::InvalidJson("citation_spans"), 1362 ); 1363 1364 for claim_type in ["hypothesis", "observation", "question"] { 1365 let mut uncited = claim(); 1366 uncited.claim_type = claim_type.to_string(); 1367 uncited.citation_spans.clear(); 1368 let decoded = knowledge_claim_from_event(event_from_parts( 1369 knowledge_claim_to_wire_parts(&uncited).unwrap(), 1370 )) 1371 .unwrap(); 1372 assert_eq!(decoded.data.data.claim_type, claim_type); 1373 assert!(decoded.data.data.citation_spans.is_empty()); 1374 } 1375 }