mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 23:18:24 +00:00
Merge #02f42883: test(sync): preserve shared repository identity and aw…
test(sync): preserve shared repository identity and await readiness
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsq9apgsdh7ddvt37xjf0jlq50e285p27d7pp96rr6nfxtxe2r07dcm4geg4
PR-Author: DanConwayDev's Agent
nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0
CoverNote:
Shared repository setup could create competing replaceable events while sync tests waited for one exact event ID. This PR prepares one repository fixture and installs its unchanged Git commit, announcement and state event on every participating relay. It also corrects the connection-readiness and filter-refusal assumptions found in the earlier host build.
- Migrate all 16 paired setup scenarios across descendant, discovery, live-sync, metrics and tag-variation tests to reuse their fixture.
- Reserve the target listener before historic source seeding so the same initial announcement can list both endpoints without starting target sync early.
- Add a regression checking exact announcement/state IDs and remote Git refs after installation and repeated reinstallation on two relays.
- Wait for the specific source's connected gauge before asserting it, and use a one-filter fixture cap to force the refusal being tested under either startup ordering.
- Keep exact-ID visibility assertions, original success deadlines, failure diagnostics and parallel execution. Production replacement rules, runtime code and dependency versions are unchanged.
Validation: source/caller audit, cargo fmt --all -- --check and git diff --check passed. Runtime tests are delegated to CI/host. Both original corrected tests passed on fc99132; that run exposed the shared setup failure before descendant assertions. The diagnostic revision 2a13fa7 hit a GitHub archive HTTP 504 before Rust tests, so it did not establish the exact cause of that failure. This revision removes a concrete fixture replacement race across its callers; the new regression and full CI must validate the result.
Targeted tests:
cargo test -p ngit-grasp --test sync reinstalling_shared_repository_preserves_event_ids_and_git_refs
cargo test -p ngit-grasp --test sync historic_recursive_cap_is_enforced_and_reconstructed_after_restart
AI assistance: prepared with Codex (GPT-6).
This commit is contained in:
@@ -27,6 +27,24 @@ Connection tasks belong to their fixture's accept loop. Stopping the fixture
|
||||
cancels and joins them; dropping it cancels the owner. Git fixture subprocesses
|
||||
are cancelled with their request, and all three pipes are driven concurrently.
|
||||
|
||||
## Shared repository identity
|
||||
|
||||
`setup_announcement_on_relay` returns an announcement and a `RepositoryFixture`.
|
||||
Prepare it once for each logical repository, then call `fixture.install(&relay)`
|
||||
for every other relay. It reuses the signed announcement, signed state event,
|
||||
and Git commit. Repeating setup independently can create competing replaceable
|
||||
events at the same owner/identifier and different Git histories.
|
||||
|
||||
List all participating domains when preparing the fixture. If historic tests
|
||||
must seed a source before starting the syncing relay, reserve the target's
|
||||
listener first and transfer it into the target after seeding. Do not use sleeps
|
||||
or fabricated future timestamps to order copies of the same fixture. Tests of
|
||||
actual repository revisions should construct those revisions explicitly.
|
||||
|
||||
The sync regression `reinstalling_shared_repository_preserves_event_ids_and_git_refs`
|
||||
checks exact announcement/state IDs and remote Git refs across installation and
|
||||
reinstallation on two relays.
|
||||
|
||||
## Time and streaming assertions
|
||||
|
||||
Age private cache timestamps explicitly and bracket wall-clock timestamps
|
||||
|
||||
+162
-93
@@ -1351,107 +1351,176 @@ pub async fn push_unique_git_data_to_relay(
|
||||
git_temp_dir
|
||||
}
|
||||
|
||||
/// Set up a repository announcement on a relay with git data so it passes purgatory.
|
||||
/// One repository's immutable Git data and signed events for all test relays.
|
||||
///
|
||||
/// With the announcement purgatory feature, announcements (kind 30617) require git
|
||||
/// data before they are promoted to the relay's main DB. This helper:
|
||||
///
|
||||
/// 1. Creates a local git repo with a commit
|
||||
/// 2. Builds an announcement and state event (kind 30618) pointing to the relay
|
||||
/// 3. Sends both to the relay (they go to purgatory)
|
||||
/// 4. Git pushes to the relay → releases both from purgatory immediately
|
||||
/// 5. Returns the announcement event and temp dir (keep alive for test duration)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `relay` - The relay to set up the announcement on
|
||||
/// * `keys` - Keys to sign the announcement with (repo owner)
|
||||
/// * `domains` - All domains that should be listed in the announcement (including relay.domain())
|
||||
/// * `identifier` - Repository identifier (d-tag)
|
||||
///
|
||||
/// # Returns
|
||||
/// `(Event, tempfile::TempDir)` - The announcement event and temp dir.
|
||||
/// The temp dir MUST be kept alive for the duration of the test.
|
||||
/// Preparing separate copies can mint competing replaceable events for the same
|
||||
/// owner/identifier and different Git commits. Prepare once, list every endpoint
|
||||
/// (reserve its listener first if necessary), and install this fixture on each
|
||||
/// relay. Keep it alive until the test finishes.
|
||||
pub struct RepositoryFixture {
|
||||
git_dir: tempfile::TempDir,
|
||||
announcement: Event,
|
||||
state_event: Event,
|
||||
keys: Keys,
|
||||
identifier: String,
|
||||
}
|
||||
|
||||
impl RepositoryFixture {
|
||||
pub fn new(keys: &Keys, domains: &[&str], identifier: &str) -> Self {
|
||||
use super::purgatory_helpers::{
|
||||
create_state_event, create_test_repo_with_commit, CommitVariant,
|
||||
};
|
||||
|
||||
let npub = keys
|
||||
.public_key()
|
||||
.to_bech32()
|
||||
.expect("Failed to convert public key to npub");
|
||||
|
||||
// Create local git repo with a commit
|
||||
let git_temp_dir = tempfile::tempdir().expect("Failed to create temp dir for git repo");
|
||||
let commit_hash =
|
||||
create_test_repo_with_commit(git_temp_dir.path(), CommitVariant::StateTest)
|
||||
.expect("Failed to create test git repo");
|
||||
|
||||
// Build clone URLs and relay URLs from domains
|
||||
let clone_urls: Vec<String> = domains
|
||||
.iter()
|
||||
.map(|d| format!("http://{}/{}/{}.git", d, npub, identifier))
|
||||
.collect();
|
||||
let relay_urls: Vec<String> = domains.iter().map(|d| format!("ws://{}", d)).collect();
|
||||
|
||||
// Build announcement event (lists ALL domains for relay discovery)
|
||||
let announcement = EventBuilder::new(Kind::GitRepoAnnouncement, "Repository state")
|
||||
.tags(vec![
|
||||
Tag::identifier(identifier),
|
||||
Tag::custom("clone", clone_urls.clone()),
|
||||
Tag::custom("relays", relay_urls.clone()),
|
||||
])
|
||||
.finalize(keys)
|
||||
.expect("Failed to sign repo announcement");
|
||||
|
||||
// Build state event with all domains' clone URLs
|
||||
let state_event = create_state_event(
|
||||
keys,
|
||||
identifier,
|
||||
&[("main", &commit_hash)],
|
||||
&[],
|
||||
&clone_urls.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
|
||||
&relay_urls.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
|
||||
)
|
||||
.expect("Failed to create state event");
|
||||
|
||||
Self {
|
||||
git_dir: git_temp_dir,
|
||||
announcement,
|
||||
state_event,
|
||||
keys: keys.clone(),
|
||||
identifier: identifier.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &std::path::Path {
|
||||
self.git_dir.path()
|
||||
}
|
||||
|
||||
/// Publish the same events and push the same commit to another relay.
|
||||
pub async fn install(&self, relay: &TestRelay) {
|
||||
use super::purgatory_helpers::push_to_relay;
|
||||
|
||||
send_to_relay(relay, &self.announcement)
|
||||
.await
|
||||
.expect("Failed to send announcement");
|
||||
send_to_relay(relay, &self.state_event)
|
||||
.await
|
||||
.expect("Failed to send state event");
|
||||
let npub = self.keys.public_key().to_bech32().expect("owner npub");
|
||||
push_to_relay(self.path(), &relay.domain(), &npub, &self.identifier)
|
||||
.expect("Failed to push git data to relay");
|
||||
|
||||
for (label, event) in [
|
||||
("state", &self.state_event),
|
||||
("announcement", &self.announcement),
|
||||
] {
|
||||
let visible = wait_for_event_on_relay(
|
||||
relay.url(),
|
||||
Filter::new().id(event.id),
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.await;
|
||||
if !visible {
|
||||
report_setup_visibility_failure(relay, &self.keys, &self.identifier, event).await;
|
||||
}
|
||||
assert!(visible, "pushed {label} must leave purgatory");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare and install a repository on its first relay. For another relay,
|
||||
/// call `install` on the returned fixture instead of preparing a new copy.
|
||||
pub async fn setup_announcement_on_relay(
|
||||
relay: &TestRelay,
|
||||
keys: &Keys,
|
||||
domains: &[&str],
|
||||
identifier: &str,
|
||||
) -> (Event, tempfile::TempDir) {
|
||||
use super::purgatory_helpers::{
|
||||
create_state_event, create_test_repo_with_commit, push_to_relay, CommitVariant,
|
||||
};
|
||||
) -> (Event, RepositoryFixture) {
|
||||
let fixture = RepositoryFixture::new(keys, domains, identifier);
|
||||
fixture.install(relay).await;
|
||||
(fixture.announcement.clone(), fixture)
|
||||
}
|
||||
|
||||
let npub = keys
|
||||
.public_key()
|
||||
.to_bech32()
|
||||
.expect("Failed to convert public key to npub");
|
||||
|
||||
// Create local git repo with a commit
|
||||
let git_temp_dir = tempfile::tempdir().expect("Failed to create temp dir for git repo");
|
||||
let commit_hash = create_test_repo_with_commit(git_temp_dir.path(), CommitVariant::StateTest)
|
||||
.expect("Failed to create test git repo");
|
||||
|
||||
// Build clone URLs and relay URLs from domains
|
||||
let clone_urls: Vec<String> = domains
|
||||
.iter()
|
||||
.map(|d| format!("http://{}/{}/{}.git", d, npub, identifier))
|
||||
.collect();
|
||||
let relay_urls: Vec<String> = domains.iter().map(|d| format!("ws://{}", d)).collect();
|
||||
|
||||
// Build announcement event (lists ALL domains for relay discovery)
|
||||
let announcement = EventBuilder::new(Kind::GitRepoAnnouncement, "Repository state")
|
||||
.tags(vec![
|
||||
Tag::identifier(identifier),
|
||||
Tag::custom("clone", clone_urls.clone()),
|
||||
Tag::custom("relays", relay_urls.clone()),
|
||||
])
|
||||
.finalize(keys)
|
||||
.expect("Failed to sign repo announcement");
|
||||
|
||||
// Build state event with all domains' clone URLs
|
||||
let state_event = create_state_event(
|
||||
keys,
|
||||
identifier,
|
||||
&[("main", &commit_hash)],
|
||||
&[],
|
||||
&clone_urls.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
|
||||
&relay_urls.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
|
||||
)
|
||||
.expect("Failed to create state event");
|
||||
|
||||
// Send announcement and state event to relay (both go to purgatory)
|
||||
send_to_relay(relay, &announcement)
|
||||
.await
|
||||
.expect("Failed to send announcement");
|
||||
send_to_relay(relay, &state_event)
|
||||
.await
|
||||
.expect("Failed to send state event");
|
||||
|
||||
// Git push to relay → releases both from purgatory
|
||||
push_to_relay(git_temp_dir.path(), &relay.domain(), &npub, identifier)
|
||||
.expect("Failed to push git data to relay");
|
||||
|
||||
assert!(
|
||||
wait_for_event_on_relay(
|
||||
relay.url(),
|
||||
Filter::new().id(state_event.id),
|
||||
Duration::from_secs(10)
|
||||
)
|
||||
.await,
|
||||
"pushed state event must leave purgatory"
|
||||
/// Diagnose a failed exact-ID check without extending its success deadline.
|
||||
async fn report_setup_visibility_failure(
|
||||
relay: &TestRelay,
|
||||
keys: &Keys,
|
||||
identifier: &str,
|
||||
expected: &Event,
|
||||
) {
|
||||
eprintln!(
|
||||
"setup visibility failure: relay={} kind={} author={} identifier={} expected_id={} created_at={}",
|
||||
relay.url(), expected.kind.as_u16(), expected.pubkey, identifier,
|
||||
expected.id, expected.created_at
|
||||
);
|
||||
|
||||
assert!(
|
||||
wait_for_event_on_relay(
|
||||
relay.url(),
|
||||
Filter::new().id(announcement.id),
|
||||
Duration::from_secs(10)
|
||||
)
|
||||
.await,
|
||||
"pushed announcement must leave purgatory"
|
||||
);
|
||||
|
||||
(announcement, git_temp_dir)
|
||||
// A different event at the same replaceable address distinguishes a lost
|
||||
// replacement race from an event that was never promoted or served.
|
||||
let diagnostic = tokio::time::timeout(Duration::from_secs(5), async {
|
||||
let client = TestClient::new(relay.url(), keys.clone()).await?;
|
||||
let result = client
|
||||
.client
|
||||
.fetch_events(
|
||||
Filter::new()
|
||||
.kind(expected.kind)
|
||||
.author(expected.pubkey)
|
||||
.identifier(identifier),
|
||||
)
|
||||
.timeout(Duration::from_secs(2))
|
||||
.await;
|
||||
client.disconnect().await;
|
||||
let events = result.map_err(|error| error.to_string())?;
|
||||
for event in events.iter() {
|
||||
eprintln!(
|
||||
"stored candidate: id={} created_at={}",
|
||||
event.id, event.created_at
|
||||
);
|
||||
}
|
||||
eprintln!("stored candidate count: {}", events.len());
|
||||
Ok::<(), String>(())
|
||||
})
|
||||
.await;
|
||||
match diagnostic {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(error)) => eprintln!("setup diagnostic query failed: {error}"),
|
||||
Err(_) => eprintln!("setup diagnostic query timed out"),
|
||||
}
|
||||
match std::fs::read_to_string(relay.log_path()) {
|
||||
Ok(log) => {
|
||||
let tail = log.lines().rev().take(80).collect::<Vec<_>>();
|
||||
eprintln!("setup relay log tail:");
|
||||
for line in tail.into_iter().rev() {
|
||||
eprintln!("{line}");
|
||||
}
|
||||
}
|
||||
Err(error) => eprintln!("setup relay log unavailable: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Unified sync test helper that automatically determines sync mode.
|
||||
|
||||
@@ -46,6 +46,7 @@ mod sync {
|
||||
pub mod proactive_sync_plus;
|
||||
pub mod purgatory_fetch;
|
||||
pub mod reconnect_backoff;
|
||||
pub mod repository_fixture;
|
||||
pub mod req_concurrency;
|
||||
pub mod stale_connect_result;
|
||||
pub mod tag_variations;
|
||||
|
||||
@@ -31,12 +31,14 @@ async fn historic_recursive_cap_is_enforced_and_reconstructed_after_restart() {
|
||||
let source = TestRelay::start_with_relay_max_subscriptions(4).await;
|
||||
let keys = Keys::generate();
|
||||
let repo_id = "restart-safe-recursive-cap";
|
||||
let source_domains = [source.domain()];
|
||||
let syncing_reservation = reserve_port();
|
||||
let syncing_domain = format!("127.0.0.1:{}", syncing_reservation.port());
|
||||
let source_domains = [source.domain(), syncing_domain];
|
||||
let source_refs = source_domains
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<_>>();
|
||||
let (_announcement, _source_git) =
|
||||
let (_announcement, repository) =
|
||||
setup_announcement_on_relay(&source, &keys, &source_refs, repo_id).await;
|
||||
let source_client = TestClient::new(source.url(), keys.clone())
|
||||
.await
|
||||
@@ -64,7 +66,7 @@ async fn historic_recursive_cap_is_enforced_and_reconstructed_after_restart() {
|
||||
let syncing_git_dir = tempfile::tempdir().expect("create syncing git dir");
|
||||
let syncing_data_dir = tempfile::tempdir().expect("create syncing relay data dir");
|
||||
let syncing = TestRelay::start_on_reservation_persistent_sync_with_recursive_limit(
|
||||
reserve_port(),
|
||||
syncing_reservation,
|
||||
None,
|
||||
true,
|
||||
syncing_git_dir.path().to_path_buf(),
|
||||
@@ -72,10 +74,7 @@ async fn historic_recursive_cap_is_enforced_and_reconstructed_after_restart() {
|
||||
LIMIT,
|
||||
)
|
||||
.await;
|
||||
let domains = [source.domain(), syncing.domain()];
|
||||
let domain_refs = domains.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
let (_target_announcement, _target_git) =
|
||||
setup_announcement_on_relay(&syncing, &keys, &domain_refs, repo_id).await;
|
||||
repository.install(&syncing).await;
|
||||
|
||||
for descendant in [&first, &second] {
|
||||
assert!(
|
||||
@@ -154,12 +153,14 @@ async fn historic_sync_recovers_recursive_parent_only_descendants() {
|
||||
|
||||
// Seed the source before the syncing relay exists so this exercises the
|
||||
// complete historic rotation, not ordinary live coverage.
|
||||
let source_domains = [source.domain()];
|
||||
let syncing_reservation = reserve_port();
|
||||
let syncing_domain = format!("127.0.0.1:{}", syncing_reservation.port());
|
||||
let source_domains = [source.domain(), syncing_domain];
|
||||
let source_refs = source_domains
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<_>>();
|
||||
let (_announcement, _source_git) =
|
||||
let (_announcement, repository) =
|
||||
setup_announcement_on_relay(&source, &keys, &source_refs, repo_id).await;
|
||||
let source_client = TestClient::new(source.url(), keys.clone())
|
||||
.await
|
||||
@@ -188,11 +189,9 @@ async fn historic_sync_recovers_recursive_parent_only_descendants() {
|
||||
.expect("seed source event");
|
||||
}
|
||||
|
||||
let syncing = TestRelay::start_with_sync(None).await;
|
||||
let domains = [source.domain(), syncing.domain()];
|
||||
let domain_refs = domains.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
let (_target_announcement, _target_git) =
|
||||
setup_announcement_on_relay(&syncing, &keys, &domain_refs, repo_id).await;
|
||||
let syncing =
|
||||
TestRelay::start_on_reservation_with_options(syncing_reservation, None, false).await;
|
||||
repository.install(&syncing).await;
|
||||
|
||||
assert!(
|
||||
wait_for_event_on_relay(
|
||||
@@ -235,12 +234,14 @@ async fn descendant_live_coverage_is_preferred_when_capacity_remains() {
|
||||
let source = TestRelay::start().await;
|
||||
let keys = Keys::generate();
|
||||
let repo_id = "live-descendant-coverage";
|
||||
let source_domains = [source.domain()];
|
||||
let syncing_reservation = reserve_port();
|
||||
let syncing_domain = format!("127.0.0.1:{}", syncing_reservation.port());
|
||||
let source_domains = [source.domain(), syncing_domain];
|
||||
let source_refs = source_domains
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<_>>();
|
||||
let (_announcement, _source_git) =
|
||||
let (_announcement, repository) =
|
||||
setup_announcement_on_relay(&source, &keys, &source_refs, repo_id).await;
|
||||
let source_client = TestClient::new(source.url(), keys.clone())
|
||||
.await
|
||||
@@ -252,11 +253,9 @@ async fn descendant_live_coverage_is_preferred_when_capacity_remains() {
|
||||
source_client.send_event(&issue).await.unwrap();
|
||||
source_client.send_event(&direct_reply).await.unwrap();
|
||||
|
||||
let syncing = TestRelay::start_with_sync(None).await;
|
||||
let domains = [source.domain(), syncing.domain()];
|
||||
let domain_refs = domains.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
let (_target_announcement, _target_git) =
|
||||
setup_announcement_on_relay(&syncing, &keys, &domain_refs, repo_id).await;
|
||||
let syncing =
|
||||
TestRelay::start_on_reservation_with_options(syncing_reservation, None, false).await;
|
||||
repository.install(&syncing).await;
|
||||
assert!(
|
||||
wait_for_log(
|
||||
&syncing.log_path(),
|
||||
@@ -295,12 +294,14 @@ async fn historic_sync_recovers_address_tag_descendants() {
|
||||
let source = TestRelay::start_with_relay_max_subscriptions(4).await;
|
||||
let keys = Keys::generate();
|
||||
let repo_id = "addressable-descendant-history";
|
||||
let source_domains = [source.domain()];
|
||||
let syncing_reservation = reserve_port();
|
||||
let syncing_domain = format!("127.0.0.1:{}", syncing_reservation.port());
|
||||
let source_domains = [source.domain(), syncing_domain];
|
||||
let source_refs = source_domains
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<_>>();
|
||||
let (_announcement, _source_git) =
|
||||
let (_announcement, repository) =
|
||||
setup_announcement_on_relay(&source, &keys, &source_refs, repo_id).await;
|
||||
let source_client = TestClient::new(source.url(), keys.clone())
|
||||
.await
|
||||
@@ -334,11 +335,9 @@ async fn historic_sync_recovers_address_tag_descendants() {
|
||||
source_client.send_event(event).await.unwrap();
|
||||
}
|
||||
|
||||
let syncing = TestRelay::start_with_sync(None).await;
|
||||
let domains = [source.domain(), syncing.domain()];
|
||||
let domain_refs = domains.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
let (_target_announcement, _target_git) =
|
||||
setup_announcement_on_relay(&syncing, &keys, &domain_refs, repo_id).await;
|
||||
let syncing =
|
||||
TestRelay::start_on_reservation_with_options(syncing_reservation, None, false).await;
|
||||
repository.install(&syncing).await;
|
||||
|
||||
for event in &address_only_children {
|
||||
assert!(
|
||||
|
||||
@@ -61,7 +61,7 @@ async fn test_discovers_layer3_via_layer2() {
|
||||
let domains = [relay_a.domain(), relay_b.domain()];
|
||||
let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
let (announcement, _git_dir_a) =
|
||||
let (announcement, repository) =
|
||||
setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await;
|
||||
let announcement_id = announcement.id;
|
||||
println!(
|
||||
@@ -102,8 +102,7 @@ async fn test_discovers_layer3_via_layer2() {
|
||||
client_a.disconnect().await;
|
||||
|
||||
// 8. Set up announcement on relay_b (triggers discovery of relay_a)
|
||||
let (_announcement_b, _git_dir_b) =
|
||||
setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await;
|
||||
repository.install(&relay_b).await;
|
||||
println!("Announcement set up on relay_b (should trigger discovery of relay_a)");
|
||||
|
||||
// 9/10. Verify the patch syncs to relay_b. The bounded poll on the synced
|
||||
@@ -176,7 +175,7 @@ async fn test_relay_discovery_via_announcements_with_historic_sync() {
|
||||
let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect();
|
||||
let repo_id = "test-repo-chain";
|
||||
|
||||
let (announcement, _git_dir_a) =
|
||||
let (announcement, repository) =
|
||||
setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await;
|
||||
let announcement_id = announcement.id;
|
||||
println!(
|
||||
@@ -207,8 +206,7 @@ async fn test_relay_discovery_via_announcements_with_historic_sync() {
|
||||
client_a.disconnect().await;
|
||||
|
||||
// 6. Set up announcement on relay_b (triggers discovery of relay_a)
|
||||
let (_announcement_b, _git_dir_b) =
|
||||
setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await;
|
||||
repository.install(&relay_b).await;
|
||||
println!("Announcement set up on relay_b (should trigger discovery of relay_a)");
|
||||
|
||||
// 7/8. Verify the Layer 2 event syncs to relay_b. The bounded poll on the
|
||||
|
||||
+9
-10
@@ -205,7 +205,9 @@ async fn test_live_sync_batches_repo_filters_below_source_req_limit() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_sync_regroups_after_filter_count_refusal() {
|
||||
let source = MockRelay::start_with_max_filters(3).await;
|
||||
// Minimum-churn startup can split core and auxiliary coverage into two
|
||||
// filters each. A one-filter cap forces regrouping under either ordering.
|
||||
let source = MockRelay::start_with_max_filters(1).await;
|
||||
let syncing = TestRelay::start_with_sync(None).await;
|
||||
let keys = Keys::generate();
|
||||
let repo_id = "adaptive-filter-count";
|
||||
@@ -297,12 +299,11 @@ async fn test_live_sync_layer2_events() {
|
||||
let domains = [relay_a.domain(), relay_b.domain()];
|
||||
let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
let (_announcement, _git_dir_a) =
|
||||
let (_announcement, repository) =
|
||||
setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await;
|
||||
println!("Announcement set up on relay_a with git data");
|
||||
|
||||
let (_announcement_b, _git_dir_b) =
|
||||
setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await;
|
||||
repository.install(&relay_b).await;
|
||||
println!("Announcement set up on relay_b with git data (triggers discovery)");
|
||||
|
||||
// 5. Wait for discovery: events published before the syncing relay has
|
||||
@@ -395,12 +396,11 @@ async fn test_live_sync_layer3_events() {
|
||||
let domains = [relay_a.domain(), relay_b.domain()];
|
||||
let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
let (_announcement, _git_dir_a) =
|
||||
let (_announcement, repository) =
|
||||
setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await;
|
||||
println!("Announcement set up on relay_a with git data");
|
||||
|
||||
let (_announcement_b, _git_dir_b) =
|
||||
setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await;
|
||||
repository.install(&relay_b).await;
|
||||
println!("Announcement set up on relay_b with git data (triggers discovery)");
|
||||
|
||||
// 3. Wait for discovery: events published before the syncing relay has
|
||||
@@ -571,10 +571,9 @@ async fn test_live_sync_event_ordering() {
|
||||
let domains = [relay_a.domain(), relay_b.domain()];
|
||||
let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
let (_announcement, _git_dir_a) =
|
||||
let (_announcement, repository) =
|
||||
setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await;
|
||||
let (_announcement_b, _git_dir_b) =
|
||||
setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await;
|
||||
repository.install(&relay_b).await;
|
||||
println!("Announcements set up on both relays with git data");
|
||||
|
||||
// 3. Wait for discovery: events published before the syncing relay has
|
||||
|
||||
+15
-5
@@ -243,7 +243,7 @@ async fn test_startup_sync_event_count() {
|
||||
let domains = [source_relay.domain(), syncing_relay.domain()];
|
||||
let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
let (announcement, _git_dir_source) =
|
||||
let (announcement, repository) =
|
||||
setup_announcement_on_relay(&source_relay, &keys, &domain_refs, repo_id).await;
|
||||
println!(
|
||||
"Announcement {} set up on source relay with git data",
|
||||
@@ -286,8 +286,7 @@ async fn test_startup_sync_event_count() {
|
||||
source_client.disconnect().await;
|
||||
|
||||
// 8. Set up announcement on SYNCING relay (triggers discovery of source relay)
|
||||
let (_announcement_syncing, _git_dir_syncing) =
|
||||
setup_announcement_on_relay(&syncing_relay, &keys, &domain_refs, repo_id).await;
|
||||
repository.install(&syncing_relay).await;
|
||||
println!("Announcement set up on syncing relay (triggers discovery of source)");
|
||||
|
||||
// 9. Wait for discovery + sync on the observable condition the test
|
||||
@@ -523,8 +522,19 @@ async fn test_relay_connected_status() {
|
||||
|
||||
let source_url = harness.source_url(0).to_string();
|
||||
|
||||
// Check connected status
|
||||
let metrics = harness.get_metrics().await.unwrap();
|
||||
// HTTP readiness does not imply that outbound sync has connected yet.
|
||||
// Observe this source's gauge, not an aggregate that another relay can satisfy.
|
||||
let metrics = tokio::time::timeout(Duration::from_secs(30), async {
|
||||
loop {
|
||||
let metrics = harness.get_metrics().await.unwrap();
|
||||
if metrics.relay_connected(&source_url) == Some(true) {
|
||||
return metrics;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("source relay should become connected within the deadline");
|
||||
|
||||
println!("Checking connection status for {}", source_url);
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
//! Reusing a repository fixture must not mint replaceable event revisions.
|
||||
use std::{collections::BTreeSet, time::Duration};
|
||||
|
||||
use nostr_sdk::prelude::*;
|
||||
|
||||
use crate::common::{reserve_port, setup_announcement_on_relay, TestRelay};
|
||||
|
||||
async fn repository_event_ids(
|
||||
relay: &TestRelay,
|
||||
keys: &Keys,
|
||||
identifier: &str,
|
||||
) -> BTreeSet<EventId> {
|
||||
let client = Client::builder()
|
||||
.authenticator(SignerAuthenticator::new(keys.clone()))
|
||||
.build();
|
||||
client.add_relay(relay.url()).await.unwrap();
|
||||
client
|
||||
.try_connect_relay(relay.url(), Duration::from_secs(5))
|
||||
.await
|
||||
.expect("connect to installed repository");
|
||||
let events = client
|
||||
.fetch_events(
|
||||
Filter::new()
|
||||
.author(keys.public_key())
|
||||
.identifier(identifier)
|
||||
.kinds([Kind::GitRepoAnnouncement, Kind::RepoState]),
|
||||
)
|
||||
.timeout(Duration::from_secs(5))
|
||||
.await
|
||||
.expect("fetch installed repository events");
|
||||
client.disconnect().await;
|
||||
events.iter().map(|event| event.id).collect()
|
||||
}
|
||||
|
||||
async fn remote_head(relay: &TestRelay, keys: &Keys, identifier: &str) -> String {
|
||||
let url = format!(
|
||||
"http://{}/{}/{}.git",
|
||||
relay.domain(),
|
||||
keys.public_key().to_bech32().unwrap(),
|
||||
identifier
|
||||
);
|
||||
let output = tokio::process::Command::from(grasp_audit::git_command())
|
||||
.args(["ls-remote", &url, "refs/heads/main"])
|
||||
.kill_on_drop(true)
|
||||
.output()
|
||||
.await
|
||||
.expect("query repository ref");
|
||||
assert!(output.status.success());
|
||||
String::from_utf8(output.stdout)
|
||||
.unwrap()
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.expect("main ref exists")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reinstalling_shared_repository_preserves_event_ids_and_git_refs() {
|
||||
tokio::time::timeout(Duration::from_secs(120), async {
|
||||
let source = TestRelay::start().await;
|
||||
let target_reservation = reserve_port();
|
||||
let target_domain = format!("127.0.0.1:{}", target_reservation.port());
|
||||
let domains = [source.domain(), target_domain];
|
||||
let refs = domains.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
let keys = Keys::generate();
|
||||
let identifier = "shared-fixture-identity";
|
||||
let (announcement, repository) =
|
||||
setup_announcement_on_relay(&source, &keys, &refs, identifier).await;
|
||||
let original_ids = repository_event_ids(&source, &keys, identifier).await;
|
||||
assert_eq!(
|
||||
original_ids.len(),
|
||||
2,
|
||||
"announcement and state must both exist"
|
||||
);
|
||||
assert!(original_ids.contains(&announcement.id));
|
||||
let original_head = remote_head(&source, &keys, identifier).await;
|
||||
|
||||
// Start the peer only after seeding the source, matching historic sync.
|
||||
let target =
|
||||
TestRelay::start_on_reservation_with_options(target_reservation, None, false).await;
|
||||
repository.install(&target).await;
|
||||
assert_eq!(
|
||||
repository_event_ids(&target, &keys, identifier).await,
|
||||
original_ids
|
||||
);
|
||||
assert_eq!(remote_head(&target, &keys, identifier).await, original_head);
|
||||
|
||||
// Reinstall after sync has seen the same address. There must be no new
|
||||
// event IDs, timestamps or competing Git state in either direction.
|
||||
repository.install(&source).await;
|
||||
repository.install(&target).await;
|
||||
for relay in [&source, &target] {
|
||||
assert_eq!(
|
||||
repository_event_ids(relay, &keys, identifier).await,
|
||||
original_ids
|
||||
);
|
||||
assert_eq!(remote_head(relay, &keys, identifier).await, original_head);
|
||||
}
|
||||
target.stop().await;
|
||||
source.stop().await;
|
||||
})
|
||||
.await
|
||||
.expect("shared repository setup and reinstalls must complete");
|
||||
}
|
||||
@@ -61,12 +61,11 @@ async fn test_layer2_sync_with_lowercase_a_tag() {
|
||||
let domains = [relay_a.domain(), relay_b.domain()];
|
||||
let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
let (_announcement, _git_dir_a) =
|
||||
let (_announcement, repository) =
|
||||
setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await;
|
||||
println!("Announcement set up on relay_a with git data");
|
||||
|
||||
let (_announcement_b, _git_dir_b) =
|
||||
setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await;
|
||||
repository.install(&relay_b).await;
|
||||
println!("Announcement set up on relay_b with git data (triggers discovery)");
|
||||
|
||||
// 3. Wait for discovery: events published before the syncing relay has
|
||||
@@ -152,12 +151,11 @@ async fn test_layer2_sync_with_uppercase_a_tag() {
|
||||
let domains = [relay_a.domain(), relay_b.domain()];
|
||||
let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
let (_announcement, _git_dir_a) =
|
||||
let (_announcement, repository) =
|
||||
setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await;
|
||||
println!("Announcement set up on relay_a with git data");
|
||||
|
||||
let (_announcement_b, _git_dir_b) =
|
||||
setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await;
|
||||
repository.install(&relay_b).await;
|
||||
println!("Announcement set up on relay_b with git data (triggers discovery)");
|
||||
|
||||
// 3. Wait for discovery: events published before the syncing relay has
|
||||
@@ -245,12 +243,11 @@ async fn test_layer2_sync_with_q_tag() {
|
||||
let domains = [relay_a.domain(), relay_b.domain()];
|
||||
let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
let (_announcement, _git_dir_a) =
|
||||
let (_announcement, repository) =
|
||||
setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await;
|
||||
println!("Announcement set up on relay_a with git data");
|
||||
|
||||
let (_announcement_b, _git_dir_b) =
|
||||
setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await;
|
||||
repository.install(&relay_b).await;
|
||||
println!("Announcement set up on relay_b with git data (triggers discovery)");
|
||||
|
||||
// 3. Wait for discovery: events published before the syncing relay has
|
||||
@@ -344,12 +341,11 @@ async fn test_layer3_sync_with_lowercase_e_tag() {
|
||||
let domains = [relay_a.domain(), relay_b.domain()];
|
||||
let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
let (_announcement, _git_dir_a) =
|
||||
let (_announcement, repository) =
|
||||
setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await;
|
||||
println!("Announcement set up on relay_a with git data");
|
||||
|
||||
let (_announcement_b, _git_dir_b) =
|
||||
setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await;
|
||||
repository.install(&relay_b).await;
|
||||
println!("Announcement set up on relay_b with git data (triggers discovery)");
|
||||
|
||||
// 3. Wait for discovery: events published before the syncing relay has
|
||||
@@ -460,12 +456,11 @@ async fn test_layer3_sync_with_uppercase_e_tag() {
|
||||
let domains = [relay_a.domain(), relay_b.domain()];
|
||||
let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
let (_announcement, _git_dir_a) =
|
||||
let (_announcement, repository) =
|
||||
setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await;
|
||||
println!("Announcement set up on relay_a with git data");
|
||||
|
||||
let (_announcement_b, _git_dir_b) =
|
||||
setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await;
|
||||
repository.install(&relay_b).await;
|
||||
println!("Announcement set up on relay_b with git data (triggers discovery)");
|
||||
|
||||
// 3. Wait for discovery: events published before the syncing relay has
|
||||
@@ -580,12 +575,11 @@ async fn test_layer3_sync_with_q_tag() {
|
||||
let domains = [relay_a.domain(), relay_b.domain()];
|
||||
let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
let (_announcement, _git_dir_a) =
|
||||
let (_announcement, repository) =
|
||||
setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await;
|
||||
println!("Announcement set up on relay_a with git data");
|
||||
|
||||
let (_announcement_b, _git_dir_b) =
|
||||
setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await;
|
||||
repository.install(&relay_b).await;
|
||||
println!("Announcement set up on relay_b with git data (triggers discovery)");
|
||||
|
||||
// 3. Wait for discovery: events published before the syncing relay has
|
||||
|
||||
Reference in New Issue
Block a user