app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit b86468be9147bef986e2f18e08793a97d931933a
parent bb4c426c25ea96cbad62578437d172a6a3b772ca
Author: triesap <tyson@radroots.org>
Date:   Fri,  2 Oct 2026 02:39:45 +0000

test: cover replacement and deletion protocol ordering

- Verify public raw heads before focused projection and deletion evaluation
- Cover arrival permutations, inclusive cutoffs and forged deletion refusal
- Preserve newer deleted or unsupported heads without older resurrection
- Pass formatting, workspace check, 29 tests, Clippy and foundation review

Diffstat:
Acore/crates/harvestcircle_nostr/tests/fixtures/availability_lifecycle_v1.rs | 191+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/harvestcircle_nostr/tests/focused_availability_lifecycle.rs | 627+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 818 insertions(+), 0 deletions(-)

diff --git a/core/crates/harvestcircle_nostr/tests/fixtures/availability_lifecycle_v1.rs b/core/crates/harvestcircle_nostr/tests/fixtures/availability_lifecycle_v1.rs @@ -0,0 +1,191 @@ +//! Public raw lifecycle recipes; identity and signature bytes are produced in isolation. + +pub const IDENTIFIER: &str = "hcav-lifecycle-v1"; +pub const OTHER_IDENTIFIER: &str = "hcav-lifecycle-distinct"; +// Synthetic protocol control only; this does not enable another product kind. +pub const OTHER_ADDRESSABLE_KIND: u16 = 30023; +pub const CUTOFF: u64 = 1_800_000_100; +pub const ALL_THREE_PERMUTATIONS: [[usize; 3]; 6] = [ + [0, 1, 2], + [0, 2, 1], + [1, 0, 2], + [1, 2, 0], + [2, 0, 1], + [2, 1, 0], +]; + +pub enum Profile { + Focused(&'static str), + Generic, +} + +pub struct Version { + pub name: &'static str, + pub created_at: u64, + pub identifier: &'static str, + pub profile: Profile, + pub content: &'static str, + pub other_author: bool, +} + +pub const VERSIONS: [Version; 11] = [ + Version { + name: "old_active", + created_at: 1_800_000_100, + identifier: IDENTIFIER, + profile: Profile::Focused("active"), + content: "Public lifecycle conformance.", + other_author: false, + }, + Version { + name: "newer_sold", + created_at: 1_800_000_200, + identifier: IDENTIFIER, + profile: Profile::Focused("sold"), + content: "Public lifecycle conformance.", + other_author: false, + }, + Version { + name: "tie_left", + created_at: 1_800_000_300, + identifier: IDENTIFIER, + profile: Profile::Focused("active"), + content: "Public equal-time left version.", + other_author: false, + }, + Version { + name: "tie_right", + created_at: 1_800_000_300, + identifier: IDENTIFIER, + profile: Profile::Focused("sold"), + content: "Public equal-time right version.", + other_author: false, + }, + Version { + name: "generic_head", + created_at: 1_800_000_200, + identifier: IDENTIFIER, + profile: Profile::Generic, + content: "Public unsupported generic version.", + other_author: false, + }, + Version { + name: "malformed_head", + created_at: 1_800_000_200, + identifier: IDENTIFIER, + profile: Profile::Focused("unsupported"), + content: "Public unsupported focused version.", + other_author: false, + }, + Version { + name: "other_identifier", + created_at: 1_800_000_100, + identifier: OTHER_IDENTIFIER, + profile: Profile::Focused("active"), + content: "Public lifecycle conformance.", + other_author: false, + }, + Version { + name: "other_author", + created_at: 1_800_000_100, + identifier: IDENTIFIER, + profile: Profile::Focused("active"), + content: "Public lifecycle conformance.", + other_author: true, + }, + Version { + name: "before_cutoff", + created_at: 1_800_000_099, + identifier: IDENTIFIER, + profile: Profile::Focused("active"), + content: "Public version before cutoff.", + other_author: false, + }, + Version { + name: "at_cutoff", + created_at: 1_800_000_100, + identifier: IDENTIFIER, + profile: Profile::Focused("active"), + content: "Public version at cutoff.", + other_author: false, + }, + Version { + name: "after_cutoff", + created_at: 1_800_000_101, + identifier: IDENTIFIER, + profile: Profile::Focused("active"), + content: "Public version after cutoff.", + other_author: false, + }, +]; + +pub enum Target { + Event(&'static str), + Address, + EventAndAddress(&'static str), +} + +pub struct Deletion { + pub name: &'static str, + pub created_at: u64, + pub target: Target, + pub other_author: bool, +} + +pub const DELETIONS: [Deletion; 6] = [ + Deletion { + name: "exact_newer", + created_at: 1_800_000_000, + target: Target::Event("newer_sold"), + other_author: false, + }, + Deletion { + name: "address_cutoff", + created_at: 1_800_000_100, + target: Target::Address, + other_author: false, + }, + Deletion { + name: "forged_exact", + created_at: 1_800_000_300, + target: Target::Event("newer_sold"), + other_author: true, + }, + Deletion { + name: "forged_address", + created_at: 1_800_000_300, + target: Target::Address, + other_author: true, + }, + Deletion { + name: "combined_cutoff", + created_at: 1_800_000_100, + target: Target::EventAndAddress("at_cutoff"), + other_author: false, + }, + Deletion { + name: "unrelated_exact", + created_at: 1_800_000_300, + target: Target::Event("tie_left"), + other_author: false, + }, +]; + +pub fn listing_tags(version: &Version) -> Vec<Vec<String>> { + let mut tags = vec![ + vec!["d".into(), version.identifier.into()], + vec!["title".into(), "Public lifecycle fixture".into()], + vec!["summary".into(), "Public lifecycle conformance".into()], + vec!["published_at".into(), "1800000000".into()], + vec!["location".into(), "Protocol test location".into()], + vec!["price".into(), "3.5".into(), "CAD".into()], + ]; + match version.profile { + Profile::Focused(status) => { + tags.push(vec!["radroots:price_unit".into(), "lb".into()]); + tags.push(vec!["status".into(), status.into()]); + } + Profile::Generic => tags.push(vec!["status".into(), "active".into()]), + } + tags +} diff --git a/core/crates/harvestcircle_nostr/tests/focused_availability_lifecycle.rs b/core/crates/harvestcircle_nostr/tests/focused_availability_lifecycle.rs @@ -0,0 +1,627 @@ +use nostr::{Event, EventBuilder, JsonUtil, Keys, Kind, Tag, Timestamp}; +use radroots_event::envelope::event_head::{ + CurrentEventHead, EventHeadCandidate, EventHeadCandidateResult, EventHeadCoordinate, + EventHeadDecision, event_head_candidate_for_nip01_event, select_event_head, +}; +use radroots_event::food::availability::FoodAvailabilityError; +use radroots_event::listing::classified::ClassifiedListingPartition; +use radroots_event::wire::Nip01EventWire; +use radroots_event_codec::admission::deletion::{ + RadrootsAdmittedNip09DeletionRequestEvent, RadrootsNip09DeletionAdmissionError, + RadrootsNip09SuppressionOutcome, RadrootsNip09SuppressionReason, evaluate_nip09_suppression, + verify_and_admit_nip09_deletion_request_event, +}; +use radroots_event_codec::admission::food_availability::{ + RadrootsFoodAvailabilityAdmissionError, RadrootsFoodAvailabilityAdmissionOutcome, + admit_verified_food_availability_event, +}; +use radroots_event_codec::decode::food_availability::RadrootsFoodAvailabilityProjectionError; +use radroots_event_codec::verify::{ + RadrootsNip01VerificationError, RadrootsSignatureVerifiedEvent, verify_nip01_event, +}; + +#[path = "fixtures/availability_lifecycle_v1.rs"] +mod corpus; + +const MAX_EVENT_BYTES: usize = 4 * 1024; +const MAX_CORPUS_BYTES: usize = 64 * 1024; +const MAX_RETAINED_SOURCE_BYTES: usize = 16 * 1024; +const RETAINED_SOURCE: &str = include_str!("fixtures/availability_lifecycle_v1.rs"); + +struct PublicEvent { + name: &'static str, + json: String, +} + +struct PublicCorpus { + author: String, + other_author: String, + events: Vec<PublicEvent>, +} + +fn sign_event( + keys: &Keys, + kind: u16, + created_at: u64, + content: &str, + tags: Vec<Vec<String>>, +) -> Event { + assert!(tags.len() <= 16); + assert!( + content.len() + tags.iter().flatten().map(String::len).sum::<usize>() <= MAX_EVENT_BYTES + ); + let tags = tags + .into_iter() + .map(|tag| Tag::parse(tag).expect("bounded public raw tag")); + EventBuilder::new(Kind::from(kind), content) + .tags(tags) + .custom_created_at(Timestamp::from(created_at)) + .sign_with_keys(keys) + .expect("isolated public lifecycle signing") +} + +/// Both signers are dropped before any retained public JSON leaves the helper. +fn public_corpus() -> PublicCorpus { + let owner = Keys::generate(); + let other = Keys::generate(); + let author = owner.public_key().to_hex(); + let other_author = other.public_key().to_hex(); + assert_ne!(author, other_author); + let mut signed = Vec::<(&'static str, Event)>::new(); + for version in &corpus::VERSIONS { + let keys = if version.other_author { &other } else { &owner }; + signed.push(( + version.name, + sign_event( + keys, + 30402, + version.created_at, + version.content, + corpus::listing_tags(version), + ), + )); + } + let original = &corpus::VERSIONS[0]; + assert_eq!(original.name, "old_active"); + signed.push(( + "other_kind", + sign_event( + &owner, + corpus::OTHER_ADDRESSABLE_KIND, + original.created_at, + original.content, + corpus::listing_tags(original), + ), + )); + let address = format!("30402:{author}:{}", corpus::IDENTIFIER); + for deletion in &corpus::DELETIONS { + let mut tags = Vec::new(); + match deletion.target { + corpus::Target::Event(name) | corpus::Target::EventAndAddress(name) => { + let target = signed + .iter() + .find(|(target_name, _)| *target_name == name) + .expect("public target recipe exists"); + tags.push(vec!["e".into(), target.1.id.to_hex()]); + } + corpus::Target::Address => {} + } + if matches!( + deletion.target, + corpus::Target::Address | corpus::Target::EventAndAddress(_) + ) { + tags.push(vec!["a".into(), address.clone()]); + } + tags.push(vec!["k".into(), "30402".into()]); + let keys = if deletion.other_author { + &other + } else { + &owner + }; + signed.push(( + deletion.name, + sign_event( + keys, + 5, + deletion.created_at, + "Public deletion conformance.", + tags, + ), + )); + } + drop(owner); + drop(other); + let events = signed + .into_iter() + .map(|(name, event)| { + let json = event.as_json(); + assert!(json.len() <= MAX_EVENT_BYTES); + PublicEvent { name, json } + }) + .collect::<Vec<_>>(); + assert_eq!(events.len(), 18); + assert!(events.iter().map(|event| event.json.len()).sum::<usize>() <= MAX_CORPUS_BYTES); + PublicCorpus { + author, + other_author, + events, + } +} + +fn public_json<'a>(corpus: &'a PublicCorpus, name: &str) -> &'a str { + &corpus + .events + .iter() + .find(|event| event.name == name) + .expect("named public event") + .json +} + +fn verified(corpus: &PublicCorpus, name: &str) -> RadrootsSignatureVerifiedEvent { + let wire = Nip01EventWire::parse_json(public_json(corpus, name)) + .expect("bounded public ID-verified wire"); + assert!(wire.extra.is_empty()); + verify_nip01_event(wire.into_envelope().expect("ID-verified envelope")) + .expect("shared ID and signature verification") +} + +fn deletion(corpus: &PublicCorpus, name: &str) -> RadrootsAdmittedNip09DeletionRequestEvent { + let wire = Nip01EventWire::parse_json(public_json(corpus, name)).expect("public deletion wire"); + verify_and_admit_nip09_deletion_request_event(wire.into_envelope().expect("deletion envelope")) + .expect("shared NIP-09 request admission without a target lookup") +} + +fn candidate(event: &RadrootsSignatureVerifiedEvent) -> EventHeadCandidate { + match event_head_candidate_for_nip01_event(event.event()) { + EventHeadCandidateResult::Candidate(candidate) => candidate, + _ => panic!("verified addressable fixture must produce a raw protocol head candidate"), + } +} + +/// Every supplied verified version enters shared ordering before product projection or suppression. +fn select_all_versions(versions: &[&RadrootsSignatureVerifiedEvent]) -> CurrentEventHead { + assert!(!versions.is_empty()); + assert!(versions.len() <= 3); + let mut head = None; + for version in versions { + match select_event_head(candidate(version), head.as_ref()) { + EventHeadDecision::Applied(next) => head = Some(next), + EventHeadDecision::SkippedDuplicate + | EventHeadDecision::SkippedOlder + | EventHeadDecision::SkippedSameTimestampHigherEventId => {} + EventHeadDecision::CoordinateMismatch => { + panic!("single-coordinate permutation fixture") + } + } + } + head.expect("shared selected head") +} + +fn assert_focused_status(event: &RadrootsSignatureVerifiedEvent, status: &str) { + match admit_verified_food_availability_event(event.clone()).expect("focused head admission") { + RadrootsFoodAvailabilityAdmissionOutcome::Admitted(admitted) => { + assert_eq!(admitted.projection().status().as_str(), status); + } + _ => panic!("selected focused version must be admitted"), + } +} + +#[test] +fn raw_head_selection_preserves_reverse_arrival_and_exact_duplicates() { + let corpus = public_corpus(); + let old = verified(&corpus, "old_active"); + let new = verified(&corpus, "newer_sold"); + let mut cases = 0; + for order in [[&old, &new, &new], [&new, &old, &new], [&new, &new, &old]] { + let head = select_all_versions(&order); + assert_eq!(head.event_id, *new.event().id()); + assert_eq!(head.created_at, 1_800_000_200); + assert_eq!( + select_event_head(candidate(&new), Some(&head)), + EventHeadDecision::SkippedDuplicate + ); + assert_eq!( + select_event_head(candidate(&old), Some(&head)), + EventHeadDecision::SkippedOlder + ); + assert_focused_status(&new, "sold"); + cases += 1; + } + assert_eq!(cases, 3); +} + +#[test] +fn equal_timestamp_heads_select_lowest_event_id_in_every_order() { + let corpus = public_corpus(); + let left = verified(&corpus, "tie_left"); + let right = verified(&corpus, "tie_right"); + assert_ne!(left.event().id(), right.event().id()); + assert_eq!( + left.event().created_at_u64(), + right.event().created_at_u64() + ); + let (lowest, higher) = if left.event().id() < right.event().id() { + (&left, &right) + } else { + (&right, &left) + }; + let mut cases = 0; + for order in [[&left, &right, &left], [&right, &left, &right]] { + let head = select_all_versions(&order); + assert_eq!(head.event_id, *lowest.event().id()); + assert_eq!(head.created_at, 1_800_000_300); + assert_eq!( + select_event_head(candidate(higher), Some(&head)), + EventHeadDecision::SkippedSameTimestampHigherEventId + ); + assert_eq!( + select_event_head(candidate(lowest), Some(&head)), + EventHeadDecision::SkippedDuplicate + ); + cases += 1; + } + assert_eq!(cases, 2); +} + +#[test] +fn shared_coordinates_keep_distinct_authors_kinds_and_identifiers_separate() { + let corpus = public_corpus(); + let original = verified(&corpus, "old_active"); + let head = select_all_versions(&[&original]); + match &head.coordinate { + EventHeadCoordinate::Addressable { + kind, + pubkey, + d_tag, + } => { + assert_eq!(*kind, 30402); + assert_eq!(pubkey.to_hex(), corpus.author); + assert_eq!(d_tag, "hcav-lifecycle-v1"); + } + _ => panic!("kind 30402 has an addressable coordinate"), + } + for name in ["other_identifier", "other_author", "other_kind"] { + let distinct = verified(&corpus, name); + assert_eq!(distinct.event().content(), original.event().content()); + assert_eq!( + select_event_head(candidate(&distinct), Some(&head)), + EventHeadDecision::CoordinateMismatch + ); + assert_ne!(candidate(&distinct).coordinate, head.coordinate); + if name == "other_kind" { + assert_eq!(distinct.event().author(), original.event().author()); + match &candidate(&distinct).coordinate { + EventHeadCoordinate::Addressable { + kind, + pubkey, + d_tag, + } => { + assert_eq!(*kind, u32::from(corpus::OTHER_ADDRESSABLE_KIND)); + assert_ne!(*kind, original.event().kind_u32()); + assert_eq!(*pubkey, *original.event().author()); + assert_eq!(d_tag, corpus::IDENTIFIER); + } + _ => panic!("control must have an addressable coordinate"), + } + } + assert_eq!( + select_all_versions(&[&distinct]).event_id, + *distinct.event().id() + ); + } + assert_eq!( + verified(&corpus, "other_author").event().author().to_hex(), + corpus.other_author + ); +} + +#[test] +fn exact_deletion_before_target_and_deleted_head_never_resurrect_older_active() { + let corpus = public_corpus(); + // Admission first retains the request without consulting any target event. + let request = deletion(&corpus, "exact_newer"); + assert_eq!(request.event().created_at_u64(), 1_800_000_000); + let old = verified(&corpus, "old_active"); + let new = verified(&corpus, "newer_sold"); + assert!(request.event().created_at_u64() < new.event().created_at_u64()); + let mut cases = 0; + for order in corpus::ALL_THREE_PERMUTATIONS { + let mut versions = Vec::new(); + let mut requests = Vec::new(); + for index in order { + match index { + 0 => versions.push(&old), + 1 => versions.push(&new), + 2 => requests.push(request.clone()), + _ => unreachable!("three-item permutation"), + } + if !versions.is_empty() { + let head = select_all_versions(&versions); + let winner = if head.event_id == *new.event().id() { + &new + } else { + &old + }; + let decision = evaluate_nip09_suppression(winner, &requests); + if head.event_id == *new.event().id() && !requests.is_empty() { + assert_eq!( + decision.outcome(), + RadrootsNip09SuppressionOutcome::Suppressed + ); + assert_eq!( + decision.reason(), + RadrootsNip09SuppressionReason::EventIdReference + ); + assert_eq!( + decision + .event_reference() + .expect("exact reference evidence") + .request_id(), + request.event().id() + ); + } else { + assert_eq!(decision.outcome(), RadrootsNip09SuppressionOutcome::Visible); + assert_eq!( + decision.reason(), + RadrootsNip09SuppressionReason::NoAuthorizedReference + ); + } + } + } + let head = select_all_versions(&versions); + assert_eq!(head.event_id, *new.event().id()); + assert_eq!( + evaluate_nip09_suppression(&new, &requests).outcome(), + RadrootsNip09SuppressionOutcome::Suppressed + ); + assert_eq!( + evaluate_nip09_suppression(&old, &requests).outcome(), + RadrootsNip09SuppressionOutcome::Visible + ); + assert_eq!( + select_event_head(candidate(&old), Some(&head)), + EventHeadDecision::SkippedOlder + ); + assert_focused_status(&old, "active"); + assert_ne!(head.event_id, *old.event().id()); + cases += 1; + } + assert_eq!(cases, 6); +} + +#[test] +fn address_deletion_cutoff_is_inclusive_and_arrival_order_independent() { + let corpus = public_corpus(); + let request = deletion(&corpus, "address_cutoff"); + assert_eq!(request.event().created_at_u64(), 1_800_000_100); + let mut cases = 0; + for (name, outcome, reason) in [ + ( + "before_cutoff", + RadrootsNip09SuppressionOutcome::Suppressed, + RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff, + ), + ( + "at_cutoff", + RadrootsNip09SuppressionOutcome::Suppressed, + RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff, + ), + ( + "after_cutoff", + RadrootsNip09SuppressionOutcome::Visible, + RadrootsNip09SuppressionReason::AddressCutoffPrecedesTarget, + ), + ] { + for order in [[0, 1], [1, 0]] { + let mut target = None; + let mut retained = Vec::new(); + for arrival in order { + match arrival { + 0 => target = Some(verified(&corpus, name)), + 1 => retained.push(deletion(&corpus, "address_cutoff")), + _ => unreachable!("two-item arrival permutation"), + } + if let Some(target) = target.as_ref() { + let head = select_all_versions(&[target]); + assert_eq!(head.event_id, *target.event().id()); + let decision = evaluate_nip09_suppression(target, &retained); + if retained.is_empty() { + assert_eq!(decision.outcome(), RadrootsNip09SuppressionOutcome::Visible); + assert_eq!( + decision.reason(), + RadrootsNip09SuppressionReason::NoAuthorizedReference + ); + } else { + assert_eq!(decision.outcome(), outcome); + assert_eq!(decision.reason(), reason); + } + } else { + assert_eq!( + retained.len(), + 1, + "deletion retained before any target observation" + ); + } + } + let target = target.expect("target observed in each permutation"); + let head = select_all_versions(&[&target]); + let decision = evaluate_nip09_suppression(&target, &retained); + assert_eq!(head.event_id, *target.event().id()); + assert_eq!(decision.outcome(), outcome); + assert_eq!(decision.reason(), reason); + assert!(decision.event_reference().is_none()); + let evidence = decision + .address_reference() + .expect("retained matching address evidence"); + assert_eq!(evidence.inclusive_cutoff(), corpus::CUTOFF); + assert_eq!(evidence.request_id(), request.event().id()); + assert_eq!(evidence.coordinate().kind(), 30402); + assert_eq!(evidence.coordinate().pubkey().to_hex(), corpus.author); + assert_eq!(evidence.coordinate().identifier(), "hcav-lifecycle-v1"); + cases += 1; + } + } + assert_eq!(cases, 6); + let combined = deletion(&corpus, "combined_cutoff"); + let target = verified(&corpus, "at_cutoff"); + let decision = evaluate_nip09_suppression(&target, std::slice::from_ref(&combined)); + assert_eq!( + decision.outcome(), + RadrootsNip09SuppressionOutcome::Suppressed + ); + assert_eq!( + decision.reason(), + RadrootsNip09SuppressionReason::EventIdAndAddressReference + ); + assert_eq!( + decision + .event_reference() + .expect("exact evidence") + .request_id(), + combined.event().id() + ); + assert_eq!( + decision + .address_reference() + .expect("address evidence") + .request_id(), + combined.event().id() + ); +} + +#[test] +fn forged_deletions_cannot_suppress_other_author_and_invalid_signatures_are_rejected() { + let corpus = public_corpus(); + let target = verified(&corpus, "newer_sold"); + let head = select_all_versions(&[&target]); + for name in ["forged_exact", "forged_address"] { + let forged = deletion(&corpus, name); + assert_eq!(forged.event().author().to_hex(), corpus.other_author); + assert_ne!(forged.event().author(), target.event().author()); + let decision = evaluate_nip09_suppression(&target, std::slice::from_ref(&forged)); + assert_eq!(decision.outcome(), RadrootsNip09SuppressionOutcome::Visible); + assert_eq!( + decision.reason(), + RadrootsNip09SuppressionReason::RequestAuthorMismatch + ); + assert!(decision.event_reference().is_none()); + assert!(decision.address_reference().is_none()); + assert_eq!(head.event_id, *target.event().id()); + } + let unrelated = deletion(&corpus, "unrelated_exact"); + let decision = evaluate_nip09_suppression(&target, std::slice::from_ref(&unrelated)); + assert_eq!(decision.outcome(), RadrootsNip09SuppressionOutcome::Visible); + assert_eq!( + decision.reason(), + RadrootsNip09SuppressionReason::NoAuthorizedReference + ); + + let json = public_json(&corpus, "exact_newer"); + let wire = Nip01EventWire::parse_json_unverified(json).expect("public deletion wire"); + let signature_field = format!("\"sig\":\"{}\"", wire.sig); + assert_eq!(json.matches(&signature_field).count(), 1); + let tampered = json.replacen( + &signature_field, + &format!("\"sig\":\"{}\"", "0".repeat(128)), + 1, + ); + let envelope = Nip01EventWire::parse_json_unverified(&tampered) + .expect("bounded invalid-signature wire") + .into_unverified_envelope() + .expect("untrusted deletion envelope"); + assert_eq!( + verify_and_admit_nip09_deletion_request_event(envelope) + .expect_err("invalid deletion has no admitted request authority"), + RadrootsNip09DeletionAdmissionError::Nip01Verification( + RadrootsNip01VerificationError::SignatureInvalid + ) + ); +} + +#[test] +fn newer_verified_unsupported_heads_never_promote_older_active_versions() { + let corpus = public_corpus(); + let old = verified(&corpus, "old_active"); + let mut cases = 0; + for name in ["generic_head", "malformed_head"] { + let unsupported = verified(&corpus, name); + for order in [ + [&old, &unsupported, &old], + [&unsupported, &old, &unsupported], + ] { + // The unsupported version participates before focused admission is attempted. + let head = select_all_versions(&order); + assert_eq!(head.event_id, *unsupported.event().id()); + assert_eq!(head.created_at, 1_800_000_200); + let result = admit_verified_food_availability_event(unsupported.clone()); + if name == "generic_head" { + match result.expect("verified generic exclusion") { + RadrootsFoodAvailabilityAdmissionOutcome::Excluded(candidate) => { + assert_eq!( + candidate.partition(), + ClassifiedListingPartition::GenericNip99 + ); + assert_eq!(candidate.event().id(), &head.event_id); + } + _ => panic!("generic winning head remains unsupported"), + } + } else { + assert_eq!( + result.expect_err("malformed focused winning head remains known"), + RadrootsFoodAvailabilityAdmissionError::Projection( + RadrootsFoodAvailabilityProjectionError::Domain( + FoodAvailabilityError::StatusInvalid + ) + ) + ); + } + assert_eq!( + evaluate_nip09_suppression(&unsupported, &[]).outcome(), + RadrootsNip09SuppressionOutcome::Visible + ); + assert_eq!( + select_event_head(candidate(&old), Some(&head)), + EventHeadDecision::SkippedOlder + ); + assert_focused_status(&old, "active"); + assert_ne!(head.event_id, *old.event().id()); + cases += 1; + } + } + assert_eq!(cases, 4); +} + +#[test] +fn lifecycle_corpus_is_bounded_and_retains_only_verified_public_material() { + assert_eq!(corpus::VERSIONS.len(), 11); + assert_eq!(corpus::DELETIONS.len(), 6); + assert_eq!(corpus::ALL_THREE_PERMUTATIONS.len(), 6); + assert!(RETAINED_SOURCE.len() <= MAX_RETAINED_SOURCE_BYTES); + for forbidden in [ + "nsec1", + "SecretKey", + "secret_key", + "private_key", + "Keys::", + "credential", + ] { + assert!( + !RETAINED_SOURCE.contains(forbidden), + "public retained raw recipes" + ); + } + let corpus = public_corpus(); + assert_eq!(corpus.events.len(), 18); + for event in &corpus.events { + for forbidden in ["nsec1", "secret_key", "private_key", "credential"] { + assert!( + !event.json.contains(forbidden), + "public signed lifecycle artifact" + ); + } + let verified = verified(&corpus, event.name); + assert!(matches!(verified.event().kind_u32(), 5 | 30402 | 30023)); + let author = verified.event().author().to_hex(); + assert!(author == corpus.author || author == corpus.other_author); + } +}