upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 21:22:57 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 21:22:57 +0000
commitbfbdd1c2fe2a556af099d79ea25d1b9bd1d3fd2c (patch)
tree1d99708fac8f8d40ae6968f38299689ae8b739ca
parent80053758daf365896cdfd2b9a40496adad229ce9 (diff)
fixtures dependancy overhaul
-rw-r--r--grasp-audit/src/fixtures.rs565
1 files changed, 245 insertions, 320 deletions
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs
index fef5c5c..d894ed8 100644
--- a/grasp-audit/src/fixtures.rs
+++ b/grasp-audit/src/fixtures.rs
@@ -217,13 +217,61 @@ pub enum FixtureKind {
217 /// 3. **Verified**: Confirms events accepted by relay 217 /// 3. **Verified**: Confirms events accepted by relay
218 /// 4. **DataPushed**: Clones repo, creates maintainer deterministic commit, pushes to relay 218 /// 4. **DataPushed**: Clones repo, creates maintainer deterministic commit, pushes to relay
219 /// 219 ///
220 /// - Requires ValidRepo (owner's announcement lists maintainer) 220 /// - Requires OwnerStateDataPushed (owner's data already pushed to git)
221 /// - State event signed by maintainer keys (`client.maintainer_keys()`) 221 /// - State event signed by maintainer keys (`client.maintainer_keys()`)
222 /// - Points to MAINTAINER_DETERMINISTIC_COMMIT_HASH 222 /// - Points to MAINTAINER_DETERMINISTIC_COMMIT_HASH
223 /// - Git push verified to succeed (maintainer's state event authorizes the commit) 223 /// - Git push verified to succeed (force push with maintainer's state event authorizes the commit)
224 MaintainerStateDataPushed, 224 MaintainerStateDataPushed,
225} 225}
226 226
227impl FixtureKind {
228 /// Get the fixture dependencies that must be ensured before this one
229 ///
230 /// Dependencies are processed in order and cached, so if a fixture
231 /// depends on another that's already been created, it won't be recreated.
232 pub fn dependencies(&self) -> Vec<FixtureKind> {
233 match self {
234 // Base fixtures - no dependencies
235 Self::ValidRepo => vec![],
236
237 // Fixtures that depend on ValidRepo
238 Self::RepoWithIssue => vec![Self::ValidRepo],
239 Self::RepoState => vec![Self::ValidRepo],
240 Self::MaintainerAnnouncement => vec![Self::ValidRepo],
241 Self::MaintainerState => vec![Self::ValidRepo],
242 Self::RecursiveMaintainerAnnouncement => vec![Self::ValidRepo],
243 Self::RecursiveMaintainerState => vec![Self::ValidRepo],
244 Self::RecursiveMaintainerRepoAndState => vec![Self::ValidRepo],
245 Self::PREvent => vec![Self::ValidRepo],
246 Self::OwnerStateDataPushed => vec![Self::ValidRepo],
247
248 // Fixtures that depend on RepoWithIssue
249 Self::RepoWithComment => vec![Self::RepoWithIssue],
250
251 // MaintainerStateDataPushed depends on OwnerStateDataPushed
252 // (maintainer force-pushes over owner's data)
253 Self::MaintainerStateDataPushed => vec![Self::OwnerStateDataPushed],
254 }
255 }
256
257 /// Whether this fixture sends its own events to the relay
258 ///
259 /// Some fixtures (like DataPushed variants) handle event sending internally
260 /// as part of their build process. For these, the generic ensure_fixture
261 /// should NOT send the event again.
262 pub fn sends_own_events(&self) -> bool {
263 match self {
264 // These fixtures send events and push git data internally
265 Self::OwnerStateDataPushed => true,
266 Self::MaintainerStateDataPushed => true,
267 // RecursiveMaintainerRepoAndState sends multiple events internally
268 Self::RecursiveMaintainerRepoAndState => true,
269 // All other fixtures return a single event for the caller to send
270 _ => false,
271 }
272 }
273}
274
227/// Context mode for fixture management 275/// Context mode for fixture management
228#[derive(Debug, Clone, Copy, PartialEq, Eq)] 276#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub enum ContextMode { 277pub enum ContextMode {
@@ -313,10 +361,11 @@ impl<'a> TestContext<'a> {
313 361
314 /// Get a fixture, creating it if needed based on mode 362 /// Get a fixture, creating it if needed based on mode
315 /// 363 ///
316 /// # Behavior 364 /// This is an alias for `ensure_fixture` - the core method for fixture management.
317 /// 365 /// It automatically handles:
318 /// - **Isolated mode**: Always creates a fresh fixture 366 /// - Mode-aware caching (Isolated vs Shared)
319 /// - **Shared mode**: Returns cached fixture or creates and caches if not present 367 /// - Dependency resolution
368 /// - Event sending
320 /// 369 ///
321 /// # Example 370 /// # Example
322 /// 371 ///
@@ -328,10 +377,7 @@ impl<'a> TestContext<'a> {
328 /// # } 377 /// # }
329 /// ``` 378 /// ```
330 pub async fn get_fixture(&self, kind: FixtureKind) -> Result<Event> { 379 pub async fn get_fixture(&self, kind: FixtureKind) -> Result<Event> {
331 match self.mode { 380 self.ensure_fixture(kind).await
332 ContextMode::Isolated => self.create_fresh(kind).await,
333 ContextMode::Shared => self.get_or_create_shared(kind).await,
334 }
335 } 381 }
336 382
337 /// Get the underlying client for direct access 383 /// Get the underlying client for direct access
@@ -347,277 +393,208 @@ impl<'a> TestContext<'a> {
347 self.mode 393 self.mode
348 } 394 }
349 395
350 /// Build a fixture event WITHOUT publishing it to the relay. 396 // ============================================================
351 /// 397 // Cache Helper Methods
352 /// This is useful for tests that need to get a fixture's event ID before 398 // ============================================================
353 /// actually publishing it. For example, testing refs/nostr/<event-id>
354 /// behavior before the corresponding event exists on the relay.
355 ///
356 /// Note: This may still create and publish dependencies (e.g., ValidRepo
357 /// will be created/published if PREvent needs it), but the requested
358 /// fixture itself will NOT be published.
359 ///
360 /// # Example
361 ///
362 /// ```no_run
363 /// # use grasp_audit::*;
364 /// # async fn example(ctx: &TestContext<'_>) -> anyhow::Result<()> {
365 /// // Build PR event to get its ID without publishing
366 /// let pr_event = ctx.build_fixture_only(FixtureKind::PREvent).await?;
367 /// let pr_event_id = pr_event.id.to_hex();
368 ///
369 /// // Now push to refs/nostr/<pr_event_id> before event exists
370 /// // ... git push ...
371 ///
372 /// // Later, publish the PR event when ready
373 /// ctx.client().send_event(pr_event).await?;
374 /// # Ok(())
375 /// # }
376 /// ```
377 pub async fn build_fixture_only(&self, kind: FixtureKind) -> Result<Event> {
378 self.build_fixture(kind).await
379 }
380 399
381 /// Create a fresh fixture (always creates new) 400 /// Get a cached fixture if it exists
382 async fn create_fresh(&self, kind: FixtureKind) -> Result<Event> { 401 fn get_cached(&self, kind: FixtureKind) -> Option<Event> {
383 let event = self
384 .build_fixture(kind)
385 .await
386 .with_context(|| format!("Failed to build {:?} fixture", kind))?;
387
388 self.client
389 .send_event(event.clone())
390 .await
391 .with_context(|| format!("Failed to send {:?} fixture event to relay", kind))?;
392
393 Ok(event)
394 }
395
396 /// Get or create a shared fixture (caches for reuse)
397 ///
398 /// Uses the client's fixture cache to ensure fixtures are reused across
399 /// all TestContext instances in Production mode.
400 async fn get_or_create_shared(&self, kind: FixtureKind) -> Result<Event> {
401 // Check client's cache first (shared across all TestContext instances using same client)
402 {
403 let cache = self.client.fixture_cache().lock().unwrap();
404 if let Some(event) = cache.get(&kind) {
405 tracing::debug!("get_or_create_shared({:?}) found in client cache", kind);
406 return Ok(event.clone());
407 }
408 }
409
410 // Check relay connection before attempting to build
411 let is_connected = self.client.is_connected().await;
412 if !is_connected {
413 return Err(anyhow::anyhow!(
414 "Relay connection lost before building {:?} fixture (shared cache mode)",
415 kind
416 ));
417 }
418
419 // Not in cache, create it
420 let event = self
421 .build_fixture(kind)
422 .await
423 .with_context(|| format!("Failed to build {:?} fixture for shared cache", kind))?;
424
425 self.client
426 .send_event(event.clone())
427 .await
428 .with_context(|| {
429 format!(
430 "Failed to send {:?} fixture event to relay (shared cache)",
431 kind
432 )
433 })?;
434
435 // Store in client's cache (shared across all TestContext instances using same client)
436 {
437 let mut cache = self.client.fixture_cache().lock().unwrap();
438 cache.insert(kind, event.clone());
439 tracing::debug!(
440 "get_or_create_shared({:?}) stored in client cache ({} entries)",
441 kind,
442 cache.len()
443 );
444 }
445
446 Ok(event)
447 }
448
449 /// Get or create a ValidRepo, with mode-aware caching.
450 /// This is a helper method that avoids async recursion by not going
451 /// through get_fixture. It handles the repo specifically.
452 ///
453 /// Caching strategy:
454 /// - **Isolated mode**: Uses per-TestContext local_cache to maintain fixture
455 /// dependencies within a single test, while ensuring isolation between tests.
456 /// - **Shared mode**: Uses client's fixture_cache for cross-test reuse.
457 async fn get_or_create_repo(&self) -> Result<Event> {
458 // Check the appropriate cache based on mode
459 match self.mode { 402 match self.mode {
460 ContextMode::Isolated => { 403 ContextMode::Isolated => {
461 // In Isolated mode, use local TestContext cache
462 // This ensures fixture dependencies work within a single test
463 let cache = self.local_cache.lock().unwrap(); 404 let cache = self.local_cache.lock().unwrap();
464 if let Some(event) = cache.get(&FixtureKind::ValidRepo) { 405 cache.get(&kind).cloned()
465 tracing::debug!("get_or_create_repo() found in local cache (Isolated mode)");
466 return Ok(event.clone());
467 }
468 } 406 }
469 ContextMode::Shared => { 407 ContextMode::Shared => {
470 // In Shared mode, use client's cache for cross-test sharing
471 let cache = self.client.fixture_cache().lock().unwrap(); 408 let cache = self.client.fixture_cache().lock().unwrap();
472 if let Some(event) = cache.get(&FixtureKind::ValidRepo) { 409 cache.get(&kind).cloned()
473 tracing::debug!("get_or_create_repo() found in client cache (Shared mode)");
474 return Ok(event.clone());
475 }
476 } 410 }
477 } 411 }
412 }
478 413
479 // Check relay connection before creating repo 414 /// Store a fixture in the cache
480 let is_connected = self.client.is_connected().await; 415 fn store_cached(&self, kind: FixtureKind, event: Event) {
481 if !is_connected {
482 return Err(anyhow::anyhow!(
483 "Relay connection lost before creating ValidRepo fixture"
484 ));
485 }
486
487 // Create a new repo
488 let test_name = format!(
489 "fixture-{:?}-{}",
490 FixtureKind::ValidRepo,
491 &uuid::Uuid::new_v4().to_string()[..8]
492 );
493
494 let repo = self
495 .client
496 .create_repo_announcement(&test_name)
497 .await
498 .with_context(|| format!("create_repo_announcement failed for {}", test_name))?;
499
500 // Send it
501 self.client
502 .send_event(repo.clone())
503 .await
504 .with_context(|| "Failed to send repo announcement to relay")?;
505
506 // Store in the appropriate cache based on mode
507 match self.mode { 416 match self.mode {
508 ContextMode::Isolated => { 417 ContextMode::Isolated => {
509 // Store in local cache for within-test fixture dependencies
510 let mut cache = self.local_cache.lock().unwrap(); 418 let mut cache = self.local_cache.lock().unwrap();
511 cache.insert(FixtureKind::ValidRepo, repo.clone()); 419 cache.insert(kind, event);
512 tracing::debug!( 420 tracing::debug!(
513 "get_or_create_repo() stored in local cache ({} entries)", 421 "store_cached({:?}) stored in local cache ({} entries)",
422 kind,
514 cache.len() 423 cache.len()
515 ); 424 );
516 } 425 }
517 ContextMode::Shared => { 426 ContextMode::Shared => {
518 // Store in client cache for cross-test sharing
519 let mut cache = self.client.fixture_cache().lock().unwrap(); 427 let mut cache = self.client.fixture_cache().lock().unwrap();
520 cache.insert(FixtureKind::ValidRepo, repo.clone()); 428 cache.insert(kind, event);
521 tracing::debug!( 429 tracing::debug!(
522 "get_or_create_repo() stored in client cache ({} entries)", 430 "store_cached({:?}) stored in client cache ({} entries)",
431 kind,
523 cache.len() 432 cache.len()
524 ); 433 );
525 } 434 }
526 } 435 }
527
528 Ok(repo)
529 } 436 }
530 437
531 /// Get or create a RepoWithIssue, with mode-aware caching. 438 // ============================================================
532 /// Returns the issue event (repo is already sent/cached via get_or_create_repo). 439 // Core Fixture Methods
533 async fn get_or_create_issue(&self) -> Result<Event> { 440 // ============================================================
534 // Check the appropriate cache based on mode 441
535 match self.mode { 442 /// Ensure a fixture exists (with all dependencies)
536 ContextMode::Isolated => { 443 ///
537 let cache = self.local_cache.lock().unwrap(); 444 /// This is the core method for fixture management. It:
538 if let Some(event) = cache.get(&FixtureKind::RepoWithIssue) { 445 /// 1. Checks the cache, returning immediately if found
539 return Ok(event.clone()); 446 /// 2. Ensures all dependencies are met (recursively)
540 } 447 /// 3. Builds the fixture
541 } 448 /// 4. Sends to relay (unless fixture handles this internally)
542 ContextMode::Shared => { 449 /// 5. Caches and returns the result
543 let cache = self.client.fixture_cache().lock().unwrap(); 450 ///
544 if let Some(event) = cache.get(&FixtureKind::RepoWithIssue) { 451 /// # Example
545 return Ok(event.clone()); 452 ///
546 } 453 /// ```no_run
454 /// # use grasp_audit::*;
455 /// # async fn example(ctx: &TestContext<'_>) -> anyhow::Result<()> {
456 /// // This ensures ValidRepo exists first, then creates MaintainerState
457 /// let state = ctx.ensure_fixture(FixtureKind::MaintainerState).await?;
458 /// # Ok(())
459 /// # }
460 /// ```
461 pub fn ensure_fixture(&self, kind: FixtureKind) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Event>> + Send + '_>> {
462 Box::pin(async move {
463 // Check cache first
464 if let Some(cached) = self.get_cached(kind) {
465 tracing::debug!("ensure_fixture({:?}) found in cache", kind);
466 return Ok(cached);
547 } 467 }
548 }
549 468
550 // Get or create repo (reuses cached via appropriate cache) 469 // Check relay connection before proceeding
551 let repo = self.get_or_create_repo().await?; 470 if !self.client.is_connected().await {
471 return Err(anyhow::anyhow!(
472 "Relay connection lost before creating {:?} fixture",
473 kind
474 ));
475 }
552 476
553 // Create the issue 477 // Ensure all dependencies are met first
554 let issue = 478 for dep in kind.dependencies() {
555 self.client 479 tracing::debug!("ensure_fixture({:?}) ensuring dependency {:?}", kind, dep);
556 .create_issue(&repo, "Test Issue", "Issue content for testing", vec![])?; 480 self.ensure_fixture(dep).await.with_context(|| {
481 format!("Failed to ensure dependency {:?} for {:?}", dep, kind)
482 })?;
483 }
557 484
558 // Send it 485 // Build the fixture
559 self.client.send_event(issue.clone()).await?; 486 let event = self.build_fixture_inner(kind).await.with_context(|| {
487 format!("Failed to build {:?} fixture", kind)
488 })?;
560 489
561 // Store in the appropriate cache based on mode 490 // Send to relay if this fixture doesn't handle it internally
562 match self.mode { 491 if !kind.sends_own_events() {
563 ContextMode::Isolated => { 492 self.client.send_event(event.clone()).await.with_context(|| {
564 let mut cache = self.local_cache.lock().unwrap(); 493 format!("Failed to send {:?} fixture event to relay", kind)
565 cache.insert(FixtureKind::RepoWithIssue, issue.clone()); 494 })?;
566 }
567 ContextMode::Shared => {
568 let mut cache = self.client.fixture_cache().lock().unwrap();
569 cache.insert(FixtureKind::RepoWithIssue, issue.clone());
570 } 495 }
496
497 // Cache and return
498 self.store_cached(kind, event.clone());
499 Ok(event)
500 })
501 }
502
503 /// Build a fixture event WITHOUT publishing it to the relay.
504 ///
505 /// This is useful for tests that need to get a fixture's event ID before
506 /// actually publishing it. For example, testing refs/nostr/<event-id>
507 /// behavior before the corresponding event exists on the relay.
508 ///
509 /// Note: This ensures dependencies are created/published first, but the
510 /// requested fixture itself will NOT be published.
511 ///
512 /// # Example
513 ///
514 /// ```no_run
515 /// # use grasp_audit::*;
516 /// # async fn example(ctx: &TestContext<'_>) -> anyhow::Result<()> {
517 /// // Build PR event to get its ID without publishing
518 /// let pr_event = ctx.build_fixture_only(FixtureKind::PREvent).await?;
519 /// let pr_event_id = pr_event.id.to_hex();
520 ///
521 /// // Now push to refs/nostr/<pr_event_id> before event exists
522 /// // ... git push ...
523 ///
524 /// // Later, publish the PR event when ready
525 /// ctx.client().send_event(pr_event).await?;
526 /// # Ok(())
527 /// # }
528 /// ```
529 pub async fn build_fixture_only(&self, kind: FixtureKind) -> Result<Event> {
530 // Ensure dependencies are met first
531 for dep in kind.dependencies() {
532 self.ensure_fixture(dep).await?;
571 } 533 }
534 // Build but don't send/cache
535 self.build_fixture_inner(kind).await
536 }
572 537
573 Ok(issue) 538 /// Get a cached dependency (assumes ensure_fixture processed dependencies first)
539 ///
540 /// This is a convenience helper for build_fixture_inner to retrieve dependencies
541 /// that were already ensured by ensure_fixture before calling build_fixture_inner.
542 fn get_cached_dependency(&self, kind: FixtureKind) -> Result<Event> {
543 self.get_cached(kind).ok_or_else(|| {
544 anyhow::anyhow!(
545 "Dependency {:?} not found in cache - this is a bug in fixture dependencies",
546 kind
547 )
548 })
574 } 549 }
575 550
576 /// Build a fixture event (doesn't send it) 551 /// Build a fixture event (internal - assumes dependencies are cached)
577 async fn build_fixture(&self, kind: FixtureKind) -> Result<Event> { 552 ///
553 /// This method is called by `ensure_fixture` after all dependencies have been
554 /// ensured and cached. It should NOT call `ensure_fixture` or it will cause
555 /// infinite recursion. Instead, use `get_cached_dependency` to retrieve
556 /// already-cached dependencies.
557 async fn build_fixture_inner(&self, kind: FixtureKind) -> Result<Event> {
578 match kind { 558 match kind {
579 FixtureKind::ValidRepo => { 559 FixtureKind::ValidRepo => {
580 // Delegate to get_or_create_repo() which handles caching properly. 560 // ValidRepo has no dependencies - create a new repo announcement
581 self.get_or_create_repo().await 561 let test_name = format!(
562 "fixture-ValidRepo-{}",
563 &uuid::Uuid::new_v4().to_string()[..8]
564 );
565
566 self.client
567 .create_repo_announcement(&test_name)
568 .await
569 .with_context(|| format!("create_repo_announcement failed for {}", test_name))
582 } 570 }
583 571
584 FixtureKind::RepoWithIssue => { 572 FixtureKind::RepoWithIssue => {
585 // Reuse ValidRepo fixture - this leverages caching in Shared mode 573 // ValidRepo is ensured by ensure_fixture before this is called
586 // In Isolated mode: creates fresh repo 574 let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?;
587 // In Shared mode: returns cached repo (no duplicate events!) 575
588 // Uses direct helper to avoid async recursion through get_fixture 576 // Build issue referencing it - caller will send it
589 let repo = self.get_or_create_repo().await?; 577 self.client.create_issue(
590
591 // Then create issue referencing it - this will have 'a' tag to repo
592 // Note: We build the issue but DON'T send it here - the caller will send it
593 let issue = self.client.create_issue(
594 &repo, 578 &repo,
595 "Test Issue", 579 "Test Issue",
596 "Issue content for testing", 580 "Issue content for testing",
597 vec![], 581 vec![],
598 )?; 582 )
599
600 // Return the issue - tests can extract repo reference from its 'a' tag
601 // The caller (create_fresh/get_or_create_shared) will send this event
602 Ok(issue)
603 } 583 }
604 584
605 FixtureKind::RepoWithComment => { 585 FixtureKind::RepoWithComment => {
606 // Reuse RepoWithIssue fixture - this leverages caching in Shared mode 586 // RepoWithIssue is ensured by ensure_fixture before this is called
607 // In Isolated mode: creates fresh repo + issue 587 let issue = self.get_cached_dependency(FixtureKind::RepoWithIssue)?;
608 // In Shared mode: returns cached issue (repo already cached too!)
609 let issue = self.get_or_create_issue().await?;
610 588
611 // Then create comment on issue 589 // Build comment on issue - caller will send it
612 // Note: We build the comment but DON'T send it here - the caller will send it
613 self.client.create_comment(&issue, "Test comment", vec![]) 590 self.client.create_comment(&issue, "Test comment", vec![])
614 } 591 }
615 592
616 FixtureKind::RepoState => { 593 FixtureKind::RepoState => {
617 use nostr_sdk::prelude::*; 594 use nostr_sdk::prelude::*;
618 595
619 // Reuse ValidRepo fixture - this leverages caching in Shared mode 596 // ValidRepo is ensured by ensure_fixture before this is called
620 let repo = self.get_or_create_repo().await?; 597 let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?;
621 598
622 // Extract repo_id from repo announcement 599 // Extract repo_id from repo announcement
623 let repo_id = repo 600 let repo_id = repo
@@ -651,98 +628,45 @@ impl<'a> TestContext<'a> {
651 } 628 }
652 629
653 FixtureKind::MaintainerAnnouncement => { 630 FixtureKind::MaintainerAnnouncement => {
654 use nostr_sdk::prelude::*; 631 // ValidRepo is ensured by ensure_fixture before this is called
655 632 let owner_repo = self.get_cached_dependency(FixtureKind::ValidRepo)?;
656 // Get the owner's repo to use the SAME repo_id
657 let owner_repo = self.get_or_create_repo().await?;
658
659 // Extract repo_id from owner's repo announcement
660 let repo_id = owner_repo
661 .tags
662 .iter()
663 .find(|t| t.kind() == TagKind::d())
664 .and_then(|t| t.content())
665 .ok_or_else(|| anyhow::anyhow!("Missing d tag in owner repo announcement"))?
666 .to_string();
667 633
634 let repo_id = self.extract_repo_id(&owner_repo)?;
668 self.build_maintainer_announcement(&repo_id).await 635 self.build_maintainer_announcement(&repo_id).await
669 } 636 }
670 637
671 FixtureKind::MaintainerState => { 638 FixtureKind::MaintainerState => {
672 use nostr_sdk::prelude::*; 639 // ValidRepo is ensured by ensure_fixture before this is called
640 let owner_repo = self.get_cached_dependency(FixtureKind::ValidRepo)?;
673 641
674 // Get the owner's repo to use the SAME repo_id 642 let repo_id = self.extract_repo_id(&owner_repo)?;
675 let owner_repo = self.get_or_create_repo().await?;
676
677 // Extract repo_id from owner's repo announcement
678 let repo_id = owner_repo
679 .tags
680 .iter()
681 .find(|t| t.kind() == TagKind::d())
682 .and_then(|t| t.content())
683 .ok_or_else(|| anyhow::anyhow!("Missing d tag in owner repo announcement"))?
684 .to_string();
685
686 // Build state event ONLY - does NOT send announcement
687 // This allows testing state-only scenarios
688 self.build_maintainer_state(&repo_id) 643 self.build_maintainer_state(&repo_id)
689 } 644 }
690 645
691 FixtureKind::RecursiveMaintainerAnnouncement => { 646 FixtureKind::RecursiveMaintainerAnnouncement => {
692 use nostr_sdk::prelude::*; 647 // ValidRepo is ensured by ensure_fixture before this is called
693 648 let owner_repo = self.get_cached_dependency(FixtureKind::ValidRepo)?;
694 // Get the owner's repo to use the SAME repo_id
695 let owner_repo = self.get_or_create_repo().await?;
696
697 // Extract repo_id from owner's repo announcement
698 let repo_id = owner_repo
699 .tags
700 .iter()
701 .find(|t| t.kind() == TagKind::d())
702 .and_then(|t| t.content())
703 .ok_or_else(|| anyhow::anyhow!("Missing d tag in owner repo announcement"))?
704 .to_string();
705 649
650 let repo_id = self.extract_repo_id(&owner_repo)?;
706 self.build_recursive_maintainer_announcement(&repo_id).await 651 self.build_recursive_maintainer_announcement(&repo_id).await
707 } 652 }
708 653
709 FixtureKind::RecursiveMaintainerState => { 654 FixtureKind::RecursiveMaintainerState => {
710 use nostr_sdk::prelude::*; 655 // ValidRepo is ensured by ensure_fixture before this is called
711 656 let owner_repo = self.get_cached_dependency(FixtureKind::ValidRepo)?;
712 // Get the owner's repo to use the SAME repo_id
713 let owner_repo = self.get_or_create_repo().await?;
714
715 // Extract repo_id from owner's repo announcement
716 let repo_id = owner_repo
717 .tags
718 .iter()
719 .find(|t| t.kind() == TagKind::d())
720 .and_then(|t| t.content())
721 .ok_or_else(|| anyhow::anyhow!("Missing d tag in owner repo announcement"))?
722 .to_string();
723 657
724 // Build state event ONLY - does NOT send announcement 658 let repo_id = self.extract_repo_id(&owner_repo)?;
725 self.build_recursive_maintainer_state(&repo_id) 659 self.build_recursive_maintainer_state(&repo_id)
726 } 660 }
727 661
728 FixtureKind::RecursiveMaintainerRepoAndState => { 662 FixtureKind::RecursiveMaintainerRepoAndState => {
729 use nostr_sdk::prelude::*; 663 // ValidRepo is ensured by ensure_fixture before this is called
664 let owner_repo = self.get_cached_dependency(FixtureKind::ValidRepo)?;
730 665
731 // Get the owner's repo to use the SAME repo_id 666 let repo_id = self.extract_repo_id(&owner_repo)?;
732 let owner_repo = self.get_or_create_repo().await?;
733
734 // Extract repo_id from owner's repo announcement
735 let repo_id = owner_repo
736 .tags
737 .iter()
738 .find(|t| t.kind() == TagKind::d())
739 .and_then(|t| t.content())
740 .ok_or_else(|| anyhow::anyhow!("Missing d tag in owner repo announcement"))?
741 .to_string();
742 667
743 // Build and send the maintainer's repo announcement first 668 // Build and send the maintainer's repo announcement first
744 // This establishes the chain: Owner -> Maintainer -> RecursiveMaintainer 669 // This establishes the chain: Owner -> Maintainer -> RecursiveMaintainer
745 // The maintainer's announcement lists the recursive maintainer in its maintainers tag
746 let maintainer_announcement = self.build_maintainer_announcement(&repo_id).await?; 670 let maintainer_announcement = self.build_maintainer_announcement(&repo_id).await?;
747 self.client.send_event(maintainer_announcement).await?; 671 self.client.send_event(maintainer_announcement).await?;
748 672
@@ -761,8 +685,8 @@ impl<'a> TestContext<'a> {
761 FixtureKind::PREvent => { 685 FixtureKind::PREvent => {
762 use nostr_sdk::prelude::*; 686 use nostr_sdk::prelude::*;
763 687
764 // Reuse ValidRepo fixture to get repo_id and owner pubkey 688 // ValidRepo is ensured by ensure_fixture before this is called
765 let repo = self.get_or_create_repo().await?; 689 let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?;
766 690
767 let repo_id = repo 691 let repo_id = repo
768 .tags 692 .tags
@@ -971,6 +895,17 @@ impl<'a> TestContext<'a> {
971 }) 895 })
972 } 896 }
973 897
898 /// Extract repo_id from a repo announcement event
899 fn extract_repo_id(&self, repo: &Event) -> Result<String> {
900 use nostr_sdk::prelude::*;
901 repo.tags
902 .iter()
903 .find(|t| t.kind() == TagKind::d())
904 .and_then(|t| t.content())
905 .map(|s| s.to_string())
906 .ok_or_else(|| anyhow::anyhow!("Missing d tag in repo announcement"))
907 }
908
974 /// Build OwnerStateDataPushed fixture: full 4-stage fixture for push authorization 909 /// Build OwnerStateDataPushed fixture: full 4-stage fixture for push authorization
975 /// 910 ///
976 /// This handles all stages of the fixture: 911 /// This handles all stages of the fixture:
@@ -985,19 +920,10 @@ impl<'a> TestContext<'a> {
985 use nostr_sdk::prelude::*; 920 use nostr_sdk::prelude::*;
986 921
987 // ============================================================ 922 // ============================================================
988 // Stage 1 & 2: Generate and Send RepoState fixture 923 // Stage 1 & 2: ValidRepo is ensured by ensure_fixture before this is called
989 // (get_or_create_repo handles caching, build_fixture builds state event)
990 // ============================================================ 924 // ============================================================
991 let repo = self.get_or_create_repo().await?; 925 let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?;
992 926 let repo_id = self.extract_repo_id(&repo)?;
993 // Extract repo_id from repo announcement
994 let repo_id = repo
995 .tags
996 .iter()
997 .find(|t| t.kind() == TagKind::d())
998 .and_then(|t| t.content())
999 .ok_or_else(|| anyhow::anyhow!("Missing d tag in repo announcement"))?
1000 .to_string();
1001 927
1002 // Build state event 928 // Build state event
1003 let base_time = Timestamp::now().as_u64(); 929 let base_time = Timestamp::now().as_u64();
@@ -1129,26 +1055,25 @@ impl<'a> TestContext<'a> {
1129 /// This tests that a maintainer can authorize pushes with ONLY a state event, 1055 /// This tests that a maintainer can authorize pushes with ONLY a state event,
1130 /// without publishing their own repo announcement. 1056 /// without publishing their own repo announcement.
1131 /// 1057 ///
1058 /// Depends on OwnerStateDataPushed - the owner's data has already been pushed.
1059 /// The maintainer force-pushes their commit on top.
1060 ///
1132 /// # Returns 1061 /// # Returns
1133 /// The maintainer's state event (kind 30618) after all stages complete successfully 1062 /// The maintainer's state event (kind 30618) after all stages complete successfully
1134 async fn build_maintainer_state_data_pushed(&self) -> Result<Event> { 1063 async fn build_maintainer_state_data_pushed(&self) -> Result<Event> {
1135 use nostr_sdk::prelude::*; 1064 use nostr_sdk::prelude::*;
1136 1065
1137 // ============================================================ 1066 // ============================================================
1138 // Stage 1 & 2: Generate and Send ValidRepo + MaintainerState fixtures 1067 // Stage 1: OwnerStateDataPushed is ensured by ensure_fixture before this is called
1068 // The owner's repo and state event are already on the relay, and git data is pushed
1139 // ============================================================ 1069 // ============================================================
1070 let owner_state = self.get_cached_dependency(FixtureKind::OwnerStateDataPushed)?;
1140 1071
1141 // Get owner's repo (ValidRepo) - this includes maintainer in maintainers tag 1072 // Extract repo_id from owner's state event (same d-tag structure)
1142 let repo = self.get_or_create_repo().await?; 1073 let repo_id = self.extract_repo_id(&owner_state)?;
1143 1074
1144 // Extract repo_id from repo announcement 1075 // Get the repo (ValidRepo, also cached) for the owner's npub
1145 let repo_id = repo 1076 let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?;
1146 .tags
1147 .iter()
1148 .find(|t| t.kind() == TagKind::d())
1149 .and_then(|t| t.content())
1150 .ok_or_else(|| anyhow::anyhow!("Missing d tag in repo announcement"))?
1151 .to_string();
1152 1077
1153 // Build maintainer's state event (state event ONLY - no announcement) 1078 // Build maintainer's state event (state event ONLY - no announcement)
1154 let base_time = Timestamp::now().as_u64(); 1079 let base_time = Timestamp::now().as_u64();