upleb.uk

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

summaryrefslogtreecommitdiff
path: root/tests/common
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 /tests/common
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 'tests/common')
-rw-r--r--tests/common/purgatory_helpers.rs38
-rw-r--r--tests/common/relay.rs13
-rw-r--r--tests/common/sync_helpers.rs423
3 files changed, 432 insertions, 42 deletions
diff --git a/tests/common/purgatory_helpers.rs b/tests/common/purgatory_helpers.rs
index 1d06f22..cfcea1c 100644
--- a/tests/common/purgatory_helpers.rs
+++ b/tests/common/purgatory_helpers.rs
@@ -338,6 +338,44 @@ pub fn build_repo_coord(keys: &Keys, identifier: &str) -> String {
338 format!("30617:{}:{}", keys.public_key().to_hex(), identifier) 338 format!("30617:{}:{}", keys.public_key().to_hex(), identifier)
339} 339}
340 340
341/// Create a repository announcement event (kind 30617) for purgatory tests.
342///
343/// Creates a minimal but valid NIP-34 repository announcement with a `d` tag,
344/// optional `clone` URLs, and optional `relays` URLs.
345///
346/// # Arguments
347/// * `keys` - Keys for signing
348/// * `identifier` - Repository identifier (d-tag)
349/// * `clone_urls` - Clone URLs to include (may be empty)
350/// * `relay_urls` - Relay URLs to include (may be empty)
351///
352/// # Returns
353/// * `Ok(Event)` - Signed announcement event
354/// * `Err(String)` - If signing fails
355pub fn create_announcement_event(
356 keys: &Keys,
357 identifier: &str,
358 clone_urls: &[&str],
359 relay_urls: &[&str],
360) -> Result<Event, String> {
361 let mut tags = vec![Tag::identifier(identifier)];
362
363 if !clone_urls.is_empty() {
364 let urls: Vec<String> = clone_urls.iter().map(|s| s.to_string()).collect();
365 tags.push(Tag::custom(TagKind::custom("clone"), urls));
366 }
367
368 if !relay_urls.is_empty() {
369 let urls: Vec<String> = relay_urls.iter().map(|s| s.to_string()).collect();
370 tags.push(Tag::custom(TagKind::custom("relays"), urls));
371 }
372
373 EventBuilder::new(Kind::GitRepoAnnouncement, "")
374 .tags(tags)
375 .sign_with_keys(keys)
376 .map_err(|e| format!("Failed to sign announcement event: {}", e))
377}
378
341/// Wait for an event to be served by a relay (not in purgatory). 379/// Wait for an event to be served by a relay (not in purgatory).
342/// 380///
343/// Polls the relay until the event is queryable, indicating it has 381/// Polls the relay until the event is queryable, indicating it has
diff --git a/tests/common/relay.rs b/tests/common/relay.rs
index 227849a..b1e96cf 100644
--- a/tests/common/relay.rs
+++ b/tests/common/relay.rs
@@ -204,7 +204,7 @@ impl TestRelay {
204 .env("NGIT_GIT_DATA_PATH", git_data_dir.path()) 204 .env("NGIT_GIT_DATA_PATH", git_data_dir.path())
205 .env("NGIT_DATABASE_BACKEND", "memory") // Force in-memory database for isolation 205 .env("NGIT_DATABASE_BACKEND", "memory") // Force in-memory database for isolation
206 .env("NGIT_OWNER_NPUB", &test_npub) 206 .env("NGIT_OWNER_NPUB", &test_npub)
207 .env("NGIT_SYNC_BATCH_WINDOW_MS", "200") // Fast batch window for tests (200ms instead of 5s default) 207 .env("NGIT_TEST", "1") // Enable test mode: fast timers (200ms batch window, 200ms purgatory sync)
208 .env("NGIT_SYNC_STARTUP_DELAY_SECS", "0") // No startup delay for faster tests 208 .env("NGIT_SYNC_STARTUP_DELAY_SECS", "0") // No startup delay for faster tests
209 .env("NGIT_SYNC_STARTUP_JITTER_MS", "0") // No jitter for tests 209 .env("NGIT_SYNC_STARTUP_JITTER_MS", "0") // No jitter for tests
210 .env("NGIT_SYNC_DISCONNECT_CHECK_INTERVAL_SECS", "1") // Fast reconnect attempts for tests 210 .env("NGIT_SYNC_DISCONNECT_CHECK_INTERVAL_SECS", "1") // Fast reconnect attempts for tests
@@ -213,8 +213,15 @@ impl TestRelay {
213 "RUST_LOG", 213 "RUST_LOG",
214 std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()), 214 std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
215 ) // Use RUST_LOG from environment or default to info 215 ) // Use RUST_LOG from environment or default to info
216 .stdout(Stdio::null()) // Suppress stdout for cleaner test output 216 .stdout(
217 .stderr(Stdio::null()); // Suppress stderr for cleaner test output 217 std::fs::OpenOptions::new()
218 .create(true)
219 .append(true)
220 .open(format!("/tmp/relay-{}.log", port))
221 .map(Stdio::from)
222 .unwrap_or(Stdio::null()),
223 )
224 .stderr(Stdio::inherit()); // Inherit stderr for test output
218 225
219 // Add bootstrap relay URL if provided 226 // Add bootstrap relay URL if provided
220 if let Some(ref bootstrap_url) = bootstrap_relay_url { 227 if let Some(ref bootstrap_url) = bootstrap_relay_url {
diff --git a/tests/common/sync_helpers.rs b/tests/common/sync_helpers.rs
index 5fc2ad7..af51e78 100644
--- a/tests/common/sync_helpers.rs
+++ b/tests/common/sync_helpers.rs
@@ -507,41 +507,53 @@ fn check_sync_connections_in_metrics(metrics: &str, expected: usize) -> bool {
507/// assert!(found, "Expected event {} to sync to relay", event.id); 507/// assert!(found, "Expected event {} to sync to relay", event.id);
508/// ``` 508/// ```
509pub async fn wait_for_event_on_relay(relay_url: &str, filter: Filter, timeout: Duration) -> bool { 509pub async fn wait_for_event_on_relay(relay_url: &str, filter: Filter, timeout: Duration) -> bool {
510 // Create a temporary client for querying 510 let deadline = tokio::time::Instant::now() + timeout;
511 let temp_keys = Keys::generate(); 511 let poll_interval = Duration::from_millis(200);
512 let client = Client::new(temp_keys);
513
514 // Try to connect
515 if client.add_relay(relay_url).await.is_err() {
516 return false;
517 }
518 512
519 client.connect().await; 513 loop {
514 // Create a fresh client for each poll attempt (avoids stale connection state)
515 let temp_keys = Keys::generate();
516 let client = Client::new(temp_keys);
520 517
521 // Wait for connection (brief timeout) 518 if client.add_relay(relay_url).await.is_err() {
522 let mut connected = false; 519 if tokio::time::Instant::now() >= deadline {
523 for _ in 0..10 { 520 return false;
524 tokio::time::sleep(Duration::from_millis(100)).await; 521 }
525 let relays = client.relays().await; 522 tokio::time::sleep(poll_interval).await;
526 if relays.values().any(|r| r.is_connected()) { 523 continue;
527 connected = true;
528 break;
529 } 524 }
530 }
531 525
532 if !connected { 526 client.connect().await;
533 client.disconnect().await; 527
534 return false; 528 // Wait for connection
535 } 529 let mut connected = false;
530 for _ in 0..10 {
531 tokio::time::sleep(Duration::from_millis(100)).await;
532 let relays = client.relays().await;
533 if relays.values().any(|r| r.is_connected()) {
534 connected = true;
535 break;
536 }
537 }
536 538
537 // Fetch events with the provided timeout 539 if connected {
538 let result = client.fetch_events(filter, timeout).await; 540 // Use a short fetch window — if the event is there, EOSE comes back quickly
541 let fetch_timeout = Duration::from_millis(500);
542 let result = client.fetch_events(filter.clone(), fetch_timeout).await;
543 client.disconnect().await;
539 544
540 client.disconnect().await; 545 match result {
546 Ok(events) if !events.is_empty() => return true,
547 _ => {}
548 }
549 } else {
550 client.disconnect().await;
551 }
541 552
542 match result { 553 if tokio::time::Instant::now() >= deadline {
543 Ok(events) => !events.is_empty(), 554 return false;
544 Err(_) => false, 555 }
556 tokio::time::sleep(poll_interval).await;
545 } 557 }
546} 558}
547 559
@@ -774,6 +786,11 @@ impl MetricsTestHarness {
774 self.source_relays[idx].domain() 786 self.source_relays[idx].domain()
775 } 787 }
776 788
789 /// Get a reference to a source relay (for advanced test operations)
790 pub fn source_relay(&self, idx: usize) -> &TestRelay {
791 &self.source_relays[idx]
792 }
793
777 /// Submit events to a specific source relay 794 /// Submit events to a specific source relay
778 pub async fn submit_events(&self, source_idx: usize, events: &[Event]) -> Result<(), String> { 795 pub async fn submit_events(&self, source_idx: usize, events: &[Event]) -> Result<(), String> {
779 let relay = &self.source_relays[source_idx]; 796 let relay = &self.source_relays[source_idx];
@@ -1071,12 +1088,16 @@ pub struct SyncTestResult {
1071 pub syncing_relay: TestRelay, 1088 pub syncing_relay: TestRelay,
1072 pub maintainer_keys: Keys, 1089 pub maintainer_keys: Keys,
1073 pub repo_coord: String, 1090 pub repo_coord: String,
1091 // Keep SmartGitServer alive for the test duration
1092 _git_server: Option<super::git_server::SmartGitServer>,
1093 // Keep temp dir alive for the test duration
1094 _git_temp_dir: Option<tempfile::TempDir>,
1074} 1095}
1075 1096
1076/// Helper to send an event to a relay 1097/// Helper to send an event to a relay
1077/// 1098///
1078/// Creates a temporary client, sends the event, and disconnects. 1099/// Creates a temporary client, sends the event, and disconnects.
1079async fn send_to_relay(relay: &TestRelay, event: &Event) -> Result<(), String> { 1100pub async fn send_to_relay(relay: &TestRelay, event: &Event) -> Result<(), String> {
1080 let temp_keys = Keys::generate(); 1101 let temp_keys = Keys::generate();
1081 let client = TestClient::new(relay.url(), temp_keys).await?; 1102 let client = TestClient::new(relay.url(), temp_keys).await?;
1082 client.send_event(event).await?; 1103 client.send_event(event).await?;
@@ -1084,6 +1105,270 @@ async fn send_to_relay(relay: &TestRelay, event: &Event) -> Result<(), String> {
1084 Ok(()) 1105 Ok(())
1085} 1106}
1086 1107
1108/// Helper to send an event to a relay by URL
1109///
1110/// Creates a temporary client, sends the event, and disconnects.
1111pub async fn send_to_relay_url(relay_url: &str, event: &Event) -> Result<(), String> {
1112 let temp_keys = Keys::generate();
1113 let client = TestClient::new(relay_url, temp_keys).await?;
1114 client.send_event(event).await?;
1115 client.disconnect().await;
1116 Ok(())
1117}
1118
1119/// Push git repository data to a relay to release a purgatory-held announcement.
1120///
1121/// Creates a local git repo, sends a state event, and pushes to the relay.
1122/// Use this when you need to build a custom announcement but still need the
1123/// relay to accept it (i.e., release it from purgatory).
1124///
1125/// # Arguments
1126/// * `relay` - The relay to push to
1127/// * `keys` - Keys of the repository owner
1128/// * `identifier` - Repository identifier
1129/// * `domains` - All domains in the announcement (for state event URLs)
1130///
1131/// # Returns
1132/// `tempfile::TempDir` - Keep alive for test duration
1133pub async fn push_git_data_to_relay(
1134 relay: &TestRelay,
1135 keys: &Keys,
1136 identifier: &str,
1137 domains: &[&str],
1138) -> tempfile::TempDir {
1139 use super::purgatory_helpers::{
1140 create_state_event, create_test_repo_with_commit, push_to_relay, CommitVariant,
1141 };
1142
1143 let npub = keys
1144 .public_key()
1145 .to_bech32()
1146 .expect("Failed to convert public key to npub");
1147
1148 // Create local git repo
1149 let git_temp_dir = tempfile::tempdir().expect("Failed to create temp dir for git repo");
1150 let commit_hash = create_test_repo_with_commit(git_temp_dir.path(), CommitVariant::StateTest)
1151 .expect("Failed to create test git repo");
1152
1153 let clone_urls: Vec<String> = domains
1154 .iter()
1155 .map(|d| format!("http://{}/{}/{}.git", d, npub, identifier))
1156 .collect();
1157 let relay_urls: Vec<String> = domains.iter().map(|d| format!("ws://{}", d)).collect();
1158
1159 // Build and send state event with all domains' clone URLs
1160 let state_event = create_state_event(
1161 keys,
1162 identifier,
1163 &[("main", &commit_hash)],
1164 &[],
1165 &clone_urls.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
1166 &relay_urls.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
1167 )
1168 .expect("Failed to create state event");
1169
1170 send_to_relay(relay, &state_event)
1171 .await
1172 .expect("Failed to send state event");
1173
1174 // Git push to relay → releases state event from purgatory, authorizes push
1175 push_to_relay(git_temp_dir.path(), &relay.domain(), &npub, identifier)
1176 .expect("Failed to push git data to relay");
1177
1178 // Brief wait for push processing
1179 tokio::time::sleep(Duration::from_millis(500)).await;
1180
1181 git_temp_dir
1182}
1183
1184/// Like `push_git_data_to_relay` but writes a unique marker file so each call
1185/// produces a distinct commit hash.
1186///
1187/// Use this when multiple callers push to the same relay with the same identifier
1188/// but different keys — identical commit hashes cause git to skip pack transfer,
1189/// which can leave the announcement in purgatory.
1190///
1191/// # Arguments
1192/// * `relay` - The relay to push to
1193/// * `keys` - Keys of the repository owner
1194/// * `identifier` - Repository identifier
1195/// * `domains` - All domains in the announcement (for state event URLs)
1196/// * `unique_seed` - A string written into a `.unique` file to differentiate commits
1197///
1198/// # Returns
1199/// `tempfile::TempDir` - Keep alive for test duration
1200pub async fn push_unique_git_data_to_relay(
1201 relay: &TestRelay,
1202 keys: &Keys,
1203 identifier: &str,
1204 domains: &[&str],
1205 unique_seed: &str,
1206) -> tempfile::TempDir {
1207 use super::purgatory_helpers::{create_state_event, push_to_relay};
1208
1209 let npub = keys
1210 .public_key()
1211 .to_bech32()
1212 .expect("Failed to convert public key to npub");
1213
1214 let git_temp_dir = tempfile::tempdir().expect("Failed to create temp dir for git repo");
1215 let path = git_temp_dir.path();
1216
1217 fn git(path: &std::path::Path, args: &[&str]) {
1218 let status = std::process::Command::new("git")
1219 .args(args)
1220 .current_dir(path)
1221 .env("GIT_AUTHOR_NAME", "Test User")
1222 .env("GIT_AUTHOR_EMAIL", "test@example.com")
1223 .env("GIT_COMMITTER_NAME", "Test User")
1224 .env("GIT_COMMITTER_EMAIL", "test@example.com")
1225 .env("GIT_AUTHOR_DATE", "2024-01-01T00:00:00+00:00")
1226 .env("GIT_COMMITTER_DATE", "2024-01-01T00:00:00+00:00")
1227 .output()
1228 .unwrap_or_else(|e| panic!("git {:?} failed to spawn: {}", args, e));
1229 assert!(
1230 status.status.success(),
1231 "git {:?} failed: {}",
1232 args,
1233 String::from_utf8_lossy(&status.stderr)
1234 );
1235 }
1236
1237 git(path, &["init", "--initial-branch=main"]);
1238 git(path, &["config", "user.email", "test@example.com"]);
1239 git(path, &["config", "user.name", "Test User"]);
1240 git(path, &["config", "commit.gpgsign", "false"]);
1241
1242 // Write a unique file so each maintainer gets a distinct commit hash
1243 std::fs::write(path.join("state_test.txt"), "State test content for purgatory sync")
1244 .expect("write state_test.txt");
1245 std::fs::write(path.join(".unique"), unique_seed).expect("write .unique");
1246 git(path, &["add", "."]);
1247 git(path, &["commit", "-m", "State test commit"]);
1248
1249 let commit_hash = {
1250 let out = std::process::Command::new("git")
1251 .args(["rev-parse", "HEAD"])
1252 .current_dir(path)
1253 .output()
1254 .expect("git rev-parse");
1255 String::from_utf8_lossy(&out.stdout).trim().to_string()
1256 };
1257
1258 let clone_urls: Vec<String> = domains
1259 .iter()
1260 .map(|d| format!("http://{}/{}/{}.git", d, npub, identifier))
1261 .collect();
1262 let relay_urls: Vec<String> = domains.iter().map(|d| format!("ws://{}", d)).collect();
1263
1264 let state_event = create_state_event(
1265 keys,
1266 identifier,
1267 &[("main", &commit_hash)],
1268 &[],
1269 &clone_urls.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
1270 &relay_urls.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
1271 )
1272 .expect("Failed to create state event");
1273
1274 send_to_relay(relay, &state_event)
1275 .await
1276 .expect("Failed to send state event");
1277
1278 push_to_relay(path, &relay.domain(), &npub, identifier)
1279 .expect("Failed to push git data to relay");
1280
1281 tokio::time::sleep(Duration::from_millis(500)).await;
1282
1283 git_temp_dir
1284}
1285
1286/// Set up a repository announcement on a relay with git data so it passes purgatory.
1287///
1288/// With the announcement purgatory feature, announcements (kind 30617) require git
1289/// data before they are promoted to the relay's main DB. This helper:
1290///
1291/// 1. Creates a local git repo with a commit
1292/// 2. Builds an announcement and state event (kind 30618) pointing to the relay
1293/// 3. Sends both to the relay (they go to purgatory)
1294/// 4. Git pushes to the relay → releases both from purgatory immediately
1295/// 5. Returns the announcement event and temp dir (keep alive for test duration)
1296///
1297/// # Arguments
1298/// * `relay` - The relay to set up the announcement on
1299/// * `keys` - Keys to sign the announcement with (repo owner)
1300/// * `domains` - All domains that should be listed in the announcement (including relay.domain())
1301/// * `identifier` - Repository identifier (d-tag)
1302///
1303/// # Returns
1304/// `(Event, tempfile::TempDir)` - The announcement event and temp dir.
1305/// The temp dir MUST be kept alive for the duration of the test.
1306pub async fn setup_announcement_on_relay(
1307 relay: &TestRelay,
1308 keys: &Keys,
1309 domains: &[&str],
1310 identifier: &str,
1311) -> (Event, tempfile::TempDir) {
1312 use super::purgatory_helpers::{
1313 create_state_event, create_test_repo_with_commit, push_to_relay, CommitVariant,
1314 };
1315
1316 let npub = keys
1317 .public_key()
1318 .to_bech32()
1319 .expect("Failed to convert public key to npub");
1320
1321 // Create local git repo with a commit
1322 let git_temp_dir = tempfile::tempdir().expect("Failed to create temp dir for git repo");
1323 let commit_hash = create_test_repo_with_commit(git_temp_dir.path(), CommitVariant::StateTest)
1324 .expect("Failed to create test git repo");
1325
1326 // Build clone URLs and relay URLs from domains
1327 let clone_urls: Vec<String> = domains
1328 .iter()
1329 .map(|d| format!("http://{}/{}/{}.git", d, npub, identifier))
1330 .collect();
1331 let relay_urls: Vec<String> = domains.iter().map(|d| format!("ws://{}", d)).collect();
1332
1333 // Build announcement event (lists ALL domains for relay discovery)
1334 let announcement = EventBuilder::new(Kind::GitRepoAnnouncement, "Repository state")
1335 .tags(vec![
1336 Tag::identifier(identifier),
1337 Tag::custom(TagKind::custom("clone"), clone_urls.clone()),
1338 Tag::custom(TagKind::custom("relays"), relay_urls.clone()),
1339 ])
1340 .sign_with_keys(keys)
1341 .expect("Failed to sign repo announcement");
1342
1343 // Build state event with all domains' clone URLs
1344 let state_event = create_state_event(
1345 keys,
1346 identifier,
1347 &[("main", &commit_hash)],
1348 &[],
1349 &clone_urls.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
1350 &relay_urls.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
1351 )
1352 .expect("Failed to create state event");
1353
1354 // Send announcement and state event to relay (both go to purgatory)
1355 send_to_relay(relay, &announcement)
1356 .await
1357 .expect("Failed to send announcement");
1358 send_to_relay(relay, &state_event)
1359 .await
1360 .expect("Failed to send state event");
1361
1362 // Git push to relay → releases both from purgatory
1363 push_to_relay(git_temp_dir.path(), &relay.domain(), &npub, identifier)
1364 .expect("Failed to push git data to relay");
1365
1366 // Brief wait for push processing
1367 tokio::time::sleep(Duration::from_millis(500)).await;
1368
1369 (announcement, git_temp_dir)
1370}
1371
1087/// Unified sync test helper that automatically determines sync mode. 1372/// Unified sync test helper that automatically determines sync mode.
1088/// 1373///
1089/// This function sets up a complete sync test environment by determining whether 1374/// This function sets up a complete sync test environment by determining whether
@@ -1119,6 +1404,10 @@ async fn send_to_relay(relay: &TestRelay, event: &Event) -> Result<(), String> {
1119/// // Assert comment synced to result.syncing_relay 1404/// // Assert comment synced to result.syncing_relay
1120/// ``` 1405/// ```
1121pub async fn run_sync_test(historic_events: &[Event], live_events: &[Event]) -> SyncTestResult { 1406pub async fn run_sync_test(historic_events: &[Event], live_events: &[Event]) -> SyncTestResult {
1407 use super::purgatory_helpers::{
1408 create_state_event, create_test_repo_with_commit, push_to_relay, CommitVariant,
1409 };
1410
1122 // Validate usage - cannot provide events in both slices 1411 // Validate usage - cannot provide events in both slices
1123 let historic_mode = !historic_events.is_empty(); 1412 let historic_mode = !historic_events.is_empty();
1124 let live_mode = !live_events.is_empty(); 1413 let live_mode = !live_events.is_empty();
@@ -1137,39 +1426,93 @@ pub async fn run_sync_test(historic_events: &[Event], live_events: &[Event]) ->
1137 // 2. Start source relay 1426 // 2. Start source relay
1138 let source = TestRelay::start().await; 1427 let source = TestRelay::start().await;
1139 1428
1140 // 3. Create keys and announcement listing both relays 1429 // 3. Create local git repo with a commit
1430 let git_temp_dir = tempfile::tempdir().expect("Failed to create temp dir for git repo");
1431 let commit_hash = create_test_repo_with_commit(git_temp_dir.path(), CommitVariant::StateTest)
1432 .expect("Failed to create test git repo");
1433
1434 // 4. Create keys and build URLs
1141 let keys = Keys::generate(); 1435 let keys = Keys::generate();
1142 let announcement = 1436 let npub = keys
1143 create_repo_announcement(&keys, &[&source.domain(), &syncing_domain], "test-repo"); 1437 .public_key()
1438 .to_bech32()
1439 .expect("Failed to convert public key to npub");
1440
1441 // Clone URLs: source relay HTTP endpoint is where git data lives
1442 // The syncing relay's purgatory will fetch from source's clone URL
1443 let clone_url_source = format!("http://{}/{}/{}.git", source.domain(), npub, "test-repo");
1444 let clone_url_syncing = format!("http://{}/{}/{}.git", syncing_domain, npub, "test-repo");
1144 1445
1145 // 4. Send announcement + historic events to source BEFORE syncing relay starts 1446 let clone_urls = vec![clone_url_source.clone(), clone_url_syncing.clone()];
1447 let relay_urls = vec![
1448 format!("ws://{}", source.domain()),
1449 format!("ws://{}", syncing_domain),
1450 ];
1451
1452 let announcement = EventBuilder::new(Kind::GitRepoAnnouncement, "Repository state")
1453 .tags(vec![
1454 Tag::identifier("test-repo"),
1455 Tag::custom(TagKind::custom("clone"), clone_urls.clone()),
1456 Tag::custom(TagKind::custom("relays"), relay_urls.clone()),
1457 ])
1458 .sign_with_keys(&keys)
1459 .expect("Failed to sign repo announcement");
1460
1461 // 5. Create state event referencing the commit
1462 let state_event = create_state_event(
1463 &keys,
1464 "test-repo",
1465 &[("main", &commit_hash)],
1466 &[],
1467 &clone_urls.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
1468 &relay_urls.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
1469 )
1470 .expect("Failed to create state event");
1471
1472 // 6. Send announcement + state event to source (both go to purgatory)
1146 send_to_relay(&source, &announcement) 1473 send_to_relay(&source, &announcement)
1147 .await 1474 .await
1148 .expect("Failed to send announcement"); 1475 .expect("Failed to send announcement");
1476 send_to_relay(&source, &state_event)
1477 .await
1478 .expect("Failed to send state event");
1479
1480 // 7. Git push to source relay → releases both announcement and state event from purgatory
1481 push_to_relay(git_temp_dir.path(), &source.domain(), &npub, "test-repo")
1482 .expect("Failed to push git data to source relay");
1483
1484 // 8. Wait for source relay to process the push and release events from purgatory
1485 tokio::time::sleep(Duration::from_secs(2)).await;
1486
1487 // 9. Send historic events to source BEFORE syncing relay starts
1149 for event in historic_events { 1488 for event in historic_events {
1150 send_to_relay(&source, event) 1489 send_to_relay(&source, event)
1151 .await 1490 .await
1152 .expect("Failed to send historic event"); 1491 .expect("Failed to send historic event");
1153 } 1492 }
1154 1493
1155 // 5. Start syncing relay (connects to source) 1494 // 10. Start syncing relay (connects to source)
1156 let syncing = 1495 let syncing =
1157 TestRelay::start_on_port_with_options(syncing_port, Some(source.url().into()), false).await; 1496 TestRelay::start_on_port_with_options(syncing_port, Some(source.url().into()), false).await;
1158 1497
1159 // 6. Wait for sync connection to establish 1498 // 11. Wait for sync connection to establish
1160 let _ = wait_for_sync_connection(syncing.url(), 1, Duration::from_secs(5)).await; 1499 let _ = wait_for_sync_connection(syncing.url(), 1, Duration::from_secs(5)).await;
1161 1500
1162 // 7. Send live events AFTER connection established 1501 // 12. Send live events AFTER connection established
1163 for event in live_events { 1502 for event in live_events {
1164 send_to_relay(&source, event) 1503 send_to_relay(&source, event)
1165 .await 1504 .await
1166 .expect("Failed to send live event"); 1505 .expect("Failed to send live event");
1167 } 1506 }
1168 1507
1169 // 8. Allow sync to complete 1508 // 13. Allow sync + purgatory promotion to complete on the syncing relay.
1170 tokio::time::sleep(Duration::from_millis(100)).await; 1509 // The syncing relay receives the announcement (goes to purgatory) and state event.
1510 // The purgatory sync loop (1s interval) fetches git data from source's clone URL
1511 // (http://source-domain/npub/test-repo.git) and releases the announcement.
1512 // We wait up to 8s to allow time for this.
1513 tokio::time::sleep(Duration::from_secs(8)).await;
1171 1514
1172 // 9. Compute repo coordinate before moving keys 1515 // 14. Compute repo coordinate before moving keys
1173 let coordinate = repo_coord(&keys, "test-repo"); 1516 let coordinate = repo_coord(&keys, "test-repo");
1174 1517
1175 SyncTestResult { 1518 SyncTestResult {
@@ -1177,6 +1520,8 @@ pub async fn run_sync_test(historic_events: &[Event], live_events: &[Event]) ->
1177 syncing_relay: syncing, 1520 syncing_relay: syncing,
1178 maintainer_keys: keys, 1521 maintainer_keys: keys,
1179 repo_coord: coordinate, 1522 repo_coord: coordinate,
1523 _git_server: None,
1524 _git_temp_dir: Some(git_temp_dir),
1180 } 1525 }
1181} 1526}
1182 1527