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>2026-01-07 15:39:48 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-01-07 15:39:48 +0000
commit1db877d53c4ff45971c69fecc5165c352ec316c9 (patch)
tree4e63366e49430320117ac7d207b9c2f034f8f7b5
parent7dcbc84806e7b3000835eb9132dfc4e9003e382a (diff)
test: add test_state_event_syncs_from_remote integration test
Implements Phase 3 of the purgatory sync integration test plan. Key changes: - Add immediate sync triggering for sync-received events that go to purgatory (instead of default 3-minute delay for user-submitted events) - TestRelay now respects RUST_LOG environment variable for debugging - New test verifies end-to-end flow: state event syncs from source relay, enters purgatory, git data is fetched from source's clone URL, and event is released and served
-rw-r--r--src/purgatory/mod.rs3
-rw-r--r--src/sync/mod.rs34
-rw-r--r--tests/common/relay.rs7
-rw-r--r--tests/purgatory_sync.rs157
4 files changed, 195 insertions, 6 deletions
diff --git a/src/purgatory/mod.rs b/src/purgatory/mod.rs
index 7045923..894c941 100644
--- a/src/purgatory/mod.rs
+++ b/src/purgatory/mod.rs
@@ -189,6 +189,8 @@ impl Purgatory {
189 /// 189 ///
190 /// Automatically enqueues the identifier for background sync with the default delay 190 /// Automatically enqueues the identifier for background sync with the default delay
191 /// (3 minutes), giving time for a git push to arrive after the nostr event. 191 /// (3 minutes), giving time for a git push to arrive after the nostr event.
192 /// For sync-triggered events, the SyncManager calls `enqueue_sync_immediate` separately
193 /// to override this delay.
192 /// 194 ///
193 /// # Arguments 195 /// # Arguments
194 /// * `event` - The state event (kind 30618) to hold 196 /// * `event` - The state event (kind 30618) to hold
@@ -210,6 +212,7 @@ impl Purgatory {
210 .push(entry); 212 .push(entry);
211 213
212 // Enqueue for background sync with default delay 214 // Enqueue for background sync with default delay
215 // (SyncManager will call enqueue_sync_immediate for sync-triggered events)
213 self.enqueue_sync_default(&identifier); 216 self.enqueue_sync_default(&identifier);
214 } 217 }
215 218
diff --git a/src/sync/mod.rs b/src/sync/mod.rs
index b56b6b7..7d60ea4 100644
--- a/src/sync/mod.rs
+++ b/src/sync/mod.rs
@@ -1046,6 +1046,40 @@ impl SyncManager {
1046 } 1046 }
1047 } 1047 }
1048 1048
1049 // For sync-triggered events that go to purgatory, trigger immediate sync
1050 // (instead of the default 3-minute delay for user-submitted events)
1051 if result == ProcessResult::Purgatory {
1052 // State events (kind 30618) - extract identifier and trigger immediate sync
1053 if event.kind.as_u16() == 30618 {
1054 if let Some(identifier) = event.tags.iter().find_map(|tag| {
1055 let tag_vec = tag.clone().to_vec();
1056 if tag_vec.len() >= 2 && tag_vec[0] == "d" {
1057 Some(tag_vec[1].clone())
1058 } else {
1059 None
1060 }
1061 }) {
1062 tracing::debug!(
1063 event_id = %event.id,
1064 identifier = %identifier,
1065 "Triggering immediate sync for synced state event in purgatory"
1066 );
1067 write_policy.purgatory().enqueue_sync_immediate(&identifier);
1068 }
1069 }
1070 // PR events (kind 1617/1618) - extract identifier from 'a' tag
1071 else if event.kind.as_u16() == 1617 || event.kind.as_u16() == 1618 {
1072 if let Some(identifier) = crate::git::sync::extract_identifier_from_pr_event(&event) {
1073 tracing::debug!(
1074 event_id = %event.id,
1075 identifier = %identifier,
1076 "Triggering immediate sync for synced PR event in purgatory"
1077 );
1078 write_policy.purgatory().enqueue_sync_immediate(&identifier);
1079 }
1080 }
1081 }
1082
1049 // Track pagination state for this subscription 1083 // Track pagination state for this subscription
1050 if result == ProcessResult::Saved || result == ProcessResult::Duplicate { 1084 if result == ProcessResult::Saved || result == ProcessResult::Duplicate {
1051 let mut pending = pending_sync_index.write().await; 1085 let mut pending = pending_sync_index.write().await;
diff --git a/tests/common/relay.rs b/tests/common/relay.rs
index 55cc18e..8d20da6 100644
--- a/tests/common/relay.rs
+++ b/tests/common/relay.rs
@@ -144,10 +144,9 @@ impl TestRelay {
144 .env("NGIT_SYNC_STARTUP_JITTER_MS", "0") // No jitter for tests 144 .env("NGIT_SYNC_STARTUP_JITTER_MS", "0") // No jitter for tests
145 .env("NGIT_SYNC_DISCONNECT_CHECK_INTERVAL_SECS", "1") // Fast reconnect attempts for tests 145 .env("NGIT_SYNC_DISCONNECT_CHECK_INTERVAL_SECS", "1") // Fast reconnect attempts for tests
146 .env("NGIT_SYNC_BASE_BACKOFF_SECS", "1") // Fast backoff for tests (1s instead of 5s default) 146 .env("NGIT_SYNC_BASE_BACKOFF_SECS", "1") // Fast backoff for tests (1s instead of 5s default)
147 .env("RUST_LOG", "info") // Enable INFO logging for diagnostics 147 .env("RUST_LOG", std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string())) // Use RUST_LOG from environment or default to info
148 .stdout(Stdio::null()) // Disable stderr for cleaner test output 148 .stdout(Stdio::null()) // Suppress stdout for cleaner test output
149 // .stdout(Stdio::inherit()) // Show stdout for diagnostics 149 .stderr(Stdio::null()); // Suppress stderr for cleaner test output
150 .stderr(Stdio::null()); // Disable stderr for cleaner test output
151 150
152 // Add bootstrap relay URL if provided 151 // Add bootstrap relay URL if provided
153 if let Some(ref bootstrap_url) = bootstrap_relay_url { 152 if let Some(ref bootstrap_url) = bootstrap_relay_url {
diff --git a/tests/purgatory_sync.rs b/tests/purgatory_sync.rs
index 1065c9d..3da086c 100644
--- a/tests/purgatory_sync.rs
+++ b/tests/purgatory_sync.rs
@@ -28,8 +28,9 @@
28mod common; 28mod common;
29 29
30use common::{ 30use common::{
31 create_repo_announcement, create_state_event, create_test_repo_with_commit, push_to_relay, 31 check_ref_at_commit, create_repo_announcement, create_state_event,
32 verify_event_not_served, wait_for_event_served, CommitVariant, TestRelay, 32 create_test_repo_with_commit, push_to_relay, verify_event_not_served, wait_for_event_served,
33 wait_for_sync_connection, CommitVariant, TestRelay,
33}; 34};
34use nostr_sdk::prelude::*; 35use nostr_sdk::prelude::*;
35use std::time::Duration; 36use std::time::Duration;
@@ -124,3 +125,155 @@ async fn test_push_triggers_unified_processing() {
124 client.disconnect().await; 125 client.disconnect().await;
125 relay.stop().await; 126 relay.stop().await;
126} 127}
128
129/// Test that a state event entering purgatory triggers remote git fetch
130/// and is released once the git data is available.
131///
132/// Scenario:
133/// 1. Start source relay with git repository containing test commit
134/// 2. Start syncing relay that syncs from source
135/// 3. Syncing relay syncs state event (goes to purgatory - no local git data)
136/// 4. Wait for sync to fetch git data from source's clone URL
137/// 5. Verify state event is released and served on syncing relay
138#[tokio::test]
139async fn test_state_event_syncs_from_remote() {
140 // 1. Start source relay
141 let source_relay = TestRelay::start().await;
142 let keys = Keys::generate();
143 let identifier = "state-sync-test-repo";
144
145 // Pre-allocate syncing relay port so we can include it in announcement
146 let syncing_port = TestRelay::find_free_port();
147 let syncing_domain = format!("127.0.0.1:{}", syncing_port);
148
149 // 2. Create test repository locally with deterministic commit
150 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
151 let commit_hash = create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest)
152 .expect("Failed to create test repo");
153
154 let npub = keys.public_key().to_bech32().expect("Failed to get npub");
155
156 // 3. Create and send announcement listing BOTH relays
157 // This ensures the syncing relay will accept the state event when it syncs
158 let announcement = create_repo_announcement(
159 &keys,
160 &[&source_relay.domain(), &syncing_domain],
161 identifier,
162 );
163
164 let source_client = Client::new(keys.clone());
165 source_client
166 .add_relay(source_relay.url())
167 .await
168 .expect("Failed to add source relay");
169 source_client.connect().await;
170
171 // Wait for connection
172 tokio::time::sleep(Duration::from_millis(500)).await;
173
174 // Send announcement to source relay
175 source_client
176 .send_event(&announcement)
177 .await
178 .expect("Failed to send announcement to source");
179
180 tokio::time::sleep(Duration::from_millis(200)).await;
181
182 // 4. Create and send state event BEFORE pushing
183 // The state event goes to purgatory on source relay, which authorizes the push
184 let clone_urls = [
185 format!(
186 "http://{}/{}/{}.git",
187 source_relay.domain(),
188 npub,
189 identifier
190 ),
191 format!("http://{}/{}/{}.git", syncing_domain, npub, identifier),
192 ];
193 let relay_urls = [
194 source_relay.url().to_string(),
195 format!("ws://{}", syncing_domain),
196 ];
197
198 let state_event = create_state_event(
199 &keys,
200 identifier,
201 &[("main", &commit_hash)],
202 &[],
203 &[&clone_urls[0], &clone_urls[1]],
204 &[&relay_urls[0], &relay_urls[1]],
205 )
206 .expect("Failed to create state event");
207
208 let state_event_id = state_event.id;
209
210 // Send state event to source relay (goes to purgatory - no git data yet)
211 source_client
212 .send_event(&state_event)
213 .await
214 .expect("Failed to send state event to source");
215
216 tokio::time::sleep(Duration::from_millis(200)).await;
217
218 // 5. Push git data to source relay
219 // The state event in purgatory authorizes this push
220 push_to_relay(temp_dir.path(), &source_relay.domain(), &npub, identifier)
221 .expect("Push to source should succeed");
222
223 // After push, state event should be released from purgatory on source relay
224 // Verify source relay is serving the state event
225 wait_for_event_served(source_relay.url(), &state_event_id, Duration::from_secs(5))
226 .await
227 .expect("State event should be served on source relay after push");
228
229 // 6. Start syncing relay (syncs from source)
230 let syncing_relay = TestRelay::start_on_port_with_options(
231 syncing_port,
232 Some(source_relay.url().to_string()),
233 false,
234 )
235 .await;
236
237 // Wait for sync connection to establish
238 wait_for_sync_connection(syncing_relay.url(), 1, Duration::from_secs(5))
239 .await
240 .expect("Sync connection should establish");
241
242 // 7. Wait for state event to be released on syncing relay
243 // The sync should:
244 // a) Fetch the announcement and state event from source relay
245 // b) Accept announcement (creates bare repo structure)
246 // c) Put state event in purgatory (git data missing on syncing relay)
247 // d) Fetch git data from source relay's clone URL
248 // e) Release the state event from purgatory
249 let found = wait_for_event_served(
250 syncing_relay.url(),
251 &state_event_id,
252 Duration::from_secs(30), // Allow time for sync + git fetch
253 )
254 .await;
255
256 assert!(
257 found.is_ok(),
258 "State event should be served after sync fetches git data: {:?}",
259 found.err()
260 );
261
262 // 8. Verify refs are correct on syncing relay
263 let ref_correct = check_ref_at_commit(
264 &syncing_domain,
265 &npub,
266 identifier,
267 "refs/heads/main",
268 &commit_hash,
269 )
270 .await
271 .expect("Failed to check ref");
272
273 assert!(ref_correct, "main branch should point to correct commit");
274
275 // Cleanup
276 source_client.disconnect().await;
277 syncing_relay.stop().await;
278 source_relay.stop().await;
279}