upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit/src/fixtures.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-23 15:41:32 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-23 15:41:32 +0000
commitc54ce061d6d278cce8362d5af085808ca60c239b (patch)
treeec967d6195d9f7ec4f061449596611afe3a0950f /grasp-audit/src/fixtures.rs
parente0ad39a489b3398f8208713bf728db0cb11475b0 (diff)
parent113928aa84894ea8f65c247d9987527e792b32a9 (diff)
feat: announcement purgatory
Extends purgatory to hold repository announcements until git data arrives, preventing empty repositories from being served to clients. When an announcement is received, a bare repo is created immediately and the announcement is held in purgatory. It is only promoted and served once a git push confirms real content exists. If no push arrives before expiry, the bare repo is deleted and the announcement is silently discarded. Key behaviours: - Soft expiry: announcements are hidden from clients but kept alive while git pushes are in progress, reviving on successful push - Expiry is extended when a matching state event or git push is observed - NIP-09 deletion events remove announcements from purgatory - Purgatory state (announcements, state events, PR events, expired set) is persisted to disk on graceful shutdown and restored on startup, with elapsed downtime subtracted from expiry deadlines - Purgatory announcements drive StateOnly sync in the sync system so state events are fetched from listed relays before promotion - SyncLevel added to RepoSyncIndex to distinguish purgatory repos (StateOnly) from promoted repos (Full L2+L3 sync)
Diffstat (limited to 'grasp-audit/src/fixtures.rs')
-rw-r--r--grasp-audit/src/fixtures.rs522
1 files changed, 407 insertions, 115 deletions
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs
index bbc7740..45d3094 100644
--- a/grasp-audit/src/fixtures.rs
+++ b/grasp-audit/src/fixtures.rs
@@ -47,7 +47,7 @@
47//! let ctx = TestContext::new(&client); 47//! let ctx = TestContext::new(&client);
48//! 48//!
49//! // Request a fixture - behavior depends on mode 49//! // Request a fixture - behavior depends on mode
50//! let repo = ctx.get_fixture(FixtureKind::ValidRepo).await?; 50//! let repo = ctx.get_fixture(FixtureKind::ValidRepoSent).await?;
51//! # Ok(()) 51//! # Ok(())
52//! # } 52//! # }
53//! ``` 53//! ```
@@ -61,59 +61,68 @@ use std::sync::{Arc, Mutex};
61/// Deterministic commit hash used in RepoState fixtures (Owner variant) 61/// Deterministic commit hash used in RepoState fixtures (Owner variant)
62/// This is the hash produced by creating a commit with: 62/// This is the hash produced by creating a commit with:
63/// - Message: "Initial commit" 63/// - Message: "Initial commit"
64/// - File: test.txt containing "Initial commit" 64/// - File: test.txt containing "Initial commit\n" (with trailing newline)
65/// - Author date: 2024-01-01T00:00:00Z 65/// - Author date: 2024-01-01T00:00:00Z
66/// - Committer date: 2024-01-01T00:00:00Z 66/// - Committer date: 2024-01-01T00:00:00Z
67/// - GPG signing: disabled 67/// - GPG signing: disabled
68/// - User: "GRASP Audit Test <test@grasp-audit.local>" 68/// - User: "GRASP Audit Test <test@grasp-audit.local>"
69/// - Parent: Initial empty commit (09cc37de80f3434fa98864a86730b8d7777bd6ae) 69/// - Parent: none (root commit)
70pub const DETERMINISTIC_COMMIT_HASH: &str = "64ea71d79a57a7acb334cd9651f8aec067c0ce5d"; 70pub const DETERMINISTIC_COMMIT_HASH: &str = "d6e4b26ccf9c268d18d60e6d09804313cc850821";
71 71
72/// Deterministic commit hash for maintainer fixtures (Maintainer variant) 72/// Deterministic commit hash for maintainer fixtures (Maintainer variant)
73/// This is the hash produced by creating a commit with: 73/// This is the hash produced by creating a commit with:
74/// - Message: "Maintainer initial commit" 74/// - Message: "Maintainer initial commit"
75/// - File: test.txt containing "Maintainer initial commit" 75/// - File: test.txt containing "Maintainer initial commit\n" (with trailing newline)
76/// - Author date: 2024-01-01T00:00:00Z 76/// - Author date: 2024-01-01T00:00:00Z
77/// - Committer date: 2024-01-01T00:00:00Z 77/// - Committer date: 2024-01-01T00:00:00Z
78/// - GPG signing: disabled 78/// - GPG signing: disabled
79/// - User: "GRASP Audit Test <test@grasp-audit.local>" 79/// - User: "GRASP Audit Test <test@grasp-audit.local>"
80/// - Parent: none (root commit) 80/// - Parent: none (root commit)
81/// NOTE: This value is different from DETERMINISTIC_COMMIT_HASH due to different content 81pub const MAINTAINER_DETERMINISTIC_COMMIT_HASH: &str = "d26703c007eff6d17fee3bb70ce8be5d1427d0e7";
82pub const MAINTAINER_DETERMINISTIC_COMMIT_HASH: &str = "1c2d472c9b71ed51968a66500281a3c4a6840464";
83 82
84/// Deterministic commit hash for recursive maintainer fixtures (RecursiveMaintainer variant) 83/// Deterministic commit hash for recursive maintainer fixtures (RecursiveMaintainer variant)
85/// This is the hash produced by creating a commit with: 84/// This is the hash produced by creating a commit with:
86/// - Message: "Recursive maintainer initial commit" 85/// - Message: "Recursive maintainer initial commit"
87/// - File: test.txt containing "Recursive maintainer initial commit" 86/// - File: test.txt containing "Recursive maintainer initial commit\n" (with trailing newline)
88/// - Author date: 2024-01-01T00:00:00Z 87/// - Author date: 2024-01-01T00:00:00Z
89/// - Committer date: 2024-01-01T00:00:00Z 88/// - Committer date: 2024-01-01T00:00:00Z
90/// - GPG signing: disabled 89/// - GPG signing: disabled
91/// - User: "GRASP Audit Test <test@grasp-audit.local>" 90/// - User: "GRASP Audit Test <test@grasp-audit.local>"
92/// - Parent: none (root commit) 91/// - Parent: none (root commit)
93/// NOTE: This value is different from DETERMINISTIC_COMMIT_HASH due to different content
94pub const RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH: &str = 92pub const RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH: &str =
95 "05939b82de66fbdb9c077d0a64fc68522f3cb8e0"; 93 "54a2b4b3cbc3373ad1438b8ffad1681d12bc6c4a";
96 94
97/// Deterministic commit hash for PR test fixtures (PRTestCommit variant) 95/// Deterministic commit hash for PR test fixtures (PRTestCommit variant)
98/// This is the hash produced by creating a commit with: 96/// This is the hash produced by creating a commit with:
99/// - Message: "PR test deterministic commit" 97/// - Message: "PR test deterministic commit"
100/// - File: test.txt containing "PR test deterministic commit" 98/// - File: test.txt containing "PR test deterministic commit\n" (with trailing newline)
99/// - Author date: 2024-01-01T00:00:00Z
100/// - Committer date: 2024-01-01T00:00:00Z
101/// - GPG signing: disabled
102/// - User: "GRASP Audit Test <test@grasp-audit.local>"
103/// - Parent: none (root commit)
104pub const PR_TEST_COMMIT_HASH: &str = "5a51b30e4615b572dcd5b9e487861b58605a5c21";
105
106/// Deterministic commit hash for second PR test fixtures (PRTestCommit2 variant)
107/// This is the hash produced by creating a commit with:
108/// - Message: "PR test deterministic commit 2"
109/// - File: test.txt containing "PR test deterministic commit 2\n" (with trailing newline)
101/// - Author date: 2024-01-01T00:00:00Z 110/// - Author date: 2024-01-01T00:00:00Z
102/// - Committer date: 2024-01-01T00:00:00Z 111/// - Committer date: 2024-01-01T00:00:00Z
103/// - GPG signing: disabled 112/// - GPG signing: disabled
104/// - User: "GRASP Audit Test <test@grasp-audit.local>" 113/// - User: "GRASP Audit Test <test@grasp-audit.local>"
105/// - Parent: none (root commit) 114/// - Parent: none (root commit)
106pub const PR_TEST_COMMIT_HASH: &str = "5d40fb1555a0c28bf4d650515a73aaa54d4d9bfb"; 115pub const PR_TEST_COMMIT_HASH_2: &str = "99420bc57835f5bc8ca20ab21a8d12850043920e";
107 116
108/// Types of test fixtures available 117/// Types of test fixtures available
109/// 118///
110/// ## Fixture Dependencies 119/// ## Fixture Dependencies
111/// 120///
112/// Several fixtures depend on `ValidRepo` - they all use the SAME repo_id 121/// Several fixtures depend on `ValidRepoSent` - they all use the SAME repo_id
113/// within a single TestContext instance to ensure proper fixture relationships: 122/// within a single TestContext instance to ensure proper fixture relationships:
114/// - `RepoState` → uses ValidRepo's repo_id 123/// - `RepoState` → uses ValidRepoSent's repo_id
115/// - `MaintainerAnnouncement` + `MaintainerState` → uses ValidRepo's repo_id 124/// - `MaintainerAnnouncement` + `MaintainerState` → uses ValidRepoSent's repo_id
116/// - `RecursiveMaintainerRepoAndState` → uses ValidRepo's repo_id 125/// - `RecursiveMaintainerRepoAndState` → uses ValidRepoSent's repo_id
117/// 126///
118/// This enables testing recursive maintainer authorization chains where multiple 127/// This enables testing recursive maintainer authorization chains where multiple
119/// parties publish announcements and state events for the same repository. 128/// parties publish announcements and state events for the same repository.
@@ -122,10 +131,16 @@ pub enum FixtureKind {
122 /// Basic repository announcement (kind 30617) 131 /// Basic repository announcement (kind 30617)
123 /// - Signed by owner keys (`client.keys()`) 132 /// - Signed by owner keys (`client.keys()`)
124 /// - Lists `client.maintainer_pubkey_hex()` in maintainers tag 133 /// - Lists `client.maintainer_pubkey_hex()` in maintainers tag
125 ValidRepo, 134 ValidRepoSent,
135
136 /// Repository announcement that is queryable from the relay (served, not in purgatory)
137 /// - Depends on OwnerStateDataPushed (git data pushed, announcement promoted)
138 /// - Returns the same event as ValidRepoSent (now queryable)
139 /// - Use this for tests that need to query the announcement back from the relay
140 ValidRepoServed,
126 141
127 /// Repository with one issue (kind 1621) 142 /// Repository with one issue (kind 1621)
128 /// - Requires ValidRepo (reuses same repo_id) 143 /// - Requires ValidRepoServed (needs queryable repo for issue to reference)
129 RepoWithIssue, 144 RepoWithIssue,
130 145
131 /// Repository with issue and comment (kind 1111) 146 /// Repository with issue and comment (kind 1111)
@@ -133,14 +148,30 @@ pub enum FixtureKind {
133 RepoWithComment, 148 RepoWithComment,
134 149
135 /// Repository state announcement (kind 30618) for owner 150 /// Repository state announcement (kind 30618) for owner
136 /// - Requires ValidRepo (uses same repo_id) 151 /// - Requires ValidRepoSent (uses same repo_id)
137 /// - Signed by owner keys (`client.keys()`) 152 /// - Signed by owner keys (`client.keys()`)
138 /// - Points to DETERMINISTIC_COMMIT_HASH 153 /// - Points to DETERMINISTIC_COMMIT_HASH
139 /// - Timestamp: 10 seconds in the past 154 /// - Timestamp: 10 seconds in the past
140 RepoState, 155 RepoState,
141 156
142 /// PR (Pull Request) event for the SAME repo_id as ValidRepo 157 /// Owner's repository state announcement (kind 30618) sent to relay and accepted into purgatory
143 /// - Requires ValidRepo (uses same repo_id) 158 ///
159 /// This is the "sent" stage: the state event has been published to the relay and
160 /// accepted (OK response), but no git data has been pushed yet so it remains in
161 /// purgatory and is not served to clients.
162 ///
163 /// Use this when you need the state event to exist on the relay but do not need
164 /// the full push/serve cycle. For the complete cycle (git pushed + verified served),
165 /// use `OwnerStateDataPushed`.
166 ///
167 /// - Requires ValidRepoSent (uses same repo_id)
168 /// - Signed by owner keys (`client.keys()`)
169 /// - Points to DETERMINISTIC_COMMIT_HASH
170 /// - Timestamp: 10 seconds in the past
171 OwnerRepoStateSent,
172
173 /// PR (Pull Request) event for the SAME repo_id as ValidRepoServed
174 /// - Requires ValidRepoServed (uses same repo_id, needs queryable repo)
144 /// - Signed by `client.pr_author_keys()` 175 /// - Signed by `client.pr_author_keys()`
145 /// - Kind 1618 (NIP-34 PR) 176 /// - Kind 1618 (NIP-34 PR)
146 /// - Includes `a` tag referencing the repo 177 /// - Includes `a` tag referencing the repo
@@ -153,7 +184,7 @@ pub enum FixtureKind {
153 /// This is a "Generated" stage fixture - the event is created but not published. 184 /// This is a "Generated" stage fixture - the event is created but not published.
154 /// Useful for tests that need the PR event ID before the event exists on the relay. 185 /// Useful for tests that need the PR event ID before the event exists on the relay.
155 /// 186 ///
156 /// - Requires ValidRepo (uses same repo_id) 187 /// - Requires ValidRepoServed (uses same repo_id, needs queryable repo)
157 /// - Signed by `client.pr_author_keys()` 188 /// - Signed by `client.pr_author_keys()`
158 /// - Kind 1618 (NIP-34 PR) 189 /// - Kind 1618 (NIP-34 PR)
159 /// - Includes `c` tag pointing to PR_TEST_COMMIT_HASH 190 /// - Includes `c` tag pointing to PR_TEST_COMMIT_HASH
@@ -187,7 +218,7 @@ pub enum FixtureKind {
187 /// (the "wrong" commit), but no PR event exists yet on the relay. 218 /// (the "wrong" commit), but no PR event exists yet on the relay.
188 /// 219 ///
189 /// Server state after this fixture: 220 /// Server state after this fixture:
190 /// - ValidRepo announcement on relay 221 /// - ValidRepoServed announcement on relay (repo is queryable)
191 /// - refs/nostr/<pr-event-id> exists on git server with wrong commit 222 /// - refs/nostr/<pr-event-id> exists on git server with wrong commit
192 /// - PR event is NOT on relay (but returned for tests to publish later) 223 /// - PR event is NOT on relay (but returned for tests to publish later)
193 /// 224 ///
@@ -203,7 +234,7 @@ pub enum FixtureKind {
203 /// then the PR event was published (which may trigger cleanup). 234 /// then the PR event was published (which may trigger cleanup).
204 /// 235 ///
205 /// Server state after this fixture: 236 /// Server state after this fixture:
206 /// - ValidRepo announcement on relay 237 /// - ValidRepoServed announcement on relay
207 /// - PR event is on relay 238 /// - PR event is on relay
208 /// - refs/nostr/<pr-event-id> may have been cleaned up (that's what tests verify) 239 /// - refs/nostr/<pr-event-id> may have been cleaned up (that's what tests verify)
209 /// 240 ///
@@ -212,6 +243,50 @@ pub enum FixtureKind {
212 /// - Returns: the sent PR event 243 /// - Returns: the sent PR event
213 PREventSentAfterWrongPush, 244 PREventSentAfterWrongPush,
214 245
246 /// Second PR event generated (built) but NOT sent to relay
247 ///
248 /// Uses PR_TEST_COMMIT_HASH_2 (different from PR_TEST_COMMIT_HASH).
249 /// This allows testing purgatory mechanism with a separate PR event
250 /// that doesn't conflict with existing PR fixtures.
251 ///
252 /// - Requires ValidRepoServed (uses same repo_id, needs git data to exist)
253 /// - Signed by `client.pr_author_keys()`
254 /// - Kind 1618 (NIP-34 PR)
255 /// - Includes `c` tag pointing to PR_TEST_COMMIT_HASH_2
256 /// - NOT sent to relay
257 PREvent2Generated,
258
259 /// Second PR event sent to relay (enters purgatory)
260 ///
261 /// After this fixture:
262 /// - PR event is on relay but NOT served (in purgatory)
263 /// - No git data at refs/nostr/<pr-event-id>
264 ///
265 /// - Requires PREvent2Generated
266 /// - Sends the PR event to relay
267 /// - Returns: the sent PR event (in purgatory)
268 PREvent2Sent,
269
270 /// Git data pushed for second PR event AFTER event was sent
271 ///
272 /// After this fixture:
273 /// - PR event was in purgatory
274 /// - Correct commit pushed to refs/nostr/<pr-event-id>
275 /// - PR event should be released from purgatory
276 ///
277 /// - Requires PREvent2Sent
278 /// - Pushes correct commit (PR_TEST_COMMIT_HASH_2) to refs/nostr/<pr-event-id>
279 /// - Returns: the PR event (should now be served)
280 PREvent2GitDataPushed,
281
282 /// Full fixture: second PR event sent, git pushed, event served
283 ///
284 /// Combines PREvent2Sent + PREvent2GitDataPushed for convenience.
285 ///
286 /// - Requires PREvent2GitDataPushed
287 /// - Returns: the served PR event
288 PREvent2Served,
289
215 /// Owner's state event with git data successfully pushed (full 4-stage fixture) 290 /// Owner's state event with git data successfully pushed (full 4-stage fixture)
216 /// 291 ///
217 /// This fixture represents the complete flow for testing state push authorization: 292 /// This fixture represents the complete flow for testing state push authorization:
@@ -221,7 +296,7 @@ pub enum FixtureKind {
221 /// 4. **DataPushed**: Clones repo, creates deterministic commit, pushes to relay 296 /// 4. **DataPushed**: Clones repo, creates deterministic commit, pushes to relay
222 /// 5. **Verified**: Confirms event is served by relay 297 /// 5. **Verified**: Confirms event is served by relay
223 /// 298 ///
224 /// - Requires ValidRepo (uses same repo_id) 299 /// - Requires ValidRepoSent (uses same repo_id)
225 /// - State event signed by owner keys (`client.keys()`) 300 /// - State event signed by owner keys (`client.keys()`)
226 /// - Points to DETERMINISTIC_COMMIT_HASH 301 /// - Points to DETERMINISTIC_COMMIT_HASH
227 /// - Git push verified to succeed (state matches pushed commit) 302 /// - Git push verified to succeed (state matches pushed commit)
@@ -252,7 +327,7 @@ pub enum FixtureKind {
252 /// not the owner's announcement, so this tests the recursive maintainer traversal. 327 /// not the owner's announcement, so this tests the recursive maintainer traversal.
253 /// 328 ///
254 /// This fixture represents the complete flow for testing recursive maintainer push authorization: 329 /// This fixture represents the complete flow for testing recursive maintainer push authorization:
255 /// 1. **Generated**: (MaintainerStateDataPushed dependency includes ValidRepo + OwnerStateDataPushed) 330 /// 1. **Generated**: (MaintainerStateDataPushed dependency includes ValidRepoSent + OwnerStateDataPushed)
256 /// Creates MaintainerAnnouncement + RecursiveMaintainerState 331 /// Creates MaintainerAnnouncement + RecursiveMaintainerState
257 /// 2. **Sent**: Sends events to relay (returns OK, accepted but 'purgatory:...' message) 332 /// 2. **Sent**: Sends events to relay (returns OK, accepted but 'purgatory:...' message)
258 /// 3. **Verify Not Served**: Confirms event is not served by relays 333 /// 3. **Verify Not Served**: Confirms event is not served by relays
@@ -276,16 +351,29 @@ impl FixtureKind {
276 pub fn dependencies(&self) -> Vec<FixtureKind> { 351 pub fn dependencies(&self) -> Vec<FixtureKind> {
277 match self { 352 match self {
278 // Base fixtures - no dependencies 353 // Base fixtures - no dependencies
279 Self::ValidRepo => vec![], 354 Self::ValidRepoSent => vec![],
280 355
281 // Fixtures that depend on ValidRepo 356 // ValidRepoServed depends on OwnerStateDataPushed (announcement promoted after git push)
282 Self::RepoWithIssue => vec![Self::ValidRepo], 357 Self::ValidRepoServed => vec![Self::OwnerStateDataPushed],
283 Self::RepoState => vec![Self::ValidRepo], 358
284 Self::PREvent => vec![Self::ValidRepo], 359 // Fixtures that depend on ValidRepoServed (need queryable announcement)
285 Self::PREventGenerated => vec![Self::ValidRepo], 360 Self::RepoWithIssue => vec![Self::ValidRepoServed],
361 Self::RepoState => vec![Self::ValidRepoSent],
362 // OwnerRepoStateSent depends on ValidRepoSent: state event sent, sitting in purgatory
363 Self::OwnerRepoStateSent => vec![Self::ValidRepoSent],
364 Self::PREvent => vec![Self::ValidRepoServed],
365 Self::PREventGenerated => vec![Self::ValidRepoServed],
286 Self::PRWrongCommitPushedBeforeEvent => vec![Self::PREventGenerated], 366 Self::PRWrongCommitPushedBeforeEvent => vec![Self::PREventGenerated],
287 Self::PREventSentAfterWrongPush => vec![Self::PRWrongCommitPushedBeforeEvent], 367 Self::PREventSentAfterWrongPush => vec![Self::PRWrongCommitPushedBeforeEvent],
288 Self::OwnerStateDataPushed => vec![Self::ValidRepo], 368
369 // Second PR event fixtures (for purgatory testing)
370 Self::PREvent2Generated => vec![Self::ValidRepoServed],
371 Self::PREvent2Sent => vec![Self::PREvent2Generated],
372 Self::PREvent2GitDataPushed => vec![Self::PREvent2Sent],
373 Self::PREvent2Served => vec![Self::PREvent2GitDataPushed],
374
375 // OwnerStateDataPushed depends on OwnerRepoStateSent (git push + purgatory release)
376 Self::OwnerStateDataPushed => vec![Self::OwnerRepoStateSent],
289 377
290 // Fixtures that depend on RepoWithIssue 378 // Fixtures that depend on RepoWithIssue
291 Self::RepoWithComment => vec![Self::RepoWithIssue], 379 Self::RepoWithComment => vec![Self::RepoWithIssue],
@@ -321,8 +409,17 @@ impl FixtureKind {
321 Self::PRWrongCommitPushedBeforeEvent => true, 409 Self::PRWrongCommitPushedBeforeEvent => true,
322 // PREventSentAfterWrongPush sends the PR event internally 410 // PREventSentAfterWrongPush sends the PR event internally
323 Self::PREventSentAfterWrongPush => true, 411 Self::PREventSentAfterWrongPush => true,
412 // Second PR event fixtures handle their own events/git data
413 Self::PREvent2Generated => true,
414 Self::PREvent2Sent => true,
415 Self::PREvent2GitDataPushed => true,
416 Self::PREvent2Served => true,
324 // HeadSetToDevelopBranch sends its state event internally 417 // HeadSetToDevelopBranch sends its state event internally
325 Self::HeadSetToDevelopBranch => true, 418 Self::HeadSetToDevelopBranch => true,
419 // ValidRepoServed doesn't send anything itself, just returns cached event
420 Self::ValidRepoServed => true,
421 // OwnerRepoStateSent sends its state event and notes purgatory internally
422 Self::OwnerRepoStateSent => true,
326 // All other fixtures return a single event for the caller to send 423 // All other fixtures return a single event for the caller to send
327 _ => false, 424 _ => false,
328 } 425 }
@@ -373,7 +470,7 @@ impl From<AuditMode> for ContextMode {
373/// let ctx = TestContext::new(&client); 470/// let ctx = TestContext::new(&client);
374/// 471///
375/// // Get a repository fixture - will be reused by subsequent TestContexts 472/// // Get a repository fixture - will be reused by subsequent TestContexts
376/// let repo = ctx.get_fixture(FixtureKind::ValidRepo).await?; 473/// let repo = ctx.get_fixture(FixtureKind::ValidRepoSent).await?;
377/// 474///
378/// // For cargo test (isolated fixtures) 475/// // For cargo test (isolated fixtures)
379/// let config = AuditConfig::isolated(); 476/// let config = AuditConfig::isolated();
@@ -381,7 +478,7 @@ impl From<AuditMode> for ContextMode {
381/// let ctx = TestContext::new(&client); 478/// let ctx = TestContext::new(&client);
382/// 479///
383/// // Get a repository fixture - fresh for this TestContext only 480/// // Get a repository fixture - fresh for this TestContext only
384/// let repo = ctx.get_fixture(FixtureKind::ValidRepo).await?; 481/// let repo = ctx.get_fixture(FixtureKind::ValidRepoSent).await?;
385/// # Ok(()) 482/// # Ok(())
386/// # } 483/// # }
387/// ``` 484/// ```
@@ -436,7 +533,7 @@ impl<'a> TestContext<'a> {
436 /// ```no_run 533 /// ```no_run
437 /// # use grasp_audit::*; 534 /// # use grasp_audit::*;
438 /// # async fn example(ctx: &TestContext<'_>) -> anyhow::Result<()> { 535 /// # async fn example(ctx: &TestContext<'_>) -> anyhow::Result<()> {
439 /// let repo = ctx.get_fixture(FixtureKind::ValidRepo).await?; 536 /// let repo = ctx.get_fixture(FixtureKind::ValidRepoSent).await?;
440 /// # Ok(()) 537 /// # Ok(())
441 /// # } 538 /// # }
442 /// ``` 539 /// ```
@@ -517,8 +614,8 @@ impl<'a> TestContext<'a> {
517 /// ```no_run 614 /// ```no_run
518 /// # use grasp_audit::*; 615 /// # use grasp_audit::*;
519 /// # async fn example(ctx: &TestContext<'_>) -> anyhow::Result<()> { 616 /// # async fn example(ctx: &TestContext<'_>) -> anyhow::Result<()> {
520 /// // This ensures ValidRepo exists first, then creates MaintainerState 617 /// // This ensures ValidRepoSent exists first, then creates RepoState
521 /// let state = ctx.ensure_fixture(FixtureKind::MaintainerState).await?; 618 /// let state = ctx.ensure_fixture(FixtureKind::RepoState).await?;
522 /// # Ok(()) 619 /// # Ok(())
523 /// # } 620 /// # }
524 /// ``` 621 /// ```
@@ -625,10 +722,10 @@ impl<'a> TestContext<'a> {
625 /// already-cached dependencies. 722 /// already-cached dependencies.
626 async fn build_fixture_inner(&self, kind: FixtureKind) -> Result<Event> { 723 async fn build_fixture_inner(&self, kind: FixtureKind) -> Result<Event> {
627 match kind { 724 match kind {
628 FixtureKind::ValidRepo => { 725 FixtureKind::ValidRepoSent => {
629 // ValidRepo has no dependencies - create a new repo announcement 726 // ValidRepoSent has no dependencies - create a new repo announcement
630 let test_name = format!( 727 let test_name = format!(
631 "fixture-ValidRepo-{}", 728 "fixture-ValidRepoSent-{}",
632 &uuid::Uuid::new_v4().to_string()[..8] 729 &uuid::Uuid::new_v4().to_string()[..8]
633 ); 730 );
634 731
@@ -638,9 +735,15 @@ impl<'a> TestContext<'a> {
638 .with_context(|| format!("create_repo_announcement failed for {}", test_name)) 735 .with_context(|| format!("create_repo_announcement failed for {}", test_name))
639 } 736 }
640 737
738 FixtureKind::ValidRepoServed => {
739 // OwnerStateDataPushed is already ensured as a dependency.
740 // The announcement is now promoted (served). Return the cached ValidRepoSent event.
741 self.get_cached_dependency(FixtureKind::ValidRepoSent)
742 }
743
641 FixtureKind::RepoWithIssue => { 744 FixtureKind::RepoWithIssue => {
642 // ValidRepo is ensured by ensure_fixture before this is called 745 // ValidRepoServed is ensured by ensure_fixture before this is called
643 let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; 746 let repo = self.get_cached_dependency(FixtureKind::ValidRepoServed)?;
644 747
645 // Build issue referencing it - caller will send it 748 // Build issue referencing it - caller will send it
646 self.client 749 self.client
@@ -658,8 +761,8 @@ impl<'a> TestContext<'a> {
658 FixtureKind::RepoState => { 761 FixtureKind::RepoState => {
659 use nostr_sdk::prelude::*; 762 use nostr_sdk::prelude::*;
660 763
661 // ValidRepo is ensured by ensure_fixture before this is called 764 // ValidRepoSent is ensured by ensure_fixture before this is called
662 let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; 765 let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?;
663 766
664 // Extract repo_id from repo announcement 767 // Extract repo_id from repo announcement
665 let repo_id = repo 768 let repo_id = repo
@@ -692,18 +795,52 @@ impl<'a> TestContext<'a> {
692 .map_err(|e| anyhow::anyhow!("Failed to build state announcement: {}", e)) 795 .map_err(|e| anyhow::anyhow!("Failed to build state announcement: {}", e))
693 } 796 }
694 797
798 FixtureKind::OwnerRepoStateSent => {
799 use nostr_sdk::prelude::*;
800
801 // ValidRepoSent is ensured by ensure_fixture before this is called
802 let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?;
803 let repo_id = self.extract_repo_id(&repo)?;
804
805 let base_time = Timestamp::now().as_secs();
806 let older_timestamp = Timestamp::from(base_time - 10);
807
808 let state_event = self
809 .client
810 .event_builder(Kind::RepoState, "")
811 .tag(Tag::identifier(&repo_id))
812 .tag(Tag::custom(
813 TagKind::custom("refs/heads/main"),
814 vec![DETERMINISTIC_COMMIT_HASH.to_string()],
815 ))
816 .tag(Tag::custom(
817 TagKind::custom("HEAD"),
818 vec!["ref: refs/heads/main".to_string()],
819 ))
820 .custom_time(older_timestamp)
821 .build(self.client.keys())
822 .map_err(|e| anyhow::anyhow!("Failed to build state announcement: {}", e))?;
823
824 // Send to relay - event will be accepted but held in purgatory (no git data yet)
825 self.client
826 .send_event_and_note_purgatory(state_event.clone())
827 .await?;
828
829 Ok(state_event)
830 }
831
695 FixtureKind::PREvent => { 832 FixtureKind::PREvent => {
696 use nostr_sdk::prelude::*; 833 use nostr_sdk::prelude::*;
697 834
698 // ValidRepo is ensured by ensure_fixture before this is called 835 // ValidRepoServed is ensured by ensure_fixture before this is called
699 let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; 836 let repo = self.get_cached_dependency(FixtureKind::ValidRepoServed)?;
700 837
701 let repo_id = repo 838 let repo_id = repo
702 .tags 839 .tags
703 .iter() 840 .iter()
704 .find(|t| t.kind() == TagKind::d()) 841 .find(|t| t.kind() == TagKind::d())
705 .and_then(|t| t.content()) 842 .and_then(|t| t.content())
706 .ok_or_else(|| anyhow::anyhow!("Missing repo_id in ValidRepo fixture"))? 843 .ok_or_else(|| anyhow::anyhow!("Missing repo_id in ValidRepoServed fixture"))?
707 .to_string(); 844 .to_string();
708 845
709 // Create PR event 1 second in the past 846 // Create PR event 1 second in the past
@@ -738,15 +875,15 @@ impl<'a> TestContext<'a> {
738 // This fixture is for "Generated" stage only 875 // This fixture is for "Generated" stage only
739 use nostr_sdk::prelude::*; 876 use nostr_sdk::prelude::*;
740 877
741 // ValidRepo is ensured by ensure_fixture before this is called 878 // ValidRepoServed is ensured by ensure_fixture before this is called
742 let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; 879 let repo = self.get_cached_dependency(FixtureKind::ValidRepoServed)?;
743 880
744 let repo_id = repo 881 let repo_id = repo
745 .tags 882 .tags
746 .iter() 883 .iter()
747 .find(|t| t.kind() == TagKind::d()) 884 .find(|t| t.kind() == TagKind::d())
748 .and_then(|t| t.content()) 885 .and_then(|t| t.content())
749 .ok_or_else(|| anyhow::anyhow!("Missing repo_id in ValidRepo fixture"))? 886 .ok_or_else(|| anyhow::anyhow!("Missing repo_id in ValidRepoServed fixture"))?
750 .to_string(); 887 .to_string();
751 888
752 // Create PR event 1 second in the past 889 // Create PR event 1 second in the past
@@ -784,6 +921,11 @@ impl<'a> TestContext<'a> {
784 self.build_pr_event_sent_after_wrong_push().await 921 self.build_pr_event_sent_after_wrong_push().await
785 } 922 }
786 923
924 FixtureKind::PREvent2Generated => self.build_pr_event_2_generated().await,
925 FixtureKind::PREvent2Sent => self.build_pr_event_2_sent().await,
926 FixtureKind::PREvent2GitDataPushed => self.build_pr_event_2_git_data_pushed().await,
927 FixtureKind::PREvent2Served => self.build_pr_event_2_served().await,
928
787 FixtureKind::OwnerStateDataPushed => self.build_owner_state_data_pushed().await, 929 FixtureKind::OwnerStateDataPushed => self.build_owner_state_data_pushed().await,
788 930
789 FixtureKind::MaintainerStateDataPushed => { 931 FixtureKind::MaintainerStateDataPushed => {
@@ -858,55 +1000,26 @@ impl<'a> TestContext<'a> {
858 .ok_or_else(|| anyhow::anyhow!("Missing d tag in repo announcement")) 1000 .ok_or_else(|| anyhow::anyhow!("Missing d tag in repo announcement"))
859 } 1001 }
860 1002
861 /// Build OwnerStateDataPushed fixture: full 4-stage fixture for push authorization 1003 /// Build OwnerStateDataPushed fixture: git push + purgatory release for owner's state event
862 /// 1004 ///
863 /// This handles all stages of the fixture: 1005 /// `OwnerRepoStateSent` is ensured as a dependency before this is called — the state event
864 /// 1. **Generated**: Creates RepoState (repo announcement + state event) 1006 /// is already on the relay in purgatory. This fixture completes the cycle:
865 /// 2. **Sent**: Sends events to relay (returns OK, accepted but 'purgatory:...' message) 1007 /// 1. **DataPushed**: Clones repo, creates deterministic commit, pushes to relay
866 /// 3. **Verify Not Served**: Confirms event is not served by relays 1008 /// 2. **Verified**: Confirms state event is released from purgatory and served
867 /// 4. **DataPushed**: Clones repo, creates deterministic commit, pushes to relay
868 /// 5. **Verified**: Confirms event is served by relay
869 /// 1009 ///
870 /// # Returns 1010 /// # Returns
871 /// The state event (kind 30618) after all stages complete successfully 1011 /// The state event (kind 30618) after git data is pushed and purgatory is released
872 async fn build_owner_state_data_pushed(&self) -> Result<Event> { 1012 async fn build_owner_state_data_pushed(&self) -> Result<Event> {
873 use nostr_sdk::prelude::*; 1013 use nostr_sdk::prelude::*;
874 1014
875 // ============================================================ 1015 // OwnerRepoStateSent is ensured by ensure_fixture before this is called.
876 // Stage 1: ValidRepo is ensured by ensure_fixture before this is called 1016 // The state event is already on the relay in purgatory - retrieve it from cache.
877 // ============================================================ 1017 let state_event = self.get_cached_dependency(FixtureKind::OwnerRepoStateSent)?;
878 let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; 1018 let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?;
879 let repo_id = self.extract_repo_id(&repo)?; 1019 let repo_id = self.extract_repo_id(&repo)?;
880 1020
881 // Build state event
882 let base_time = Timestamp::now().as_secs();
883 let older_timestamp = Timestamp::from(base_time - 10); // 10 seconds ago
884
885 let state_event = self
886 .client
887 .event_builder(Kind::RepoState, "")
888 .tag(Tag::identifier(&repo_id))
889 .tag(Tag::custom(
890 TagKind::custom("refs/heads/main"),
891 vec![DETERMINISTIC_COMMIT_HASH.to_string()],
892 ))
893 .tag(Tag::custom(
894 TagKind::custom("HEAD"),
895 vec!["ref: refs/heads/main".to_string()],
896 ))
897 .custom_time(older_timestamp)
898 .build(self.client.keys())
899 .map_err(|e| anyhow::anyhow!("Failed to build state announcement: {}", e))?;
900
901 // ============================================================
902 // Stage 2 & 3: Send to Relay, get Accepted response and Verify its Not Served
903 // ============================================================
904 self.client
905 .send_event_expect_purgatory_not_served(state_event.clone())
906 .await?;
907
908 // ============================================================ 1021 // ============================================================
909 // Stage 4: DataPushed - Clone repo, create commit, push 1022 // Stage 1: DataPushed - Clone repo, create commit, push
910 // ============================================================ 1023 // ============================================================
911 1024
912 // Get relay domain from connected relay 1025 // Get relay domain from connected relay
@@ -1008,7 +1121,7 @@ impl<'a> TestContext<'a> {
1008 } 1121 }
1009 1122
1010 // ============================================================ 1123 // ============================================================
1011 // Stage 5: Verify state event is on relay 1124 // Stage 2: Verify state event is released from purgatory
1012 // ============================================================ 1125 // ============================================================
1013 1126
1014 tokio::time::sleep(Duration::from_millis(200)).await; 1127 tokio::time::sleep(Duration::from_millis(200)).await;
@@ -1048,8 +1161,8 @@ impl<'a> TestContext<'a> {
1048 // Extract repo_id from owner's state event (same d-tag structure) 1161 // Extract repo_id from owner's state event (same d-tag structure)
1049 let repo_id = self.extract_repo_id(&owner_state)?; 1162 let repo_id = self.extract_repo_id(&owner_state)?;
1050 1163
1051 // Get the repo (ValidRepo, also cached) for the owner's npub 1164 // Get the repo (ValidRepoSent, also cached) for the owner's npub
1052 let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; 1165 let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?;
1053 1166
1054 // Build maintainer's state event (state event ONLY - no announcement) 1167 // Build maintainer's state event (state event ONLY - no announcement)
1055 let base_time = Timestamp::now().as_secs(); 1168 let base_time = Timestamp::now().as_secs();
@@ -1074,9 +1187,11 @@ impl<'a> TestContext<'a> {
1074 // ============================================================ 1187 // ============================================================
1075 // Stage 2 & 3: Send to Relay, get Accepted response and Verify its Not Served 1188 // Stage 2 & 3: Send to Relay, get Accepted response and Verify its Not Served
1076 // ============================================================ 1189 // ============================================================
1077 self.client 1190 let (_, _in_purgatory) = self
1078 .send_event_expect_purgatory_not_served(maintainer_state_event.clone()) 1191 .client
1192 .send_event_and_note_purgatory(maintainer_state_event.clone())
1079 .await?; 1193 .await?;
1194 // Note: We don't fail if purgatory wasn't observed - the fixture proceeds regardless
1080 1195
1081 // ============================================================ 1196 // ============================================================
1082 // Stage 4: DataPushed - Clone repo, create maintainer commit, push 1197 // Stage 4: DataPushed - Clone repo, create maintainer commit, push
@@ -1194,7 +1309,7 @@ impl<'a> TestContext<'a> {
1194 /// recursive maintainer force-pushes their commit on top. 1309 /// recursive maintainer force-pushes their commit on top.
1195 /// 1310 ///
1196 /// This handles all stages of the fixture: 1311 /// This handles all stages of the fixture:
1197 /// 1. **Generated**: (MaintainerStateDataPushed dependency includes ValidRepo + OwnerStateDataPushed) 1312 /// 1. **Generated**: (MaintainerStateDataPushed dependency includes ValidRepoSent + OwnerStateDataPushed)
1198 /// Creates MaintainerAnnouncement + RecursiveMaintainerState 1313 /// Creates MaintainerAnnouncement + RecursiveMaintainerState
1199 /// 2. **Sent**: Sends events to relay (returns OK, accepted but 'purgatory:...' message) 1314 /// 2. **Sent**: Sends events to relay (returns OK, accepted but 'purgatory:...' message)
1200 /// 3. **Verify Not Served**: Confirms event is not served by relays 1315 /// 3. **Verify Not Served**: Confirms event is not served by relays
@@ -1215,8 +1330,8 @@ impl<'a> TestContext<'a> {
1215 // Extract repo_id from maintainer's state event (same d-tag structure) 1330 // Extract repo_id from maintainer's state event (same d-tag structure)
1216 let repo_id = self.extract_repo_id(&maintainer_state)?; 1331 let repo_id = self.extract_repo_id(&maintainer_state)?;
1217 1332
1218 // Get the repo (ValidRepo, also cached) for the owner's npub 1333 // Get the repo (ValidRepoSent, also cached) for the owner's npub
1219 let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; 1334 let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?;
1220 1335
1221 // ============================================================ 1336 // ============================================================
1222 // Stage 1 (continued): Generate MaintainerAnnouncement and RecursiveMaintainerState 1337 // Stage 1 (continued): Generate MaintainerAnnouncement and RecursiveMaintainerState
@@ -1249,9 +1364,11 @@ impl<'a> TestContext<'a> {
1249 // ============================================================ 1364 // ============================================================
1250 // Stage 2 & 3: Send to Relay, get Accepted response and Verify its Not Served 1365 // Stage 2 & 3: Send to Relay, get Accepted response and Verify its Not Served
1251 // ============================================================ 1366 // ============================================================
1252 self.client 1367 let (_, _in_purgatory) = self
1253 .send_event_expect_purgatory_not_served(recursive_maintainer_state_event.clone()) 1368 .client
1369 .send_event_and_note_purgatory(recursive_maintainer_state_event.clone())
1254 .await?; 1370 .await?;
1371 // Note: We don't fail if purgatory wasn't observed - the fixture proceeds regardless
1255 1372
1256 // ============================================================ 1373 // ============================================================
1257 // Stage 4: DataPushed - Clone repo, create recursive maintainer commit, push 1374 // Stage 4: DataPushed - Clone repo, create recursive maintainer commit, push
@@ -1428,7 +1545,7 @@ impl<'a> TestContext<'a> {
1428 /// 3. A wrong commit is pushed to refs/nostr/<pr-event-id> 1545 /// 3. A wrong commit is pushed to refs/nostr/<pr-event-id>
1429 /// 1546 ///
1430 /// Server state after: 1547 /// Server state after:
1431 /// - ValidRepo announcement on relay 1548 /// - ValidRepoSent announcement on relay
1432 /// - refs/nostr/<pr-event-id> on git server pointing to DETERMINISTIC_COMMIT_HASH (wrong) 1549 /// - refs/nostr/<pr-event-id> on git server pointing to DETERMINISTIC_COMMIT_HASH (wrong)
1433 /// - NO PR event on relay 1550 /// - NO PR event on relay
1434 /// 1551 ///
@@ -1440,8 +1557,8 @@ impl<'a> TestContext<'a> {
1440 let pr_event = self.get_cached_dependency(FixtureKind::PREventGenerated)?; 1557 let pr_event = self.get_cached_dependency(FixtureKind::PREventGenerated)?;
1441 let pr_event_id = pr_event.id.to_hex(); 1558 let pr_event_id = pr_event.id.to_hex();
1442 1559
1443 // Get the ValidRepo to extract repo info 1560 // Get the ValidRepoServed to extract repo info
1444 let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; 1561 let repo = self.get_cached_dependency(FixtureKind::ValidRepoServed)?;
1445 let repo_id = self.extract_repo_id(&repo)?; 1562 let repo_id = self.extract_repo_id(&repo)?;
1446 1563
1447 // Get relay domain for cloning 1564 // Get relay domain for cloning
@@ -1462,10 +1579,14 @@ impl<'a> TestContext<'a> {
1462 let _ = fs::remove_dir_all(path); 1579 let _ = fs::remove_dir_all(path);
1463 }; 1580 };
1464 1581
1465 // Create a WRONG commit (Owner variant, not PRTestCommit) 1582 // Create a WRONG commit using a unique file (not PRTestCommit)
1466 // This commit hash will NOT match what's in the PR event's `c` tag 1583 // We use create_commit (non-deterministic) so it always succeeds even if the
1584 // repo already has a commit (e.g. from OwnerStateDataPushed) with the same
1585 // deterministic content. The only requirement is that the hash differs from
1586 // PR_TEST_COMMIT_HASH, which is guaranteed since PR_TEST_COMMIT_HASH is a
1587 // deterministic root-commit with specific content and dates.
1467 let wrong_commit_hash = 1588 let wrong_commit_hash =
1468 match create_deterministic_commit_with_variant(&clone_path, CommitVariant::Owner) { 1589 match create_commit(&clone_path, "wrong commit - not the PR test commit") {
1469 Ok(h) => h, 1590 Ok(h) => h,
1470 Err(e) => { 1591 Err(e) => {
1471 cleanup(&clone_path); 1592 cleanup(&clone_path);
@@ -1520,7 +1641,7 @@ impl<'a> TestContext<'a> {
1520 /// 1641 ///
1521 /// This fixture builds on PRWrongCommitPushedBeforeEvent by sending the PR event. 1642 /// This fixture builds on PRWrongCommitPushedBeforeEvent by sending the PR event.
1522 /// After this fixture, the relay has: 1643 /// After this fixture, the relay has:
1523 /// - ValidRepo announcement 1644 /// - ValidRepoServed announcement
1524 /// - PR event 1645 /// - PR event
1525 /// - refs/nostr/<pr-event-id> may have been cleaned up (that's what tests verify) 1646 /// - refs/nostr/<pr-event-id> may have been cleaned up (that's what tests verify)
1526 /// 1647 ///
@@ -1539,6 +1660,173 @@ impl<'a> TestContext<'a> {
1539 Ok(pr_event) 1660 Ok(pr_event)
1540 } 1661 }
1541 1662
1663 /// Build PREvent2Generated fixture
1664 ///
1665 /// Creates a PR event with `c` tag pointing to PR_TEST_COMMIT_HASH_2.
1666 /// The event is NOT sent to the relay.
1667 async fn build_pr_event_2_generated(&self) -> Result<Event> {
1668 use nostr_sdk::prelude::*;
1669
1670 let repo = self.get_cached_dependency(FixtureKind::ValidRepoServed)?;
1671 let repo_id = self.extract_repo_id(&repo)?;
1672
1673 let base_time = Timestamp::now().as_secs();
1674 let pr_timestamp = Timestamp::from(base_time - 1);
1675
1676 self.client
1677 .event_builder(Kind::GitPullRequest, "Test PR 2 for GRASP validation")
1678 .tag(Tag::custom(
1679 TagKind::custom("a"),
1680 vec![format!(
1681 "30617:{}:{}",
1682 self.client.public_key().to_hex(),
1683 repo_id
1684 )],
1685 ))
1686 .tag(Tag::custom(
1687 TagKind::custom("c"),
1688 vec![PR_TEST_COMMIT_HASH_2.to_string()],
1689 ))
1690 .custom_time(pr_timestamp)
1691 .build(self.client.pr_author_keys())
1692 .map_err(|e| anyhow::anyhow!("Failed to build PR event 2: {}", e))
1693 }
1694
1695 /// Build PREvent2Sent fixture
1696 ///
1697 /// Sends the PR event to relay. Event should enter purgatory.
1698 async fn build_pr_event_2_sent(&self) -> Result<Event> {
1699 let pr_event = self.get_cached_dependency(FixtureKind::PREvent2Generated)?;
1700
1701 let (_, in_purgatory) = self
1702 .client
1703 .send_event_and_note_purgatory(pr_event.clone())
1704 .await?;
1705
1706 if !in_purgatory {
1707 return Err(anyhow::anyhow!(
1708 "PR event 2 was served immediately - purgatory not implemented"
1709 ));
1710 }
1711
1712 Ok(pr_event)
1713 }
1714
1715 /// Build PREvent2GitDataPushed fixture
1716 ///
1717 /// Pushes correct commit to refs/nostr/<pr-event-id> after event was sent.
1718 async fn build_pr_event_2_git_data_pushed(&self) -> Result<Event> {
1719 use nostr_sdk::prelude::*;
1720
1721 let pr_event = self.get_cached_dependency(FixtureKind::PREvent2Sent)?;
1722 let pr_event_id = pr_event.id.to_hex();
1723
1724 let repo = self.get_cached_dependency(FixtureKind::ValidRepoServed)?;
1725 let repo_id = self.extract_repo_id(&repo)?;
1726
1727 let relay_domain = self.get_relay_domain().await?;
1728
1729 let npub = repo
1730 .pubkey
1731 .to_bech32()
1732 .map_err(|e| anyhow::anyhow!("Failed to convert pubkey: {}", e))?;
1733
1734 let clone_path = clone_repo(&relay_domain, &npub, &repo_id)
1735 .map_err(|e| anyhow::anyhow!("Failed to clone repo: {}", e))?;
1736
1737 let cleanup = |path: &PathBuf| {
1738 let _ = fs::remove_dir_all(path);
1739 };
1740
1741 // Reset to orphan state and create deterministic root commit
1742 // Step 1: Create orphan branch (removes all history)
1743 let _ = Command::new("git")
1744 .args(["checkout", "--orphan", "pr-branch"])
1745 .current_dir(&clone_path)
1746 .output();
1747
1748 // Step 2: Clear staged files (orphan keeps files staged from previous branch)
1749 let _ = Command::new("git")
1750 .args(["rm", "-rf", "--cached", "."])
1751 .current_dir(&clone_path)
1752 .output();
1753
1754 // Step 3: Remove all working directory files for clean state (except .git)
1755 for entry in
1756 fs::read_dir(&clone_path).map_err(|e| anyhow::anyhow!("Failed to read dir: {}", e))?
1757 {
1758 if let Ok(entry) = entry {
1759 let path = entry.path();
1760 if path.file_name() != Some(std::ffi::OsStr::new(".git")) {
1761 let _ = fs::remove_file(&path).or_else(|_| fs::remove_dir_all(&path));
1762 }
1763 }
1764 }
1765
1766 let commit_hash = match create_deterministic_commit_with_variant(
1767 &clone_path,
1768 CommitVariant::PRTestCommit2,
1769 ) {
1770 Ok(h) => h,
1771 Err(e) => {
1772 cleanup(&clone_path);
1773 return Err(anyhow::anyhow!("Failed to create PR test commit 2: {}", e));
1774 }
1775 };
1776
1777 if commit_hash != PR_TEST_COMMIT_HASH_2 {
1778 cleanup(&clone_path);
1779 return Err(anyhow::anyhow!(
1780 "PR test commit 2 hash mismatch: got {}, expected {}",
1781 commit_hash,
1782 PR_TEST_COMMIT_HASH_2
1783 ));
1784 }
1785
1786 let push_output = Command::new("git")
1787 .args([
1788 "push",
1789 "origin",
1790 &format!("pr-branch:refs/nostr/{}", pr_event_id),
1791 ])
1792 .current_dir(&clone_path)
1793 .output()
1794 .map_err(|e| {
1795 cleanup(&clone_path);
1796 anyhow::anyhow!("Failed to execute git push: {}", e)
1797 })?;
1798
1799 cleanup(&clone_path);
1800
1801 if !push_output.status.success() {
1802 let stderr = String::from_utf8_lossy(&push_output.stderr);
1803 return Err(anyhow::anyhow!(
1804 "Push to refs/nostr/{} failed: {}",
1805 pr_event_id,
1806 stderr
1807 ));
1808 }
1809
1810 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
1811
1812 Ok(pr_event)
1813 }
1814
1815 /// Build PREvent2Served fixture
1816 ///
1817 /// Full fixture: event sent, git pushed, event now served.
1818 async fn build_pr_event_2_served(&self) -> Result<Event> {
1819 let pr_event = self.get_cached_dependency(FixtureKind::PREvent2GitDataPushed)?;
1820
1821 if !self.client.is_event_on_relay(pr_event.id).await? {
1822 return Err(anyhow::anyhow!(
1823 "PR event 2 not released from purgatory after git push"
1824 ));
1825 }
1826
1827 Ok(pr_event)
1828 }
1829
1542 /// Get relay domain (host:port) from the connected relay 1830 /// Get relay domain (host:port) from the connected relay
1543 /// 1831 ///
1544 /// Extracts the domain from the relay URL for git HTTP operations. 1832 /// Extracts the domain from the relay URL for git HTTP operations.
@@ -1845,16 +2133,19 @@ pub enum CommitVariant {
1845 RecursiveMaintainer, 2133 RecursiveMaintainer,
1846 /// PR test commit variant - for PR event tests 2134 /// PR test commit variant - for PR event tests
1847 PRTestCommit, 2135 PRTestCommit,
2136 /// Second PR test commit variant - for second PR event tests
2137 PRTestCommit2,
1848} 2138}
1849 2139
1850impl CommitVariant { 2140impl CommitVariant {
1851 /// Get the file content for this variant 2141 /// Get the file content for this variant
1852 pub fn file_content(&self) -> &'static str { 2142 pub fn file_content(&self) -> &'static str {
1853 match self { 2143 match self {
1854 CommitVariant::Owner => "Initial commit", 2144 CommitVariant::Owner => "Initial commit\n",
1855 CommitVariant::Maintainer => "Maintainer initial commit", 2145 CommitVariant::Maintainer => "Maintainer initial commit\n",
1856 CommitVariant::RecursiveMaintainer => "Recursive maintainer initial commit", 2146 CommitVariant::RecursiveMaintainer => "Recursive maintainer initial commit\n",
1857 CommitVariant::PRTestCommit => "PR test deterministic commit", 2147 CommitVariant::PRTestCommit => "PR test deterministic commit\n",
2148 CommitVariant::PRTestCommit2 => "PR test deterministic commit 2\n",
1858 } 2149 }
1859 } 2150 }
1860 2151
@@ -1865,6 +2156,7 @@ impl CommitVariant {
1865 CommitVariant::Maintainer => "Maintainer initial commit", 2156 CommitVariant::Maintainer => "Maintainer initial commit",
1866 CommitVariant::RecursiveMaintainer => "Recursive maintainer initial commit", 2157 CommitVariant::RecursiveMaintainer => "Recursive maintainer initial commit",
1867 CommitVariant::PRTestCommit => "PR test deterministic commit", 2158 CommitVariant::PRTestCommit => "PR test deterministic commit",
2159 CommitVariant::PRTestCommit2 => "PR test deterministic commit 2",
1868 } 2160 }
1869 } 2161 }
1870} 2162}
@@ -2040,10 +2332,10 @@ mod tests {
2040 use std::collections::HashSet; 2332 use std::collections::HashSet;
2041 2333
2042 let mut set = HashSet::new(); 2334 let mut set = HashSet::new();
2043 set.insert(FixtureKind::ValidRepo); 2335 set.insert(FixtureKind::ValidRepoSent);
2044 set.insert(FixtureKind::RepoWithIssue); 2336 set.insert(FixtureKind::RepoWithIssue);
2045 2337
2046 assert!(set.contains(&FixtureKind::ValidRepo)); 2338 assert!(set.contains(&FixtureKind::ValidRepoSent));
2047 assert!(!set.contains(&FixtureKind::RepoWithComment)); 2339 assert!(!set.contains(&FixtureKind::RepoWithComment));
2048 } 2340 }
2049 2341