commit 617f7b0d8c141e3c7b8a1db20fb3248f945b72a3
parent 26a5c9cfa6c231a0ddbdbb9816d2102da8fe32dc
Author: triesap <tyson@radroots.org>
Date: Sat, 21 Feb 2026 22:52:16 +0000
tests: cover tag builders and job trait error paths
- add a comprehensive tag_builders suite that executes every radrootseventtagbuilder impl and listing_build_tags
- expand job trait adapter tests to cover request/result/feedback metadata and index conversions
- add job parse error and profile encode error display and source assertions in codec error tests
- validate with cargo check, cargo test, and xtask strict preflight showing additional coverage lift
Diffstat:
3 files changed, 646 insertions(+), 5 deletions(-)
diff --git a/crates/events-codec/tests/codec_error_job.rs b/crates/events-codec/tests/codec_error_job.rs
@@ -5,6 +5,8 @@ use radroots_events_codec::job::encode::{
assert_no_inputs_when_encrypted, push_provider_tag, push_relay_tag, push_status_tag,
JobEncodeError,
};
+use radroots_events_codec::job::error::JobParseError;
+use radroots_events_codec::profile::error::ProfileEncodeError;
#[cfg(feature = "serde_json")]
use serde::ser::{Error as _, Serializer};
#[cfg(feature = "serde_json")]
@@ -133,3 +135,44 @@ fn job_encode_error_display_covers_variants() {
"empty required field: content"
);
}
+
+#[test]
+fn job_parse_error_display_and_source_covers_variants() {
+ let missing = JobParseError::MissingTag("e");
+ assert_eq!(missing.to_string(), "missing tag: e");
+ assert!(missing.source().is_none());
+
+ let invalid = JobParseError::InvalidTag("status");
+ assert_eq!(invalid.to_string(), "invalid tag structure for 'status'");
+ assert!(invalid.source().is_none());
+
+ let invalid_number = JobParseError::InvalidNumber("amount", "x".parse::<u32>().unwrap_err());
+ assert!(invalid_number
+ .to_string()
+ .contains("invalid number in 'amount'"));
+ assert!(invalid_number.source().is_some());
+
+ let non_whole = JobParseError::NonWholeSats("amount");
+ assert!(non_whole.to_string().contains("whole number of sats"));
+ assert!(non_whole.source().is_none());
+
+ let overflow = JobParseError::AmountOverflow("amount");
+ assert!(overflow.to_string().contains("does not fit u32 sat"));
+ assert!(overflow.source().is_none());
+
+ let missing_chain = JobParseError::MissingChainTag("e");
+ assert_eq!(missing_chain.to_string(), "missing required chain tag: e");
+ assert!(missing_chain.source().is_none());
+}
+
+#[test]
+fn profile_encode_error_display_covers_variants() {
+ let invalid = ProfileEncodeError::InvalidUrl("website", "ftp://example.com".to_string());
+ assert_eq!(
+ invalid.to_string(),
+ "invalid URL for website: ftp://example.com"
+ );
+
+ let json = ProfileEncodeError::Json;
+ assert_eq!(json.to_string(), "failed to serialize metadata JSON");
+}
diff --git a/crates/events-codec/tests/job_traits.rs b/crates/events-codec/tests/job_traits.rs
@@ -1,8 +1,12 @@
-use radroots_events::RadrootsNostrEvent;
-use radroots_events::job::JobInputType;
+use radroots_events::job::{JobFeedbackStatus, JobInputType, JobPaymentRequest};
+use radroots_events::job_feedback::RadrootsJobFeedback;
use radroots_events::job_request::{RadrootsJobInput, RadrootsJobParam, RadrootsJobRequest};
-use radroots_events::kinds::KIND_JOB_REQUEST_MIN;
-use radroots_events_codec::job::request::encode::to_wire_parts;
+use radroots_events::job_result::RadrootsJobResult;
+use radroots_events::kinds::{KIND_JOB_FEEDBACK, KIND_JOB_REQUEST_MIN, KIND_JOB_RESULT_MIN};
+use radroots_events::RadrootsNostrEvent;
+use radroots_events_codec::job::feedback::encode::to_wire_parts as to_feedback_wire_parts;
+use radroots_events_codec::job::request::encode::to_wire_parts as to_request_wire_parts;
+use radroots_events_codec::job::result::encode::to_wire_parts as to_result_wire_parts;
use radroots_events_codec::job::traits::{BorrowedEventAdapter, JobEventLike};
fn sample_request() -> RadrootsJobRequest {
@@ -30,7 +34,7 @@ fn sample_request() -> RadrootsJobRequest {
#[test]
fn borrowed_event_adapter_builds_request_metadata() {
let req = sample_request();
- let parts = to_wire_parts(&req, "payload").unwrap();
+ let parts = to_request_wire_parts(&req, "payload").unwrap();
let event = RadrootsNostrEvent {
id: "id".to_string(),
@@ -51,3 +55,148 @@ fn borrowed_event_adapter_builds_request_metadata() {
assert_eq!(metadata.kind, event.kind);
assert_eq!(metadata.job_request, req);
}
+
+fn sample_result() -> RadrootsJobResult {
+ RadrootsJobResult {
+ kind: (KIND_JOB_RESULT_MIN + 1) as u16,
+ request_event: radroots_events::RadrootsNostrEventPtr {
+ id: "req".to_string(),
+ relays: Some("wss://relay.example.com".to_string()),
+ },
+ request_json: Some("{\"foo\":\"bar\"}".to_string()),
+ inputs: vec![RadrootsJobInput {
+ data: "hello".to_string(),
+ input_type: JobInputType::Text,
+ relay: None,
+ marker: None,
+ }],
+ customer_pubkey: Some(
+ "58e318557257f2ab58a415d21bb57082b4824cf667a1d64e72bcbc5acc018c62".to_string(),
+ ),
+ payment: Some(JobPaymentRequest {
+ amount_sat: 1,
+ bolt11: None,
+ }),
+ content: Some("payload".to_string()),
+ encrypted: false,
+ }
+}
+
+fn sample_feedback() -> RadrootsJobFeedback {
+ RadrootsJobFeedback {
+ kind: KIND_JOB_FEEDBACK as u16,
+ status: JobFeedbackStatus::Processing,
+ extra_info: Some("processing".to_string()),
+ request_event: radroots_events::RadrootsNostrEventPtr {
+ id: "req".to_string(),
+ relays: Some("wss://relay.example.com".to_string()),
+ },
+ customer_pubkey: Some(
+ "58e318557257f2ab58a415d21bb57082b4824cf667a1d64e72bcbc5acc018c62".to_string(),
+ ),
+ payment: Some(JobPaymentRequest {
+ amount_sat: 2,
+ bolt11: None,
+ }),
+ content: Some("payload".to_string()),
+ encrypted: false,
+ }
+}
+
+#[test]
+fn borrowed_event_adapter_builds_request_metadata_and_index() {
+ let req = sample_request();
+ let parts = to_request_wire_parts(&req, "payload").unwrap();
+ let event = RadrootsNostrEvent {
+ id: "id".to_string(),
+ author: "author".to_string(),
+ created_at: 42,
+ kind: parts.kind,
+ tags: parts.tags,
+ content: "payload".to_string(),
+ sig: "sig".to_string(),
+ };
+
+ let adapter = BorrowedEventAdapter::new(&event, event.created_at, &event.tags, &event.sig);
+ assert_eq!(adapter.raw_id(), "id");
+ assert_eq!(adapter.raw_author(), "author");
+ assert_eq!(adapter.raw_published_at(), 42);
+ assert_eq!(adapter.raw_kind(), event.kind);
+ assert_eq!(adapter.raw_content(), "payload");
+ assert_eq!(adapter.raw_tags().len(), event.tags.len());
+ assert_eq!(adapter.raw_sig(), "sig");
+
+ let index = adapter.to_job_request_event_index().unwrap();
+ assert_eq!(index.event.id, event.id);
+ assert_eq!(index.event.author, event.author);
+ assert_eq!(index.event.created_at, event.created_at);
+ assert_eq!(index.event.kind, event.kind);
+ assert_eq!(index.event.content, event.content);
+ assert_eq!(index.event.sig, event.sig);
+}
+
+#[test]
+fn borrowed_event_adapter_builds_result_metadata_and_index() {
+ let result = sample_result();
+ let parts = to_result_wire_parts(&result, "payload").unwrap();
+ let event = RadrootsNostrEvent {
+ id: "id".to_string(),
+ author: "author".to_string(),
+ created_at: 42,
+ kind: parts.kind,
+ tags: parts.tags,
+ content: "payload".to_string(),
+ sig: "sig".to_string(),
+ };
+
+ let adapter = BorrowedEventAdapter::new(&event, event.created_at, &event.tags, &event.sig);
+ let metadata = adapter.to_job_result_metadata().unwrap();
+ assert_eq!(metadata.id, event.id);
+ assert_eq!(metadata.author, event.author);
+ assert_eq!(metadata.published_at, event.created_at);
+ assert_eq!(metadata.kind, event.kind);
+ assert_eq!(metadata.job_result.kind, result.kind);
+ assert_eq!(metadata.job_result.request_event.id, "req");
+ assert_eq!(metadata.job_result.content.as_deref(), Some("payload"));
+
+ let index = adapter.to_job_result_event_index().unwrap();
+ assert_eq!(index.event.id, event.id);
+ assert_eq!(index.event.author, event.author);
+ assert_eq!(index.event.created_at, event.created_at);
+ assert_eq!(index.event.kind, event.kind);
+ assert_eq!(index.event.content, event.content);
+ assert_eq!(index.event.sig, event.sig);
+}
+
+#[test]
+fn borrowed_event_adapter_builds_feedback_metadata_and_index() {
+ let feedback = sample_feedback();
+ let parts = to_feedback_wire_parts(&feedback, "payload").unwrap();
+ let event = RadrootsNostrEvent {
+ id: "id".to_string(),
+ author: "author".to_string(),
+ created_at: 42,
+ kind: parts.kind,
+ tags: parts.tags,
+ content: "payload".to_string(),
+ sig: "sig".to_string(),
+ };
+
+ let adapter = BorrowedEventAdapter::new(&event, event.created_at, &event.tags, &event.sig);
+ let metadata = adapter.to_job_feedback_metadata().unwrap();
+ assert_eq!(metadata.id, event.id);
+ assert_eq!(metadata.author, event.author);
+ assert_eq!(metadata.published_at, event.created_at);
+ assert_eq!(metadata.kind, event.kind);
+ assert_eq!(metadata.job_feedback.kind, feedback.kind);
+ assert_eq!(metadata.job_feedback.request_event.id, "req");
+ assert_eq!(metadata.job_feedback.content.as_deref(), Some("payload"));
+
+ let index = adapter.to_job_feedback_event_index().unwrap();
+ assert_eq!(index.event.id, event.id);
+ assert_eq!(index.event.author, event.author);
+ assert_eq!(index.event.created_at, event.created_at);
+ assert_eq!(index.event.kind, event.kind);
+ assert_eq!(index.event.content, event.content);
+ assert_eq!(index.event.sig, event.sig);
+}
diff --git a/crates/events-codec/tests/tag_builders.rs b/crates/events-codec/tests/tag_builders.rs
@@ -0,0 +1,449 @@
+use radroots_core::{
+ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity,
+ RadrootsCoreQuantityPrice, RadrootsCoreUnit,
+};
+use radroots_events::app_data::RadrootsAppData;
+use radroots_events::comment::RadrootsComment;
+use radroots_events::coop::RadrootsCoop;
+use radroots_events::document::{RadrootsDocument, RadrootsDocumentSubject};
+use radroots_events::farm::{
+ RadrootsFarm, RadrootsFarmRef, RadrootsGcsLocation, RadrootsGeoJsonPoint,
+ RadrootsGeoJsonPolygon,
+};
+use radroots_events::follow::{RadrootsFollow, RadrootsFollowProfile};
+use radroots_events::geochat::RadrootsGeoChat;
+use radroots_events::gift_wrap::{RadrootsGiftWrap, RadrootsGiftWrapRecipient};
+use radroots_events::job::{JobFeedbackStatus, JobInputType, JobPaymentRequest};
+use radroots_events::job_feedback::RadrootsJobFeedback;
+use radroots_events::job_request::{RadrootsJobInput, RadrootsJobParam, RadrootsJobRequest};
+use radroots_events::job_result::RadrootsJobResult;
+use radroots_events::kinds::{
+ KIND_JOB_FEEDBACK, KIND_JOB_REQUEST_MIN, KIND_JOB_RESULT_MIN, KIND_POST,
+};
+use radroots_events::list::{RadrootsList, RadrootsListEntry};
+use radroots_events::list_set::RadrootsListSet;
+use radroots_events::listing::{
+ RadrootsListing, RadrootsListingBin, RadrootsListingFarmRef, RadrootsListingProduct,
+};
+use radroots_events::message::{RadrootsMessage, RadrootsMessageRecipient};
+use radroots_events::message_file::RadrootsMessageFile;
+use radroots_events::plot::RadrootsPlot;
+use radroots_events::post::RadrootsPost;
+use radroots_events::profile::RadrootsProfile;
+use radroots_events::reaction::RadrootsReaction;
+use radroots_events::resource_area::{
+ RadrootsResourceArea, RadrootsResourceAreaLocation, RadrootsResourceAreaRef,
+};
+use radroots_events::resource_cap::{RadrootsResourceHarvestCap, RadrootsResourceHarvestProduct};
+use radroots_events::seal::RadrootsSeal;
+use radroots_events::RadrootsNostrEventPtr;
+use radroots_events::RadrootsNostrEventRef;
+use radroots_events_codec::job::encode::JobEncodeError;
+use radroots_events_codec::listing::encode::listing_build_tags;
+use radroots_events_codec::tag_builders::RadrootsEventTagBuilder;
+
+const TEST_PUBKEY_HEX: &str = "58e318557257f2ab58a415d21bb57082b4824cf667a1d64e72bcbc5acc018c62";
+const TEST_NPUB: &str = "npub1tr33s4tj2le2kk9yzhfphdtss26gyn8kv7savnnjhj794nqp333q8e7grr";
+
+fn sample_event_ref(id: &str) -> RadrootsNostrEventRef {
+ RadrootsNostrEventRef {
+ id: id.to_string(),
+ author: TEST_PUBKEY_HEX.to_string(),
+ kind: KIND_POST,
+ d_tag: None,
+ relays: None,
+ }
+}
+
+fn sample_gcs() -> RadrootsGcsLocation {
+ RadrootsGcsLocation {
+ lat: 37.0,
+ lng: -122.0,
+ geohash: "9q8yy".to_string(),
+ point: RadrootsGeoJsonPoint {
+ r#type: "Point".to_string(),
+ coordinates: [-122.0, 37.0],
+ },
+ polygon: RadrootsGeoJsonPolygon {
+ r#type: "Polygon".to_string(),
+ coordinates: vec![vec![
+ [-122.0, 37.0],
+ [-122.0, 37.0001],
+ [-122.0001, 37.0001],
+ [-122.0, 37.0],
+ ]],
+ },
+ accuracy: None,
+ altitude: None,
+ tag_0: None,
+ label: None,
+ area: None,
+ elevation: None,
+ soil: None,
+ climate: None,
+ gc_id: None,
+ gc_name: None,
+ gc_admin1_id: None,
+ gc_admin1_name: None,
+ gc_country_id: None,
+ gc_country_name: None,
+ }
+}
+
+fn sample_listing() -> RadrootsListing {
+ let quantity =
+ RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1u32), RadrootsCoreUnit::Each);
+ let price = RadrootsCoreQuantityPrice::new(
+ RadrootsCoreMoney::new(RadrootsCoreDecimal::from(10u32), RadrootsCoreCurrency::USD),
+ quantity.clone(),
+ );
+
+ RadrootsListing {
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(),
+ farm: RadrootsListingFarmRef {
+ pubkey: TEST_NPUB.to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
+ },
+ product: RadrootsListingProduct {
+ key: "sku".to_string(),
+ title: "Widget".to_string(),
+ category: "Tools".to_string(),
+ summary: None,
+ process: None,
+ lot: None,
+ location: None,
+ profile: None,
+ year: None,
+ },
+ primary_bin_id: "bin-1".to_string(),
+ bins: vec![RadrootsListingBin {
+ bin_id: "bin-1".to_string(),
+ quantity,
+ price_per_canonical_unit: price,
+ display_amount: None,
+ display_unit: None,
+ display_label: None,
+ display_price: None,
+ display_price_unit: None,
+ }],
+ resource_area: None,
+ plot: None,
+ discounts: None,
+ inventory_available: None,
+ availability: None,
+ delivery_method: None,
+ location: None,
+ images: None,
+ }
+}
+
+#[test]
+fn event_tag_builder_impls_build_tags_for_all_supported_types() {
+ let listing = sample_listing();
+ assert!(!listing.build_tags().unwrap().is_empty());
+ assert!(!listing_build_tags(&listing).unwrap().is_empty());
+
+ let app_data = RadrootsAppData {
+ d_tag: "radroots.app".to_string(),
+ content: "payload".to_string(),
+ };
+ assert!(!app_data.build_tags().unwrap().is_empty());
+
+ let comment = RadrootsComment {
+ root: sample_event_ref("root"),
+ parent: sample_event_ref("parent"),
+ content: "hello".to_string(),
+ };
+ assert!(!comment.build_tags().unwrap().is_empty());
+
+ let reaction = RadrootsReaction {
+ root: sample_event_ref("root"),
+ content: "+".to_string(),
+ };
+ assert!(!reaction.build_tags().unwrap().is_empty());
+
+ let message = RadrootsMessage {
+ recipients: vec![RadrootsMessageRecipient {
+ public_key: TEST_PUBKEY_HEX.to_string(),
+ relay_url: Some("wss://relay.example.com".to_string()),
+ }],
+ content: "hello".to_string(),
+ reply_to: Some(RadrootsNostrEventPtr {
+ id: "reply".to_string(),
+ relays: Some("wss://relay.example.com".to_string()),
+ }),
+ subject: Some("topic".to_string()),
+ };
+ assert!(!message.build_tags().unwrap().is_empty());
+
+ let message_file = RadrootsMessageFile {
+ recipients: vec![RadrootsMessageRecipient {
+ public_key: TEST_PUBKEY_HEX.to_string(),
+ relay_url: None,
+ }],
+ file_url: "https://files.example.com/blob".to_string(),
+ reply_to: None,
+ subject: None,
+ file_type: "image/jpeg".to_string(),
+ encryption_algorithm: "aes-gcm".to_string(),
+ decryption_key: "key".to_string(),
+ decryption_nonce: "nonce".to_string(),
+ encrypted_hash: "hash".to_string(),
+ original_hash: None,
+ size: None,
+ dimensions: None,
+ blurhash: None,
+ thumb: None,
+ fallbacks: vec!["https://files.example.com/fallback".to_string()],
+ };
+ assert!(!message_file.build_tags().unwrap().is_empty());
+
+ let geochat = RadrootsGeoChat {
+ geohash: "dr5rsj7".to_string(),
+ content: "hello".to_string(),
+ nickname: Some("alex".to_string()),
+ teleported: true,
+ };
+ assert!(!geochat.build_tags().unwrap().is_empty());
+
+ let follow = RadrootsFollow {
+ list: vec![RadrootsFollowProfile {
+ published_at: 1,
+ public_key: TEST_PUBKEY_HEX.to_string(),
+ relay_url: Some("wss://relay.example.com".to_string()),
+ contact_name: Some("alex".to_string()),
+ }],
+ };
+ assert!(!follow.build_tags().unwrap().is_empty());
+
+ let farm = RadrootsFarm {
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
+ name: "Farm".to_string(),
+ about: None,
+ website: None,
+ picture: None,
+ banner: None,
+ location: None,
+ tags: None,
+ };
+ assert!(!farm.build_tags().unwrap().is_empty());
+
+ let resource_area = RadrootsResourceArea {
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(),
+ name: "Area".to_string(),
+ about: None,
+ location: RadrootsResourceAreaLocation {
+ primary: None,
+ city: None,
+ region: None,
+ country: None,
+ gcs: sample_gcs(),
+ },
+ tags: None,
+ };
+ assert!(!resource_area.build_tags().unwrap().is_empty());
+
+ let resource_cap = RadrootsResourceHarvestCap {
+ d_tag: "AAAAAAAAAAAAAAAAAAAABA".to_string(),
+ resource_area: RadrootsResourceAreaRef {
+ pubkey: TEST_PUBKEY_HEX.to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(),
+ },
+ product: RadrootsResourceHarvestProduct {
+ key: "nutmeg".to_string(),
+ category: Some("spice".to_string()),
+ },
+ start: 1,
+ end: 2,
+ cap_quantity: RadrootsCoreQuantity::new(
+ RadrootsCoreDecimal::from(1000u32),
+ RadrootsCoreUnit::MassG,
+ ),
+ display_amount: None,
+ display_unit: None,
+ display_label: None,
+ tags: None,
+ };
+ assert!(!resource_cap.build_tags().unwrap().is_empty());
+
+ let coop = RadrootsCoop {
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(),
+ name: "Coop".to_string(),
+ about: None,
+ website: None,
+ picture: None,
+ banner: None,
+ location: None,
+ tags: None,
+ };
+ assert!(!coop.build_tags().unwrap().is_empty());
+
+ let document = RadrootsDocument {
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(),
+ doc_type: "charter".to_string(),
+ title: "Charter".to_string(),
+ version: "1.0.0".to_string(),
+ summary: None,
+ effective_at: None,
+ body_markdown: None,
+ subject: RadrootsDocumentSubject {
+ pubkey: TEST_PUBKEY_HEX.to_string(),
+ address: Some("30340:58e318557257f2ab58a415d21bb57082b4824cf667a1d64e72bcbc5acc018c62:AAAAAAAAAAAAAAAAAAAAAA".to_string()),
+ },
+ tags: None,
+ };
+ assert!(!document.build_tags().unwrap().is_empty());
+
+ let list = RadrootsList {
+ content: "private".to_string(),
+ entries: vec![RadrootsListEntry {
+ tag: "p".to_string(),
+ values: vec![TEST_PUBKEY_HEX.to_string()],
+ }],
+ };
+ assert!(!list.build_tags().unwrap().is_empty());
+
+ let list_set = RadrootsListSet {
+ d_tag: "members.owners".to_string(),
+ content: "private".to_string(),
+ entries: vec![RadrootsListEntry {
+ tag: "p".to_string(),
+ values: vec![TEST_PUBKEY_HEX.to_string()],
+ }],
+ title: Some("owners".to_string()),
+ description: Some("team".to_string()),
+ image: Some("https://example.com/team.png".to_string()),
+ };
+ assert!(!list_set.build_tags().unwrap().is_empty());
+
+ let plot = RadrootsPlot {
+ d_tag: "AAAAAAAAAAAAAAAAAAAABQ".to_string(),
+ farm: RadrootsFarmRef {
+ pubkey: TEST_PUBKEY_HEX.to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
+ },
+ name: "Plot".to_string(),
+ about: None,
+ location: None,
+ tags: None,
+ };
+ assert!(!plot.build_tags().unwrap().is_empty());
+
+ let job_request = RadrootsJobRequest {
+ kind: (KIND_JOB_REQUEST_MIN + 1) as u16,
+ inputs: vec![RadrootsJobInput {
+ data: "hello".to_string(),
+ input_type: JobInputType::Text,
+ relay: None,
+ marker: None,
+ }],
+ output: None,
+ params: vec![RadrootsJobParam {
+ key: "foo".to_string(),
+ value: "bar".to_string(),
+ }],
+ bid_sat: None,
+ relays: vec!["wss://relay.example.com".to_string()],
+ providers: vec![TEST_PUBKEY_HEX.to_string()],
+ topics: vec!["topic".to_string()],
+ encrypted: false,
+ };
+ assert!(!job_request.build_tags().unwrap().is_empty());
+
+ let job_result = RadrootsJobResult {
+ kind: (KIND_JOB_RESULT_MIN + 1) as u16,
+ request_event: RadrootsNostrEventPtr {
+ id: "req".to_string(),
+ relays: Some("wss://relay.example.com".to_string()),
+ },
+ request_json: None,
+ inputs: vec![RadrootsJobInput {
+ data: "hello".to_string(),
+ input_type: JobInputType::Text,
+ relay: None,
+ marker: None,
+ }],
+ customer_pubkey: Some(TEST_PUBKEY_HEX.to_string()),
+ payment: Some(JobPaymentRequest {
+ amount_sat: 1,
+ bolt11: None,
+ }),
+ content: Some("payload".to_string()),
+ encrypted: false,
+ };
+ assert!(!job_result.build_tags().unwrap().is_empty());
+
+ let job_feedback = RadrootsJobFeedback {
+ kind: KIND_JOB_FEEDBACK as u16,
+ status: JobFeedbackStatus::Processing,
+ extra_info: Some("queued".to_string()),
+ request_event: RadrootsNostrEventPtr {
+ id: "req".to_string(),
+ relays: Some("wss://relay.example.com".to_string()),
+ },
+ customer_pubkey: Some(TEST_PUBKEY_HEX.to_string()),
+ payment: Some(JobPaymentRequest {
+ amount_sat: 1,
+ bolt11: None,
+ }),
+ content: Some("payload".to_string()),
+ encrypted: false,
+ };
+ assert!(!job_feedback.build_tags().unwrap().is_empty());
+
+ let seal = RadrootsSeal {
+ content: "sealed".to_string(),
+ };
+ assert!(seal.build_tags().unwrap().is_empty());
+
+ let gift_wrap = RadrootsGiftWrap {
+ recipient: RadrootsGiftWrapRecipient {
+ public_key: TEST_PUBKEY_HEX.to_string(),
+ relay_url: Some("wss://relay.example.com".to_string()),
+ },
+ content: "encrypted".to_string(),
+ expiration: Some(1700000000),
+ };
+ assert!(!gift_wrap.build_tags().unwrap().is_empty());
+
+ let profile = RadrootsProfile {
+ name: "alice".to_string(),
+ display_name: None,
+ nip05: None,
+ about: None,
+ website: None,
+ picture: None,
+ banner: None,
+ lud06: None,
+ lud16: None,
+ bot: None,
+ };
+ assert!(profile.build_tags().unwrap().is_empty());
+
+ let post = RadrootsPost {
+ content: "hello".to_string(),
+ };
+ assert!(post.build_tags().unwrap().is_empty());
+}
+
+#[test]
+fn job_request_tag_builder_rejects_encrypted_without_provider() {
+ let request = RadrootsJobRequest {
+ kind: (KIND_JOB_REQUEST_MIN + 1) as u16,
+ inputs: vec![RadrootsJobInput {
+ data: "hello".to_string(),
+ input_type: JobInputType::Text,
+ relay: None,
+ marker: None,
+ }],
+ output: None,
+ params: Vec::new(),
+ bid_sat: None,
+ relays: Vec::new(),
+ providers: Vec::new(),
+ topics: Vec::new(),
+ encrypted: true,
+ };
+ let err = request.build_tags().unwrap_err();
+ assert!(matches!(err, JobEncodeError::MissingProvidersForEncrypted));
+}