From 16d14d07b023614c1da0fbb11693d131327a3532 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Thu, 6 Nov 2025 12:59:29 +0000 Subject: fix cli runs to prevent rate limiting --- .../src/specs/grasp01/event_acceptance_policy.rs | 495 ++++++++++++--------- 1 file changed, 291 insertions(+), 204 deletions(-) (limited to 'grasp-audit/src/specs') diff --git a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs index d235eb0..353d2c3 100644 --- a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs +++ b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs @@ -89,7 +89,7 @@ //! - Transitive tests verify multi-hop acceptance chains use nostr_sdk::{Event, Filter, Kind, Tag, TagKind, Timestamp}; -use crate::{AuditClient, AuditResult, TestResult}; +use crate::{AuditClient, AuditResult, TestResult, TestContext, FixtureKind}; use std::time::Duration; /// Test suite for GRASP-01 event acceptance policy @@ -139,6 +139,10 @@ impl EventAcceptancePolicyTests { /// /// Spec: Lines 3-5 of ../grasp/01.md /// Requirement: MUST accept repo announcements listing service in clone & relays tags + /// + /// **Using TestContext pattern:** + /// - In CI mode: Creates fresh repo for full isolation + /// - In Production mode: Reuses cached repo to minimize events async fn test_accept_valid_repo_announcement(client: &AuditClient) -> TestResult { TestResult::new( "accept_valid_repo_announcement", @@ -146,9 +150,12 @@ impl EventAcceptancePolicyTests { "Accept valid repository announcements with service in clone and relays tags", ) .run(|| async { - // Create a NIP-34 repository announcement event - let event = client.create_repo_announcement("accept_valid_repo_announcement").await - .map_err(|e| format!("Failed to create repository announcement: {}", e))?; + // Create TestContext for mode-aware fixture management + let ctx = TestContext::new(client); + + // Request repository fixture - behavior depends on mode + let event = ctx.get_fixture(FixtureKind::ValidRepo).await + .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; // Get relay URL for validation let relay_url = client.client().relays().await @@ -169,9 +176,7 @@ impl EventAcceptancePolicyTests { .ok_or("Missing d tag in announcement")? .to_string(); - // Send the event - let event_id = client.send_event(event.clone()).await - .map_err(|e| format!("Failed to send repository announcement to relay: {}", e))?; + let event_id = event.id; // Query back to verify it was accepted and stored let filter = Filter::new() @@ -355,6 +360,11 @@ impl EventAcceptancePolicyTests { /// /// Spec: Lines 6-7 of ../grasp/01.md /// Requirement: MUST accept repo state announcements with d, maintainers, and r tags + /// + /// **EXAMPLE: Using TestContext pattern for fixture management** + /// This test demonstrates the new TestContext pattern: + /// - In CI mode: Creates fresh repo for full isolation + /// - In Production mode: Reuses cached repo to minimize events async fn test_accept_valid_repo_state_announcement(client: &AuditClient) -> TestResult { TestResult::new( "accept_valid_repo_state_announcement", @@ -362,10 +372,14 @@ impl EventAcceptancePolicyTests { "Accept valid state announcements after repo announcement accepted", ) .run(|| async { - // First, create a repository announcement (kind 30617) by the same author - let test_name = format!("test-repo-multi-refs-{}", Timestamp::now().as_u64()); - let repo_event = client.create_repo_announcement(&test_name).await - .map_err(|e| format!("Failed to create repository announcement: {}", e))?; + // NEW: Create TestContext for mode-aware fixture management + let ctx = TestContext::new(client); + + // NEW: Request repository fixture - behavior depends on mode + // CI mode: Creates fresh repo for this test + // Production mode: Returns cached repo if available + let repo_event = ctx.get_fixture(FixtureKind::RepoState).await + .map_err(|e| format!("Test setup failed: could not get repository state fixture: {}", e))?; // Extract repo_id from the repository announcement let repo_id = repo_event.tags.iter() @@ -374,36 +388,7 @@ impl EventAcceptancePolicyTests { .ok_or("Missing d tag in repository announcement")? .to_string(); - // Note: npub not used in this test, removed unused variable - - // Create kind 30618 repository state announcement with multiple refs - // Format: ["r", "refs/heads/main", ""] - let event = client.event_builder(Kind::Custom(30618), "") - .tag(Tag::identifier(&repo_id)) - .tag(Tag::custom(TagKind::custom("refs/heads/main"), vec![ - "abc123def456789012345678901234567890abcd" - ])) - .tag(Tag::custom(TagKind::custom("refs/heads/develop"), vec![ - "def456789012345678901234567890abcdef123" - ])) - .tag(Tag::custom(TagKind::custom("refs/tags/v1.0.0"), vec![ - "123456789012345678901234567890abcdef456" - ])) - .tag(Tag::custom(TagKind::custom("HEAD"), vec![ - "ref: refs/heads/main" - ])) - .build(client.keys()) - .map_err(|e| format!("Failed to build state announcement: {}", e))?; - - let event_id = event.id; - - // Send the repo announcement event - client.send_event(repo_event.clone()).await - .map_err(|e| format!("Failed to send state announcement to relay: {}", e))?; - - // Send the state event - client.send_event(event.clone()).await - .map_err(|e| format!("Failed to send state announcement to relay: {}", e))?; + let event_id = repo_event.id; // Query back to verify it was accepted and stored let filter = Filter::new() @@ -447,7 +432,7 @@ impl EventAcceptancePolicyTests { async fn create_test_repo(client: &AuditClient, repo_id: &str) -> Result { client.create_repo_announcement(repo_id) .await - .map_err(|e| e.to_string()) + .map_err(|e| format!("Test setup failed: could not create test repository: {}", e)) } /// Create an issue (kind 1621) that references a repository @@ -458,7 +443,7 @@ impl EventAcceptancePolicyTests { issue_title: &str, ) -> Result { client.create_issue(repo_event, issue_title, "issue content", vec![]) - .map_err(|e| e.to_string()) + .map_err(|e| format!("Test setup failed: could not create test issue: {}", e)) } /// Create a NIP-22 comment (kind 1111) for an event @@ -469,7 +454,7 @@ impl EventAcceptancePolicyTests { content: &str, ) -> Result { client.create_comment(event, content, vec![]) - .map_err(|e| e.to_string()) + .map_err(|e| format!("Test setup failed: could not create test comment: {}", e)) } /// Send event and verify it was accepted (stored by relay) @@ -480,13 +465,14 @@ impl EventAcceptancePolicyTests { ) -> Result<(), String> { let event_id = event.id; - client.send_event(event).await?; + client.send_event(event).await + .map_err(|e| format!("Failed to send event to relay: {}", e))?; tokio::time::sleep(Duration::from_millis(100)).await; let filter = Filter::new().id(event_id); let events = client.query(filter).await - .map_err(|e| e.to_string())?; + .map_err(|e| format!("Failed to query relay for verification: {}", e))?; if events.is_empty() { return Err(format!("Event should be accepted: {}", description)); @@ -504,13 +490,13 @@ impl EventAcceptancePolicyTests { let event_id = event.id; client.send_event(event).await - .map_err(|e| e.to_string())?; + .map_err(|e| format!("Failed to send event to relay: {}", e))?; tokio::time::sleep(Duration::from_millis(100)).await; let filter = Filter::new().id(event_id); let events = client.query(filter).await - .map_err(|e| e.to_string())?; + .map_err(|e| format!("Failed to query relay for verification: {}", e))?; if !events.is_empty() { return Err(format!("Event should be rejected: {}", description)); @@ -524,6 +510,9 @@ impl EventAcceptancePolicyTests { // ============================================================ /// Test 1.1: Issue referencing repo via `a` tag should be accepted + /// + /// **EXAMPLE: Using TestContext for prerequisite events** + /// Demonstrates how TestContext simplifies test setup while supporting dual modes async fn test_accept_issue_via_a_tag(client: &AuditClient) -> TestResult { TestResult::new( "accept_issue_via_a_tag", @@ -531,11 +520,14 @@ impl EventAcceptancePolicyTests { "Accept issue referencing repo via 'a' tag", ) .run(|| async { - // 1. Create and send repo announcement - let repo = Self::create_test_repo(client, "test-repo-1").await?; - Self::send_and_verify_accepted(client, repo.clone(), "repository announcement").await?; + // NEW: Create TestContext + let ctx = TestContext::new(client); + + // NEW: Get repository fixture (mode-aware) + let repo = ctx.get_fixture(FixtureKind::ValidRepo).await + .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; - // 2. Create issue that references the repo (uses create_issue_for_repo helper) + // 2. Create issue that references the repo let issue = Self::create_issue_for_repo(client, &repo, "Test Issue 1")?; // 3. Send issue and verify it's accepted @@ -547,6 +539,10 @@ impl EventAcceptancePolicyTests { } /// Test 1.2: NIP-22 comment with root `A` tag referencing repo should be accepted + /// + /// **Using TestContext pattern:** + /// - In CI mode: Creates fresh repo for full isolation + /// - In Production mode: Reuses cached repo to minimize events async fn test_accept_comment_via_A_tag(client: &AuditClient) -> TestResult { TestResult::new( "accept_comment_via_A_tag", @@ -554,16 +550,19 @@ impl EventAcceptancePolicyTests { "Accept NIP-22 comment with root 'A' tag referencing repo", ) .run(|| async { - // 1. Create and send repo announcement - let repo = Self::create_test_repo(client, "test-repo-2").await?; - Self::send_and_verify_accepted(client, repo.clone(), "repository announcement").await?; + // Create TestContext + let ctx = TestContext::new(client); - // 2. Extract repo_id and create `A` tag manually + // Get repository fixture (mode-aware) + let repo = ctx.get_fixture(FixtureKind::ValidRepo).await + .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; + + // Extract repo_id and create `A` tag manually let repo_id = Self::extract_d_tag(&repo) .ok_or("Failed to extract repo_id from repo event")?; let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id); - // 3. Create comment with `A` tag (root reference to repo) + // Create comment with `A` tag (root reference to repo) let tags = vec![ Tag::custom(TagKind::custom("A"), vec![a_tag_value.clone(), "".to_string(), "root".to_string()]), Tag::custom(TagKind::custom("K"), vec!["30617".to_string()]), @@ -576,7 +575,7 @@ impl EventAcceptancePolicyTests { .build(client.keys()) .map_err(|e| format!("Failed to build comment: {}", e))?; - // 4. Send comment and verify it's accepted + // Send comment and verify it's accepted Self::send_and_verify_accepted(client, comment, "comment with 'A' tag to repo").await?; Ok(()) @@ -585,6 +584,10 @@ impl EventAcceptancePolicyTests { } /// Test 1.3: Kind 1 text note quoting repo via `q` tag should be accepted + /// + /// **Using TestContext pattern:** + /// - In CI mode: Creates fresh repo for full isolation + /// - In Production mode: Reuses cached repo to minimize events async fn test_accept_kind1_via_q_tag(client: &AuditClient) -> TestResult { TestResult::new( "accept_kind1_via_q_tag", @@ -592,16 +595,19 @@ impl EventAcceptancePolicyTests { "Accept kind 1 note quoting repo via 'q' tag", ) .run(|| async { - // 1. Create and send repo announcement - let repo = Self::create_test_repo(client, "test-repo-3").await?; - Self::send_and_verify_accepted(client, repo.clone(), "repository announcement").await?; + // Create TestContext + let ctx = TestContext::new(client); + + // Get repository fixture (mode-aware) + let repo = ctx.get_fixture(FixtureKind::ValidRepo).await + .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; - // 2. Extract repo_id and create `q` tag + // Extract repo_id and create `q` tag let repo_id = Self::extract_d_tag(&repo) .ok_or("Failed to extract repo_id from repo event")?; let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id); - // 3. Create kind 1 note with `q` tag (quote reference to repo) + // Create kind 1 note with `q` tag (quote reference to repo) let tags = vec![ Tag::custom(TagKind::custom("q"), vec![a_tag_value]), ]; @@ -612,7 +618,7 @@ impl EventAcceptancePolicyTests { .build(client.keys()) .map_err(|e| format!("Failed to build note: {}", e))?; - // 4. Send note and verify it's accepted + // Send note and verify it's accepted Self::send_and_verify_accepted(client, note, "kind 1 with 'q' tag to repo").await?; Ok(()) @@ -625,6 +631,10 @@ impl EventAcceptancePolicyTests { // ============================================================ /// Test 2.1: Issue quoting another accepted issue should be accepted (transitive) + /// + /// **Using TestContext pattern:** + /// - In CI mode: Creates fresh repo+issue for full isolation + /// - In Production mode: Reuses cached repo+issue to minimize events async fn test_accept_issue_quoting_issue_via_q(client: &AuditClient) -> TestResult { TestResult::new( "accept_issue_quoting_issue_via_q", @@ -632,31 +642,43 @@ impl EventAcceptancePolicyTests { "Accept issue quoting accepted issue (transitive)", ) .run(|| async { - - // 1. Create and send Repo A - let repo_a = Self::create_test_repo(client, "repo-a").await?; - Self::send_and_verify_accepted(client, repo_a.clone(), "repo A").await?; - - // 2. Create and send Issue A (references repo A, so it's accepted) - let issue_a = Self::create_issue_for_repo(client, &repo_a, "Issue A")?; - Self::send_and_verify_accepted(client, issue_a.clone(), "issue A").await?; - - // 3. Create Repo B but DON'T send it (unaccepted) - just for creating Issue B - let repo_b = Self::create_test_repo(client, "repo-b").await?; - - // 4. Create Issue B that: - // - References unaccepted Repo B (would normally be rejected) - // - BUT also quotes accepted Issue A via 'q' tag (should make it accepted) - let additional_tags = vec![ - // Quote to accepted Issue A (this makes it transitive) - Tag::custom(TagKind::custom("q"), vec![issue_a.id.to_hex()]), - ]; - - let issue_b = client - .create_issue(&repo_b, "Issue B", "issue content", additional_tags) - .map_err(|e| format!("Failed to build issue B: {}", e))?; - - // 5. Send Issue B and verify it's ACCEPTED (via transitive quote to Issue A) + // Create TestContext + let ctx = TestContext::new(client); + + // Get repo with issue fixture (mode-aware) + let repo_a = ctx.get_fixture(FixtureKind::RepoWithIssue).await + .map_err(|e| format!("Test setup failed: could not get repo with issue fixture: {}", e))?; + + // Extract the issue from the repo_a event (it's stored as the first 'e' tag) + let issue_a_id = repo_a.tags.iter() + .find(|t| t.kind() == TagKind::e()) + .and_then(|t| t.content()) + .ok_or("Missing issue reference in RepoWithIssue fixture")?; + + // Query to get the actual issue event + let filter = Filter::new().id( + nostr_sdk::EventId::from_hex(issue_a_id) + .map_err(|e| format!("Invalid issue ID: {}", e))? + ); + let issues = client.query(filter).await + .map_err(|e| format!("Failed to query issue: {}", e))?; + let issue_a = issues.first() + .ok_or("Issue not found")? + .clone(); + + // Create Repo B but DON'T send it (unaccepted) - just for creating Issue B + let repo_b = Self::create_test_repo(client, "repo-b").await?; + + // Create Issue B that quotes accepted Issue A via 'q' tag (should make it accepted) + let additional_tags = vec![ + Tag::custom(TagKind::custom("q"), vec![issue_a.id.to_hex()]), + ]; + + let issue_b = client + .create_issue(&repo_b, "Issue B", "issue content", additional_tags) + .map_err(|e| format!("Failed to build issue B: {}", e))?; + + // Send Issue B and verify it's ACCEPTED (via transitive quote to Issue A) Self::send_and_verify_accepted(client, issue_b, "issue B quoting accepted issue A").await?; Ok(()) @@ -665,6 +687,10 @@ impl EventAcceptancePolicyTests { } /// Test 2.2: NIP-22 comment with root 'E' tag to accepted issue should be accepted + /// + /// **Using TestContext pattern:** + /// - In CI mode: Creates fresh repo+issue for full isolation + /// - In Production mode: Reuses cached repo+issue to minimize events async fn test_accept_comment_via_E_tag(client: &AuditClient) -> TestResult { TestResult::new( "accept_comment_via_E_tag", @@ -672,19 +698,34 @@ impl EventAcceptancePolicyTests { "Accept NIP-22 comment with root 'E' tag to accepted issue", ) .run(|| async { - - // 1. Create and send repo - let repo = Self::create_test_repo(client, "repo-comment").await?; - Self::send_and_verify_accepted(client, repo.clone(), "repo").await?; - - // 2. Create and send issue (references repo, so it's accepted) - let issue = Self::create_issue_for_repo(client, &repo, "Issue for comment")?; - Self::send_and_verify_accepted(client, issue.clone(), "issue").await?; - - // 3. Create comment using the helper (which adds NIP-22 tags including 'E') - let comment = Self::create_comment_for_event(client, &issue, "Comment content")?; - - // 4. Send comment and verify it's accepted (via E tag to accepted issue) + // Create TestContext + let ctx = TestContext::new(client); + + // Get repo with issue fixture (mode-aware) + let repo = ctx.get_fixture(FixtureKind::RepoWithIssue).await + .map_err(|e| format!("Test setup failed: could not get repo with issue fixture: {}", e))?; + + // Extract the issue from the repo event (it's stored as the first 'e' tag) + let issue_id = repo.tags.iter() + .find(|t| t.kind() == TagKind::e()) + .and_then(|t| t.content()) + .ok_or("Missing issue reference in RepoWithIssue fixture")?; + + // Query to get the actual issue event + let filter = Filter::new().id( + nostr_sdk::EventId::from_hex(issue_id) + .map_err(|e| format!("Invalid issue ID: {}", e))? + ); + let issues = client.query(filter).await + .map_err(|e| format!("Failed to query issue: {}", e))?; + let issue = issues.first() + .ok_or("Issue not found")? + .clone(); + + // Create comment using the helper (which adds NIP-22 tags including 'E') + let comment = Self::create_comment_for_event(client, &issue, "Comment content")?; + + // Send comment and verify it's accepted (via E tag to accepted issue) Self::send_and_verify_accepted(client, comment, "comment with E tag to accepted issue").await?; Ok(()) @@ -693,6 +734,10 @@ impl EventAcceptancePolicyTests { } /// Test 2.3: Kind 1 note with 'e' tag reply to accepted kind 1 should be accepted + /// + /// **Using TestContext pattern:** + /// - In CI mode: Creates fresh repo for full isolation + /// - In Production mode: Reuses cached repo to minimize events async fn test_accept_kind1_via_e_tag(client: &AuditClient) -> TestResult { TestResult::new( "accept_kind1_via_e_tag", @@ -700,32 +745,34 @@ impl EventAcceptancePolicyTests { "Accept kind 1 reply via 'e' tag to accepted kind 1", ) .run(|| async { - - // 1. Create and send repo - let repo = Self::create_test_repo(client, "repo-notes").await?; - Self::send_and_verify_accepted(client, repo.clone(), "repo").await?; - - // 2. Create Kind 1 A that quotes the repo (makes it accepted) - let repo_id = Self::extract_d_tag(&repo) - .ok_or("Failed to extract repo_id")?; - let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id); - - let kind1_a = client - .event_builder(Kind::TextNote, "Note A about repo") - .tags(vec![Tag::custom(TagKind::custom("q"), vec![a_tag_value])]) - .build(client.keys()) - .map_err(|e| format!("Failed to build kind1 A: {}", e))?; - - Self::send_and_verify_accepted(client, kind1_a.clone(), "kind 1 A quoting repo").await?; - - // 3. Create Kind 1 B that replies to Kind 1 A via 'e' tag - let kind1_b = client - .event_builder(Kind::TextNote, "Reply to Note A") - .tags(vec![Tag::event(kind1_a.id)]) - .build(client.keys()) - .map_err(|e| format!("Failed to build kind1 B: {}", e))?; - - // 4. Send Kind 1 B and verify it's accepted (via 'e' tag to accepted kind 1 A) + // Create TestContext + let ctx = TestContext::new(client); + + // Get repository fixture (mode-aware) + let repo = ctx.get_fixture(FixtureKind::ValidRepo).await + .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; + + // Create Kind 1 A that quotes the repo (makes it accepted) + let repo_id = Self::extract_d_tag(&repo) + .ok_or("Failed to extract repo_id")?; + let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id); + + let kind1_a = client + .event_builder(Kind::TextNote, "Note A about repo") + .tags(vec![Tag::custom(TagKind::custom("q"), vec![a_tag_value])]) + .build(client.keys()) + .map_err(|e| format!("Failed to build kind1 A: {}", e))?; + + Self::send_and_verify_accepted(client, kind1_a.clone(), "kind 1 A quoting repo").await?; + + // Create Kind 1 B that replies to Kind 1 A via 'e' tag + let kind1_b = client + .event_builder(Kind::TextNote, "Reply to Note A") + .tags(vec![Tag::event(kind1_a.id)]) + .build(client.keys()) + .map_err(|e| format!("Failed to build kind1 B: {}", e))?; + + // Send Kind 1 B and verify it's accepted (via 'e' tag to accepted kind 1 A) Self::send_and_verify_accepted(client, kind1_b, "kind 1 B replying to accepted kind 1 A").await?; Ok(()) @@ -738,6 +785,10 @@ impl EventAcceptancePolicyTests { // ============================================================ /// Test 3.1: Kind 1 note should be accepted when referenced by an accepted issue (forward ref) + /// + /// **Using TestContext pattern:** + /// - In CI mode: Creates fresh repo for full isolation + /// - In Production mode: Reuses cached repo to minimize events async fn test_accept_kind1_referenced_in_issue(client: &AuditClient) -> TestResult { TestResult::new( "accept_kind1_referenced_in_issue", @@ -745,37 +796,39 @@ impl EventAcceptancePolicyTests { "Accept kind 1 referenced in accepted issue (forward ref)", ) .run(|| async { - - // 1. Create and send repo (this establishes the accepted context) - let repo = Self::create_test_repo(client, "repo-fwd-1").await?; - Self::send_and_verify_accepted(client, repo.clone(), "repo").await?; - - // 2. Create Kind 1 note locally but DON'T send it yet - let kind1_note = client - .event_builder(Kind::TextNote, "Note to be referenced") - .build(client.keys()) - .map_err(|e| format!("Failed to build kind1: {}", e))?; - - // 3. Create and send issue that QUOTES the unsent Kind 1 note - let issue_tags = vec![ - // Reference to accepted repo - Tag::custom(TagKind::custom("a"), vec![ - format!("30617:{}:{}", repo.pubkey, Self::extract_d_tag(&repo).unwrap()) - ]), - Tag::custom(TagKind::custom("subject"), vec!["Issue referencing kind1".to_string()]), - // Quote the Kind 1 that hasn't been sent yet - Tag::custom(TagKind::custom("q"), vec![kind1_note.id.to_hex()]), - ]; - - let issue = client - .event_builder(Kind::Custom(1621), "issue content") - .tags(issue_tags) - .build(client.keys()) - .map_err(|e| format!("Failed to build issue: {}", e))?; - - Self::send_and_verify_accepted(client, issue, "issue quoting unsent kind1").await?; - - // 4. NOW send the Kind 1 note - should be accepted because accepted issue quotes it + // Create TestContext + let ctx = TestContext::new(client); + + // Get repository fixture (mode-aware) + let repo = ctx.get_fixture(FixtureKind::ValidRepo).await + .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; + + // Create Kind 1 note locally but DON'T send it yet + let kind1_note = client + .event_builder(Kind::TextNote, "Note to be referenced") + .build(client.keys()) + .map_err(|e| format!("Failed to build kind1: {}", e))?; + + // Create and send issue that QUOTES the unsent Kind 1 note + let issue_tags = vec![ + // Reference to accepted repo + Tag::custom(TagKind::custom("a"), vec![ + format!("30617:{}:{}", repo.pubkey, Self::extract_d_tag(&repo).unwrap()) + ]), + Tag::custom(TagKind::custom("subject"), vec!["Issue referencing kind1".to_string()]), + // Quote the Kind 1 that hasn't been sent yet + Tag::custom(TagKind::custom("q"), vec![kind1_note.id.to_hex()]), + ]; + + let issue = client + .event_builder(Kind::Custom(1621), "issue content") + .tags(issue_tags) + .build(client.keys()) + .map_err(|e| format!("Failed to build issue: {}", e))?; + + Self::send_and_verify_accepted(client, issue, "issue quoting unsent kind1").await?; + + // NOW send the Kind 1 note - should be accepted because accepted issue quotes it Self::send_and_verify_accepted(client, kind1_note, "kind1 note referenced by accepted issue").await?; Ok(()) @@ -784,6 +837,10 @@ impl EventAcceptancePolicyTests { } /// Test 3.2: Comment should be accepted when referenced by another accepted comment (forward ref) + /// + /// **Using TestContext pattern:** + /// - In CI mode: Creates fresh repo+issue for full isolation + /// - In Production mode: Reuses cached repo+issue to minimize events async fn test_accept_comment_referenced_in_comment(client: &AuditClient) -> TestResult { TestResult::new( "accept_comment_referenced_in_comment", @@ -791,38 +848,53 @@ impl EventAcceptancePolicyTests { "Accept comment referenced in another accepted comment (forward ref)", ) .run(|| async { - - // 1. Create and send repo - let repo = Self::create_test_repo(client, "repo-fwd-2").await?; - Self::send_and_verify_accepted(client, repo.clone(), "repo").await?; - - // 2. Create and send issue (references repo, so it's accepted) - let issue = Self::create_issue_for_repo(client, &repo, "Issue for comments")?; - Self::send_and_verify_accepted(client, issue.clone(), "issue").await?; - - // 3. Create Comment A locally but DON'T send it yet - let comment_a = Self::create_comment_for_event(client, &issue, "Comment A")?; - - // 4. Create and send Comment B that quotes Comment A (which hasn't been sent) - let comment_b_tags = vec![ - // NIP-22 tags for the original issue - Tag::custom(TagKind::custom("E"), vec![issue.id.to_hex(), "".to_string(), "root".to_string()]), - Tag::event(issue.id), - Tag::custom(TagKind::custom("K"), vec![issue.kind.as_u16().to_string()]), - Tag::public_key(issue.pubkey), - // Quote Comment A which hasn't been sent yet - Tag::custom(TagKind::custom("q"), vec![comment_a.id.to_hex()]), - ]; - - let comment_b = client - .event_builder(Kind::Custom(1111), "Comment B quoting Comment A") - .tags(comment_b_tags) - .build(client.keys()) - .map_err(|e| format!("Failed to build comment B: {}", e))?; - - Self::send_and_verify_accepted(client, comment_b, "comment B quoting unsent comment A").await?; - - // 5. NOW send Comment A - should be accepted because accepted Comment B quotes it + // Create TestContext + let ctx = TestContext::new(client); + + // Get repo with issue fixture (mode-aware) + let repo = ctx.get_fixture(FixtureKind::RepoWithIssue).await + .map_err(|e| format!("Test setup failed: could not get repo with issue fixture: {}", e))?; + + // Extract the issue from the repo event (it's stored as the first 'e' tag) + let issue_id = repo.tags.iter() + .find(|t| t.kind() == TagKind::e()) + .and_then(|t| t.content()) + .ok_or("Missing issue reference in RepoWithIssue fixture")?; + + // Query to get the actual issue event + let filter = Filter::new().id( + nostr_sdk::EventId::from_hex(issue_id) + .map_err(|e| format!("Invalid issue ID: {}", e))? + ); + let issues = client.query(filter).await + .map_err(|e| format!("Failed to query issue: {}", e))?; + let issue = issues.first() + .ok_or("Issue not found")? + .clone(); + + // Create Comment A locally but DON'T send it yet + let comment_a = Self::create_comment_for_event(client, &issue, "Comment A")?; + + // Create and send Comment B that quotes Comment A (which hasn't been sent) + let comment_b_tags = vec![ + // NIP-22 tags for the original issue + Tag::custom(TagKind::custom("E"), vec![issue.id.to_hex(), "".to_string(), "root".to_string()]), + Tag::event(issue.id), + Tag::custom(TagKind::custom("K"), vec![issue.kind.as_u16().to_string()]), + Tag::public_key(issue.pubkey), + // Quote Comment A which hasn't been sent yet + Tag::custom(TagKind::custom("q"), vec![comment_a.id.to_hex()]), + ]; + + let comment_b = client + .event_builder(Kind::Custom(1111), "Comment B quoting Comment A") + .tags(comment_b_tags) + .build(client.keys()) + .map_err(|e| format!("Failed to build comment B: {}", e))?; + + Self::send_and_verify_accepted(client, comment_b, "comment B quoting unsent comment A").await?; + + // NOW send Comment A - should be accepted because accepted Comment B quotes it Self::send_and_verify_accepted(client, comment_a, "comment A referenced by accepted comment B").await?; Ok(()) @@ -831,6 +903,10 @@ impl EventAcceptancePolicyTests { } /// Test 3.3: Kind 1 note should be accepted when referenced by another accepted kind 1 (forward ref) + /// + /// **Using TestContext pattern:** + /// - In CI mode: Creates fresh repo for full isolation + /// - In Production mode: Reuses cached repo to minimize events async fn test_accept_kind1_referenced_in_kind1(client: &AuditClient) -> TestResult { TestResult::new( "accept_kind1_referenced_in_kind1", @@ -838,17 +914,20 @@ impl EventAcceptancePolicyTests { "Accept kind 1 referenced in another accepted kind 1 (forward ref)", ) .run(|| async { - // 1. Create and send repo - let repo = Self::create_test_repo(client, "repo-fwd-3").await?; - Self::send_and_verify_accepted(client, repo.clone(), "repo").await?; + // Create TestContext + let ctx = TestContext::new(client); - // 2. Create Kind 1 A locally but DON'T send it yet + // Get repository fixture (mode-aware) + let repo = ctx.get_fixture(FixtureKind::ValidRepo).await + .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; + + // Create Kind 1 A locally but DON'T send it yet let kind1_a = client .event_builder(Kind::TextNote, "Note A to be referenced") .build(client.keys()) .map_err(|e| format!("Failed to build kind1 A: {}", e))?; - // 3. Create and send Kind 1 B that: + // Create and send Kind 1 B that: // - Quotes the repo (makes it accepted) // - Mentions Kind 1 A via 'e' tag (which hasn't been sent yet) let repo_id = Self::extract_d_tag(&repo) @@ -866,7 +945,7 @@ impl EventAcceptancePolicyTests { Self::send_and_verify_accepted(client, kind1_b, "kind1 B mentioning unsent kind1 A").await?; - // 4. NOW send Kind 1 A - should be accepted because accepted Kind 1 B mentions it + // NOW send Kind 1 A - should be accepted because accepted Kind 1 B mentions it Self::send_and_verify_accepted(client, kind1_a, "kind1 A referenced by accepted kind1 B").await?; Ok(()) @@ -923,6 +1002,11 @@ impl EventAcceptancePolicyTests { } /// Test 4.3: Comment quoting unaccepted repo should be rejected + /// + /// **Using TestContext pattern:** + /// - In CI mode: Creates fresh accepted repo for full isolation + /// - In Production mode: Reuses cached accepted repo to minimize events + /// - Note: Unaccepted repo B is always created fresh (not cached) since it must remain unaccepted async fn test_reject_comment_quoting_other_repo(client: &AuditClient) -> TestResult { TestResult::new( "reject_comment_quoting_other_repo", @@ -930,19 +1014,22 @@ impl EventAcceptancePolicyTests { "Reject comment quoting unaccepted repo", ) .run(|| async { - // 1. Create and send Repo A (this one IS accepted) - let repo_a = Self::create_test_repo(client, "accepted-repo-a").await?; - Self::send_and_verify_accepted(client, repo_a.clone(), "repo A").await?; + // Create TestContext + let ctx = TestContext::new(client); + + // Get accepted repo A fixture (mode-aware) + let _repo_a = ctx.get_fixture(FixtureKind::ValidRepo).await + .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; - // 2. Create Repo B but DON'T send it (unaccepted) + // Create Repo B but DON'T send it (unaccepted) let repo_b = Self::create_test_repo(client, "unaccepted-repo-b").await?; - // 3. Extract repo_b info and create comment that quotes repo B (not repo A) + // Extract repo_b info and create comment that quotes repo B (not repo A) let repo_b_id = Self::extract_d_tag(&repo_b) .ok_or("Failed to extract repo_b id")?; let repo_b_a_tag = format!("30617:{}:{}", repo_b.pubkey, repo_b_id); - // 4. Create comment that references ONLY repo B (unaccepted) + // Create comment that references ONLY repo B (unaccepted) let tags = vec![ Tag::custom(TagKind::custom("A"), vec![repo_b_a_tag, "".to_string(), "root".to_string()]), Tag::custom(TagKind::custom("K"), vec!["30617".to_string()]), @@ -955,7 +1042,7 @@ impl EventAcceptancePolicyTests { .build(client.keys()) .map_err(|e| format!("Failed to build comment: {}", e))?; - // 5. Send comment and verify it's REJECTED (only references unaccepted repo B) + // Send comment and verify it's REJECTED (only references unaccepted repo B) Self::send_and_verify_rejected(client, comment, "comment quoting only unaccepted repo").await?; Ok(()) -- cgit v1.2.3