From ca0de736a4d86f5d8438219abcc7ef21ca6980fc Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 17 Jun 2026 12:00:29 +0000 Subject: [PATCH] test(deletion): add deep and shared-subgraph cascade coverage --- tests/nip09_cascade_event_types.rs | 97 +++++++++++++++++++++ tests/nip09_multi_maintainer.rs | 133 +++++++++++++++++++++++++++++ 2 files changed, 230 insertions(+) diff --git a/tests/nip09_cascade_event_types.rs b/tests/nip09_cascade_event_types.rs index 45027e0..f1a0ad0 100644 --- a/tests/nip09_cascade_event_types.rs +++ b/tests/nip09_cascade_event_types.rs @@ -240,3 +240,100 @@ async fn test_announcement_cascade_deletes_served_pr_chain_kinds() { "announcement deletion should cascade across PR-chain dependent kinds; survivors: {survivors:?}" ); } + +#[tokio::test] +async fn test_announcement_cascade_deletes_deep_mixed_reference_chain() { + let relay = TestRelay::start().await; + let client = AuditClient::new(relay.url(), AuditConfig::isolated()) + .await + .expect("create audit client"); + let keys = client.keys().clone(); + + let (announcement, repo_id) = publish_served_repo(&client, "cascade-deep-mixed-chain").await; + let coordinate = announcement_coordinate(&announcement, &repo_id); + + // Root-dependent issue and patch via `a` tags. + let issue = client + .create_issue(&announcement, "Deep chain issue", "issue body", vec![]) + .expect("build issue"); + let patch = client + .event_builder(Kind::from(1617), "deep chain patch") + .tag(Tag::custom("a", vec![coordinate.clone()])) + .build(&keys) + .expect("build patch"); + + // Mid-chain descendants mix `e` and `q` references. + let comment = client + .event_builder(Kind::from(1111), "comment on issue") + .tag(Tag::custom("e", vec![issue.id.to_hex()])) + .build(&keys) + .expect("build issue comment"); + let quote = client + .event_builder(Kind::from(1111), "quote patch and comment") + .tag(Tag::custom("q", vec![patch.id.to_hex()])) + .tag(Tag::custom("e", vec![comment.id.to_hex()])) + .build(&keys) + .expect("build quote event"); + + // Deep leaf status references quote by `q` and issue by `e`. + let status = client + .event_builder(Kind::from(1630), "deep chain status") + .tag(Tag::custom("q", vec![quote.id.to_hex()])) + .tag(Tag::custom("e", vec![issue.id.to_hex()])) + .build(&keys) + .expect("build deep status"); + + for event in [&issue, &patch, &comment, "e, &status] { + client + .send_event(event.clone()) + .await + .expect("relay should accept deep-chain event"); + } + + tokio::time::sleep(Duration::from_millis(350)).await; + + let deep_chain = [ + ("announcement", announcement.id), + ("issue", issue.id), + ("patch", patch.id), + ("comment", comment.id), + ("quote", quote.id), + ("status", status.id), + ]; + + for (label, id) in deep_chain { + assert!( + client + .is_event_on_relay(id) + .await + .expect("query event before deletion"), + "{label} ({id}) must be served before deletion" + ); + } + + let deletion = build_deletion(&client, &[], std::slice::from_ref(&coordinate)); + client + .send_event(deletion) + .await + .expect("relay should accept announcement deletion"); + + tokio::time::sleep(Duration::from_millis(700)).await; + + let mut survivors = Vec::new(); + for (label, id) in deep_chain { + let served = client + .is_event_on_relay(id) + .await + .expect("query event after deletion"); + if served { + survivors.push(format!("{label} ({id})")); + } + } + + relay.stop().await; + + assert!( + survivors.is_empty(), + "announcement deletion should cascade through deep mixed-tag chains; survivors: {survivors:?}" + ); +} diff --git a/tests/nip09_multi_maintainer.rs b/tests/nip09_multi_maintainer.rs index f3cc455..0a704ac 100644 --- a/tests/nip09_multi_maintainer.rs +++ b/tests/nip09_multi_maintainer.rs @@ -534,3 +534,136 @@ async fn test_three_maintainers_subset_anchors_survive_middle_deletion() { "announcement B coordinate must not be served after deletion" ); } + +#[tokio::test] +async fn test_shared_subgraph_survives_when_one_anchor_path_removed() { + let relay = TestRelay::start().await; + let client_a = AuditClient::new(relay.url(), AuditConfig::isolated()) + .await + .expect("create maintainer A client"); + let client_b = + AuditClient::new_with_keys(relay.url(), AuditConfig::isolated(), Keys::generate()) + .await + .expect("create maintainer B client"); + + let (announcement_a, repo_id) = publish_served_repo_with_maintainers( + &client_a, + "multi-shared-subgraph", + &[client_b.public_key().to_hex()], + ) + .await; + let announcement_b = publish_served_announcement_for_identifier(&client_b, &repo_id).await; + let coordinate_a = announcement_coordinate(&announcement_a, &repo_id); + let coordinate_b = announcement_coordinate(&announcement_b, &repo_id); + + // Top-level dependents under different anchoring conditions. + let top_shared = client_a + .event_builder(Kind::from(1617), "top shared A+B") + .tag(Tag::custom("a", vec![coordinate_a.clone()])) + .tag(Tag::custom("a", vec![coordinate_b.clone()])) + .build(client_a.keys()) + .expect("build top shared event"); + let top_unique_a = client_a + .event_builder(Kind::from(1617), "top unique A") + .tag(Tag::custom("a", vec![coordinate_a.clone()])) + .build(client_a.keys()) + .expect("build top unique A event"); + + // Shared descendant references both tops and should survive through top_shared. + let shared_descendant = client_a + .event_builder(Kind::from(1111), "shared descendant") + .tag(Tag::custom("e", vec![top_shared.id.to_hex()])) + .tag(Tag::custom("e", vec![top_unique_a.id.to_hex()])) + .build(client_a.keys()) + .expect("build shared descendant"); + + // Unique branch descendant should be deleted with top_unique_a. + let unique_descendant = client_a + .event_builder(Kind::from(1111), "unique descendant") + .tag(Tag::custom("e", vec![top_unique_a.id.to_hex()])) + .build(client_a.keys()) + .expect("build unique descendant"); + + for event in [&top_shared, &top_unique_a, &shared_descendant, &unique_descendant] { + client_a + .send_event(event.clone()) + .await + .expect("relay should accept shared-subgraph event"); + } + + tokio::time::sleep(Duration::from_millis(350)).await; + + for (label, id) in [ + ("announcement_a", announcement_a.id), + ("announcement_b", announcement_b.id), + ("top_shared", top_shared.id), + ("top_unique_a", top_unique_a.id), + ("shared_descendant", shared_descendant.id), + ("unique_descendant", unique_descendant.id), + ] { + assert!( + client_a + .is_event_on_relay(id) + .await + .expect("query event before deletion"), + "{label} ({id}) must be served before deletion" + ); + } + + let deletion_a = build_deletion(&client_a, &[], std::slice::from_ref(&coordinate_a)); + client_a + .send_event(deletion_a) + .await + .expect("relay should accept announcement A deletion"); + + tokio::time::sleep(Duration::from_millis(700)).await; + + let announcement_a_survived = client_a + .is_event_on_relay(announcement_a.id) + .await + .expect("query announcement A after deletion"); + let announcement_b_survived = client_b + .is_event_on_relay(announcement_b.id) + .await + .expect("query announcement B after deletion"); + let top_shared_survived = client_a + .is_event_on_relay(top_shared.id) + .await + .expect("query top shared after deletion"); + let top_unique_a_survived = client_a + .is_event_on_relay(top_unique_a.id) + .await + .expect("query top unique A after deletion"); + let shared_descendant_survived = client_a + .is_event_on_relay(shared_descendant.id) + .await + .expect("query shared descendant after deletion"); + let unique_descendant_survived = client_a + .is_event_on_relay(unique_descendant.id) + .await + .expect("query unique descendant after deletion"); + + relay.stop().await; + + assert!( + !announcement_a_survived, + "announcement A must be hard-deleted" + ); + assert!(announcement_b_survived, "announcement B must survive"); + assert!( + top_shared_survived, + "top shared event must survive because anchor B remains" + ); + assert!( + !top_unique_a_survived, + "top unique A event must be deleted after A deletion" + ); + assert!( + shared_descendant_survived, + "shared descendant must survive through still-anchored top_shared path" + ); + assert!( + !unique_descendant_survived, + "unique descendant must be deleted with the A-only branch" + ); +}