lib

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

commit 6683d409eedb11cd97f8a11f8f9aa50847b49bd1
parent ab97dd2440d4e08d7ad213225c88dd84eeb05cb6
Author: triesap <tyson@radroots.org>
Date:   Sun, 21 Jun 2026 21:29:44 +0000

events-codec: cover social workspace branches

- Cover absent social article metadata, post relay/thumb branches, report relay filtering, generic repost optional targets, farm workspace decode and encode edges, and farm CRDT optional metadata.

- Validate focused article/post/report/repost/farm_workspace/farm_crdt tests, full radroots_events_codec tests, crate check, diff check, refreshed coverage run, and radroots_events_codec policy gate.

Diffstat:
Mcrates/events_codec/tests/article.rs | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/events_codec/tests/farm_crdt.rs | 45+++++++++++++++++++++++++++++++++++++++++++++
Acrates/events_codec/tests/farm_workspace.rs | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/events_codec/tests/post.rs | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/events_codec/tests/report.rs | 32++++++++++++++++++++++++++++++++
Mcrates/events_codec/tests/repost.rs | 34++++++++++++++++++++++++++++++++--
6 files changed, 376 insertions(+), 2 deletions(-)

diff --git a/crates/events_codec/tests/article.rs b/crates/events_codec/tests/article.rs @@ -133,6 +133,57 @@ fn article_codec_requires_kind_required_fields_and_valid_d_tag() { } #[test] +fn article_decode_handles_minimal_and_invalid_optional_tags() { + let tags = vec![ + vec![TAG_D.to_string(), VALID_D_TAG.to_string()], + vec![TAG_TITLE.to_string(), "Minimal article".to_string()], + ]; + let decoded = article_from_event(KIND_ARTICLE, &tags, "Body").unwrap(); + assert_eq!(decoded.d_tag, VALID_D_TAG); + assert_eq!(decoded.title, "Minimal article"); + assert!(decoded.farm.is_none()); + assert_eq!(decoded.topics, None); + assert_eq!(decoded.published_at, None); + + let mut tags = tags.clone(); + tags.push(vec![TAG_PUBLISHED_AT.to_string(), "not-a-time".to_string()]); + assert!(matches!( + article_from_event(KIND_ARTICLE, &tags, "Body"), + Err(EventParseError::InvalidNumber(TAG_PUBLISHED_AT, _)) + )); + + assert!(matches!( + article_from_event(KIND_ARTICLE, &tags, " "), + Err(EventParseError::InvalidTag("content")) + )); +} + +#[test] +fn article_build_tags_handles_absent_optional_metadata() { + let article = RadrootsArticle { + d_tag: VALID_D_TAG.to_string(), + title: "Minimal article".to_string(), + content: "Body".to_string(), + summary: None, + image: None, + published_at: None, + farm: None, + location: None, + topics: None, + }; + + let tags = article_build_tags(&article).unwrap(); + assert!(has_tag(&tags, TAG_D, VALID_D_TAG)); + assert!(has_tag(&tags, TAG_TITLE, "Minimal article")); + assert!(!tags.iter().any(|tag| { + matches!( + tag.first().map(String::as_str), + Some(TAG_PUBLISHED_AT | TAG_A | TAG_LOCATION | TAG_G | TAG_T) + ) + })); +} + +#[test] fn article_wrappers_preserve_event_metadata() { let article = sample_article(); let parts = to_wire_parts(&article).unwrap(); diff --git a/crates/events_codec/tests/farm_crdt.rs b/crates/events_codec/tests/farm_crdt.rs @@ -0,0 +1,45 @@ +#![cfg(feature = "serde_json")] + +use radroots_events::{ + farm_crdt::{ + RADROOTS_FARM_CRDT_CHANGE_SCHEMA, RadrootsCrdtBackend, RadrootsFarmCrdtChange, + RadrootsFarmCrdtDocumentKind, RadrootsFarmSemanticKind, + }, + farm_workspace::RadrootsFarmWorkspaceRef, +}; +use radroots_events_codec::farm_crdt::encode::to_wire_parts; + +const WORKSPACE_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA"; +const DOCUMENT_ID: &str = "AAAAAAAAAAAAAAAAAAAAAg"; + +#[test] +fn farm_crdt_change_encodes_without_optional_metadata() { + let change = RadrootsFarmCrdtChange { + schema: RADROOTS_FARM_CRDT_CHANGE_SCHEMA.to_string(), + workspace: RadrootsFarmWorkspaceRef { + pubkey: "workspace_pubkey".to_string(), + d_tag: WORKSPACE_D_TAG.to_string(), + }, + farm_group_id: "field-group".to_string(), + document_id: DOCUMENT_ID.to_string(), + document_kind: RadrootsFarmCrdtDocumentKind::FarmTask, + crdt_backend: RadrootsCrdtBackend::Automerge, + crdt_backend_version: None, + actor_id: "actor_abc".to_string(), + change_hash: "crdt_hash_abc".to_string(), + dependencies: Vec::new(), + encoded_change: "abc-DEF_012".to_string(), + semantic_kind: RadrootsFarmSemanticKind::FarmTaskCreate, + business_time_ms: 1_780_000_000_000, + author_member_id: None, + app_version: None, + }; + + let parts = to_wire_parts(&change).unwrap(); + assert!(parts.content.contains("\"actor_id\":\"actor_abc\"")); + assert!(parts.tags.iter().any(|tag| { + tag.first().map(String::as_str) == Some("a") + && tag.get(1).map(String::as_str) + == Some("30078:workspace_pubkey:AAAAAAAAAAAAAAAAAAAAAA") + })); +} diff --git a/crates/events_codec/tests/farm_workspace.rs b/crates/events_codec/tests/farm_workspace.rs @@ -0,0 +1,153 @@ +#![cfg(feature = "serde_json")] + +use radroots_events::{ + farm::RadrootsFarmRef, + farm_crdt::KIND_FARM_CRDT_CHANGE, + farm_workspace::{ + KIND_FARM_WORKSPACE_MANIFEST, RADROOTS_FARM_WORKSPACE_PROTOCOL_VERSION, + RADROOTS_FARM_WORKSPACE_SCHEMA, RadrootsFarmWorkspaceManifest, + RadrootsFarmWorkspaceMediaServer, RadrootsFarmWorkspaceRelay, + RadrootsFarmWorkspaceRelayMode, + }, + kinds::{KIND_FARM, KIND_FARM_FILE_METADATA}, + tags::{TAG_A, TAG_H, TAG_P}, +}; +use radroots_events_codec::{ + error::{EventEncodeError, EventParseError}, + farm_workspace::decode::farm_workspace_from_event, + farm_workspace::encode::to_wire_parts, +}; + +const D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA"; +const FARM_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAQ"; +const OTHER_FARM_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAg"; +const GROUP_ID: &str = "field-group"; + +#[test] +fn farm_workspace_decode_handles_optional_and_mismatch_edges() { + let manifest = sample_manifest(); + let parts = to_wire_parts(&manifest).unwrap(); + + let mut mismatched_group = manifest.clone(); + mismatched_group.farm_group_id = "other-group".to_string(); + let content = serde_json::to_string(&mismatched_group).unwrap(); + assert!(matches!( + farm_workspace_from_event(parts.kind, &parts.tags, &content), + Err(EventParseError::InvalidTag(TAG_H)) + )); + + let mut without_owner = parts.tags.clone(); + without_owner.retain(|tag| tag.first().map(String::as_str) != Some(TAG_P)); + let decoded = farm_workspace_from_event(parts.kind, &without_owner, &parts.content).unwrap(); + assert_eq!(decoded.owner_pubkey, "workspace_owner_pubkey"); + + let mut without_farm_address = parts.tags.clone(); + without_farm_address.retain(|tag| tag.first().map(String::as_str) != Some(TAG_A)); + let decoded = + farm_workspace_from_event(parts.kind, &without_farm_address, &parts.content).unwrap(); + assert_eq!( + decoded.farm.as_ref().map(|farm| farm.d_tag.as_str()), + Some(FARM_D_TAG) + ); + + let mut mismatched_farm_address = parts.tags.clone(); + replace_first_tag( + &mut mismatched_farm_address, + TAG_A, + vec![ + TAG_A.to_string(), + format!("{KIND_FARM}:farm_pubkey:{OTHER_FARM_D_TAG}"), + ], + ); + assert!(matches!( + farm_workspace_from_event(parts.kind, &mismatched_farm_address, &parts.content), + Err(EventParseError::InvalidTag(TAG_A)) + )); + + let mut mismatched_farm_pubkey = parts.tags.clone(); + replace_first_tag( + &mut mismatched_farm_pubkey, + TAG_A, + vec![ + TAG_A.to_string(), + format!("{KIND_FARM}:other_farm:{FARM_D_TAG}"), + ], + ); + assert!(matches!( + farm_workspace_from_event(parts.kind, &mismatched_farm_pubkey, &parts.content), + Err(EventParseError::InvalidTag(TAG_A)) + )); + + for supported_kinds in [ + vec![KIND_FARM_WORKSPACE_MANIFEST], + vec![KIND_FARM_CRDT_CHANGE], + ] { + let mut unsupported = manifest.clone(); + unsupported.supported_kinds = supported_kinds; + let content = serde_json::to_string(&unsupported).unwrap(); + assert!(matches!( + farm_workspace_from_event(parts.kind, &parts.tags, &content), + Err(EventParseError::InvalidJson("supported_kinds")) + )); + } +} + +#[test] +fn farm_workspace_encode_rejects_schema_and_supported_kind_edges() { + let mut bad_schema = sample_manifest(); + bad_schema.schema = "radroots.farm.workspace.invalid".to_string(); + assert!(matches!( + to_wire_parts(&bad_schema), + Err(EventEncodeError::InvalidField("schema")) + )); + + for supported_kinds in [ + vec![KIND_FARM_WORKSPACE_MANIFEST], + vec![KIND_FARM_CRDT_CHANGE], + ] { + let mut unsupported = sample_manifest(); + unsupported.supported_kinds = supported_kinds; + assert!(matches!( + to_wire_parts(&unsupported), + Err(EventEncodeError::InvalidField("supported_kinds")) + )); + } +} + +fn sample_manifest() -> RadrootsFarmWorkspaceManifest { + RadrootsFarmWorkspaceManifest { + d_tag: D_TAG.to_string(), + schema: RADROOTS_FARM_WORKSPACE_SCHEMA.to_string(), + farm_group_id: GROUP_ID.to_string(), + name: "Small Regen Farm".to_string(), + owner_pubkey: "workspace_owner_pubkey".to_string(), + farm: Some(RadrootsFarmRef { + pubkey: "farm_pubkey".to_string(), + d_tag: FARM_D_TAG.to_string(), + }), + relays: vec![RadrootsFarmWorkspaceRelay { + url: "wss://relay.example.invalid/farm/field-group".to_string(), + mode: RadrootsFarmWorkspaceRelayMode::ReadWrite, + }], + media_servers: vec![RadrootsFarmWorkspaceMediaServer { + url: "https://media.example.invalid/farm/field-group".to_string(), + service: "RadrootsPrivateMedia".to_string(), + }], + supported_kinds: vec![ + KIND_FARM_CRDT_CHANGE, + KIND_FARM_WORKSPACE_MANIFEST, + KIND_FARM_FILE_METADATA, + ], + protocol_version: RADROOTS_FARM_WORKSPACE_PROTOCOL_VERSION.to_string(), + created_at_ms: 1_780_000_000_000, + updated_at_ms: None, + } +} + +fn replace_first_tag(tags: &mut [Vec<String>], name: &str, replacement: Vec<String>) { + let tag = tags + .iter_mut() + .find(|tag| tag.first().map(String::as_str) == Some(name)) + .expect("tag"); + *tag = replacement; +} diff --git a/crates/events_codec/tests/post.rs b/crates/events_codec/tests/post.rs @@ -271,6 +271,69 @@ fn post_build_tags_covers_optional_social_encode_branches() { tag.first().map(|value| value.as_str()) == Some(TAG_IMETA) && tag.iter().any(|value| value == "dim 120x80") })); + + let mut no_relay_post = content_post(); + no_relay_post.farm = Some(RadrootsSocialFarmAnchor { + farm: RadrootsFarmRef { + pubkey: "farm_pubkey".to_string(), + d_tag: FARM_D_TAG.to_string(), + }, + relays: None, + }); + no_relay_post.address_refs = Some(vec![RadrootsSocialTarget::Address { + address: format!("30023:article_author:{ARTICLE_D_TAG}"), + author: None, + event_kind: None, + relays: None, + }]); + no_relay_post.quote_refs = Some(vec![RadrootsSocialTarget::Event { + id: QUOTE_ID.to_string(), + author: None, + event_kind: None, + relays: None, + }]); + no_relay_post.media = Some(vec![RadrootsSocialMediaMetadata { + thumbnails: Some(vec![RadrootsSocialMediaThumbnail { + url: "https://media.example.test/thumb-no-dim.jpg".to_string(), + dimensions: None, + }]), + ..RadrootsSocialMediaMetadata::default() + }]); + + let tags = post_build_tags(&no_relay_post).unwrap(); + let farm_tag = tags + .iter() + .find(|tag| { + tag.first().map(String::as_str) == Some(TAG_A) + && tag.get(1).map(String::as_str) + == Some("30340:farm_pubkey:AAAAAAAAAAAAAAAAAAAAAA") + }) + .expect("farm tag"); + assert_eq!(farm_tag.len(), 2); + let address_tag = tags + .iter() + .find(|tag| { + tag.first().map(String::as_str) == Some(TAG_A) + && tag.get(1).map(String::as_str) + == Some("30023:article_author:BBBBBBBBBBBBBBBBBBBBBA") + }) + .expect("address tag"); + assert_eq!(address_tag.len(), 2); + let quote_tag = tags + .iter() + .find(|tag| tag.first().map(String::as_str) == Some(TAG_Q)) + .expect("quote tag"); + assert_eq!(quote_tag.len(), 2); + let imeta = tags + .iter() + .find(|tag| tag.first().map(String::as_str) == Some(TAG_IMETA)) + .expect("imeta tag"); + assert!( + imeta + .iter() + .any(|value| value == "thumb https://media.example.test/thumb-no-dim.jpg") + ); + assert!(!imeta.iter().any(|value| value.starts_with("dim "))); } #[test] diff --git a/crates/events_codec/tests/report.rs b/crates/events_codec/tests/report.rs @@ -373,6 +373,38 @@ fn report_codec_covers_report_type_and_file_variants() { Some("magnet:?xt=urn:btih:example") ); + let mut report = event_report(); + if let Some(RadrootsSocialTarget::Event { relays, .. }) = &mut report.event { + *relays = None; + } + let parts = to_wire_parts(&report).unwrap(); + let event = parts + .tags + .iter() + .find(|tag| tag.first().map(String::as_str) == Some(TAG_E)) + .expect("event tag"); + assert_eq!(event.len(), 3); + + let mut report = address_report(); + if let Some(RadrootsSocialTarget::Address { relays, .. }) = &mut report.event { + *relays = Some(vec![ + " ".to_string(), + "wss://relay.example.test".to_string(), + ]); + } + let parts = to_wire_parts(&report).unwrap(); + let address = parts + .tags + .iter() + .find(|tag| tag.first().map(String::as_str) == Some(TAG_A)) + .expect("address tag"); + assert!( + address + .iter() + .any(|value| value == "wss://relay.example.test") + ); + assert!(!address.iter().any(|value| value == " ")); + let mut tags = report_build_tags(&file_report()).unwrap(); let sha = tags .iter_mut() diff --git a/crates/events_codec/tests/repost.rs b/crates/events_codec/tests/repost.rs @@ -262,6 +262,36 @@ fn generic_repost_codecs_cover_event_targets_and_error_edges() { } )); + let no_author_event = RadrootsGenericRepost { + target: RadrootsSocialTarget::Event { + id: EVENT_ID.to_string(), + author: None, + event_kind: Some(KIND_REACTION), + relays: None, + }, + target_kind: KIND_REACTION, + content: None, + }; + let parts = generic_repost_to_wire_parts(&no_author_event).unwrap(); + assert!(has_tag(&parts.tags, TAG_E, EVENT_ID)); + assert!(!parts.tags.iter().any(|tag| { + tag.first().map(String::as_str) == Some(TAG_P) + || tag.get(2).map(|value| !value.is_empty()).unwrap_or(false) + })); + + let mut generic = generic_article_repost(); + if let RadrootsSocialTarget::Address { author, relays, .. } = &mut generic.target { + *author = None; + *relays = None; + } + let parts = generic_repost_to_wire_parts(&generic).unwrap(); + let address = parts + .tags + .iter() + .find(|tag| tag.first().map(String::as_str) == Some(TAG_A)) + .expect("address tag"); + assert_eq!(address.len(), 2); + let wrong_kind = generic_repost_from_event(KIND_REPOST, &parts.tags, "").unwrap_err(); assert!(matches!( wrong_kind, @@ -302,7 +332,7 @@ fn generic_repost_codecs_cover_event_targets_and_error_edges() { Err(EventParseError::InvalidTag(TAG_A)) )); - let mut tags = generic_repost_build_tags(&generic).unwrap(); + let mut tags = generic_repost_build_tags(&no_author_event).unwrap(); let event_tag = tags .iter_mut() .find(|tag| tag.first().map(String::as_str) == Some(TAG_E)) @@ -313,7 +343,7 @@ fn generic_repost_codecs_cover_event_targets_and_error_edges() { Err(EventParseError::InvalidTag(TAG_E)) )); - let mut tags = generic_repost_build_tags(&generic).unwrap(); + let mut tags = generic_repost_build_tags(&no_author_event).unwrap(); replace_tag_value(&mut tags, TAG_E, "not-a-lowercase-hex-id"); assert!(matches!( generic_repost_from_event(KIND_GENERIC_REPOST, &tags, ""),