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:
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, ""),