mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 15:08:24 +00:00
Same-second lower-ID replacements could be ignored by served-repository de-listing and superseded-history capture. Rollback's active-state lookup and post-deletion alignment instead preferred higher IDs, while history selection let capture time override the original event's replacement order. Share the later-timestamp/lower-ID comparator across these lifecycle paths and the existing purgatory decisions. Archive a same-second predecessor only when the incoming event wins, and only apply de-list deletion for a winning replacement. Select active and authorized alignment states with the same ordering. Rank history records by original timestamp and original event ID before capture metadata; keep timestamp cutoffs and author checks intact. Extract the authorized-state selection used by realignment to exercise both arrival orders with different authorized authors and an unauthorized newer state. Add served de-list, history-capture and history-candidate regressions. All four new regressions fail before the fixes and pass afterward. Extend the LMDB restart scenario to delete the same-second winner and observe its archived predecessor restored, with a bounded deadline and no new sleeps. This changes replacement preference, not deletion request ownership, coordinate cutoff semantics, repository membership or archive format. Existing capture metadata remains readable. Update the changelog and repository lifecycle documentation. Validation: focused ordering regressions and LMDB restart/rollback test; full ngit-grasp suite, all-target workspace Clippy with warnings denied, formatting and diff checks pass. Assisted-by: GPT-6
602 lines
20 KiB
Rust
602 lines
20 KiB
Rust
//! Integration tests for durable replaceable/addressable history capture.
|
|
|
|
#[path = "../common/mod.rs"]
|
|
mod common;
|
|
|
|
use clap::Parser;
|
|
use common::event_ordering::timestamp_after;
|
|
use common::{publish_served_audit_repo_with_state, publish_served_repo, TestRelay};
|
|
use grasp_audit::{AuditClient, AuditConfig, DETERMINISTIC_COMMIT_HASH};
|
|
use ngit_grasp::config::Config;
|
|
use ngit_grasp::grasp06::receive::new_repo_init_locks;
|
|
use ngit_grasp::nostr::builder::Nip34WritePolicy;
|
|
use ngit_grasp::nostr::lifecycle::ReplaceableHistoryStore;
|
|
use ngit_grasp::nostr::lifecycle::{HoldingStore, RepositoryLifecycle, Tombstones};
|
|
use ngit_grasp::nostr::persistence::SaveContext;
|
|
use ngit_grasp::nostr::SharedDatabase;
|
|
use ngit_grasp::purgatory::Purgatory;
|
|
use nostr_sdk::prelude::*;
|
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
async fn open_history(relay_data_path: &std::path::Path) -> ReplaceableHistoryStore {
|
|
ReplaceableHistoryStore::open_lmdb(relay_data_path)
|
|
.await
|
|
.expect("open history db")
|
|
}
|
|
|
|
async fn build_announcement_version(
|
|
client: &AuditClient,
|
|
repo_id: &str,
|
|
created_at: Timestamp,
|
|
content: &str,
|
|
) -> Event {
|
|
let relay_url = client
|
|
.relay_url()
|
|
.await
|
|
.expect("client should have relay url");
|
|
let relay_domain = relay_url
|
|
.trim_start_matches("ws://")
|
|
.trim_start_matches("wss://")
|
|
.to_string();
|
|
let http_url = format!("http://{}", relay_domain);
|
|
let npub = client.public_key().to_bech32().expect("pubkey to npub");
|
|
|
|
EventBuilder::new(Kind::GitRepoAnnouncement, content)
|
|
.tags(vec![
|
|
Tag::identifier(repo_id),
|
|
Tag::custom("name", vec![repo_id.to_string()]),
|
|
Tag::custom(
|
|
"clone",
|
|
vec![format!("{}/{}/{}.git", http_url, npub, repo_id)],
|
|
),
|
|
Tag::custom("relays", vec![relay_url]),
|
|
])
|
|
.custom_created_at(created_at)
|
|
.finalize(client.keys())
|
|
.expect("build announcement version")
|
|
}
|
|
|
|
fn build_state_version(
|
|
client: &AuditClient,
|
|
repo_id: &str,
|
|
created_at: Timestamp,
|
|
content: &str,
|
|
) -> Event {
|
|
EventBuilder::new(Kind::RepoState, content)
|
|
.tags(vec![
|
|
Tag::identifier(repo_id),
|
|
Tag::custom(
|
|
"refs/heads/main",
|
|
vec![DETERMINISTIC_COMMIT_HASH.to_string()],
|
|
),
|
|
Tag::custom("HEAD", vec!["ref: refs/heads/main".to_string()]),
|
|
])
|
|
.custom_created_at(created_at)
|
|
.finalize(client.keys())
|
|
.expect("build state version")
|
|
}
|
|
|
|
/// Build a same-coordinate candidate with ngit's one-use nonce tag.
|
|
fn build_nonce_state_version(
|
|
client: &AuditClient,
|
|
repo_id: &str,
|
|
created_at: Timestamp,
|
|
nonce: u64,
|
|
) -> Event {
|
|
EventBuilder::new(Kind::RepoState, "state-v2")
|
|
.tags(vec![
|
|
Tag::identifier(repo_id),
|
|
Tag::custom(
|
|
"refs/heads/main",
|
|
vec![DETERMINISTIC_COMMIT_HASH.to_string()],
|
|
),
|
|
Tag::custom("HEAD", vec!["ref: refs/heads/main".to_string()]),
|
|
Tag::custom(
|
|
"nonce",
|
|
vec![
|
|
nonce.to_string(),
|
|
"0".to_string(),
|
|
"ngit-created-at-tiebreak".to_string(),
|
|
],
|
|
),
|
|
])
|
|
.custom_created_at(created_at)
|
|
.finalize(client.keys())
|
|
.expect("build nonce state version")
|
|
}
|
|
|
|
fn build_policy_for_history_regression(
|
|
database: SharedDatabase,
|
|
history: ReplaceableHistoryStore,
|
|
git_data_path: &std::path::Path,
|
|
) -> Nip34WritePolicy {
|
|
let config = Config::parse_from([
|
|
"ngit-grasp-test",
|
|
"--domain",
|
|
"test.example.com",
|
|
"--archive-all",
|
|
"--archive-read-only",
|
|
"false",
|
|
]);
|
|
let purgatory = Arc::new(Purgatory::new(git_data_path.to_path_buf()));
|
|
|
|
Nip34WritePolicy::new(
|
|
database,
|
|
Tombstones::in_memory(),
|
|
HoldingStore::in_memory(),
|
|
RepositoryLifecycle::in_memory(),
|
|
history,
|
|
git_data_path.to_path_buf(),
|
|
purgatory,
|
|
config,
|
|
new_repo_init_locks(),
|
|
None,
|
|
)
|
|
}
|
|
|
|
fn build_policy_announcement_version(
|
|
keys: &Keys,
|
|
repo_id: &str,
|
|
created_at: Timestamp,
|
|
content: &str,
|
|
) -> Event {
|
|
let npub = keys.public_key().to_bech32().expect("pubkey to npub");
|
|
|
|
EventBuilder::new(Kind::GitRepoAnnouncement, content)
|
|
.tags(vec![
|
|
Tag::identifier(repo_id),
|
|
Tag::custom("name", vec![repo_id.to_string()]),
|
|
Tag::custom(
|
|
"clone",
|
|
vec![format!("http://test.example.com/{}/{}.git", npub, repo_id)],
|
|
),
|
|
Tag::custom("relays", vec!["wss://test.example.com".to_string()]),
|
|
])
|
|
.custom_created_at(created_at)
|
|
.finalize(keys)
|
|
.expect("build policy announcement version")
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn newer_30618_supersedes_older_and_preserves_old_in_history() {
|
|
let relay = TestRelay::start_with_lmdb().await;
|
|
let client = AuditClient::new(relay.url(), AuditConfig::isolated())
|
|
.await
|
|
.expect("create audit client");
|
|
|
|
let (_announcement, repo_id, initial_state) =
|
|
publish_served_audit_repo_with_state(&client, "history-30618").await;
|
|
let base_ts = timestamp_after(initial_state.created_at);
|
|
let old_state = build_state_version(&client, &repo_id, base_ts, "state-v1");
|
|
let new_state = build_state_version(&client, &repo_id, timestamp_after(base_ts), "state-v2");
|
|
|
|
client
|
|
.send_event(old_state.clone())
|
|
.await
|
|
.expect("send old state");
|
|
client
|
|
.send_event(new_state.clone())
|
|
.await
|
|
.expect("send new state");
|
|
tokio::time::sleep(Duration::from_millis(400)).await;
|
|
|
|
let coordinate = format!("30618:{}:{}", client.public_key().to_hex(), repo_id);
|
|
let history = open_history(relay.relay_data_path()).await;
|
|
let records = history
|
|
.superseded_records_for_coordinate_before(
|
|
&coordinate,
|
|
// The deterministic future timestamps can be ahead of wall-clock
|
|
// `Timestamp::now()`, so query just after the newest test event
|
|
// rather than using "now" as the history cutoff.
|
|
Timestamp::from_secs(new_state.created_at.as_secs() + 1),
|
|
)
|
|
.await;
|
|
|
|
assert!(
|
|
records
|
|
.iter()
|
|
.any(|r| r.superseded_event_id == Some(old_state.id)
|
|
&& r.replaced_by == Some(new_state.id)),
|
|
"history must contain old 30618 as superseded by the newer 30618"
|
|
);
|
|
assert!(
|
|
history
|
|
.event_by_id(&old_state.id)
|
|
.await
|
|
.expect("lookup old state in history")
|
|
.is_some(),
|
|
"history payload must persist the superseded 30618 event"
|
|
);
|
|
|
|
relay.stop().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn same_second_30618_replacement_keeps_nip01_lowest_id() {
|
|
// Own the directories outside the fixture so the exact same LMDB database
|
|
// can be reopened after the relay subprocess is stopped.
|
|
let git_data_dir = tempfile::tempdir().expect("create git data directory");
|
|
let relay_data_dir = tempfile::tempdir().expect("create relay data directory");
|
|
let git_data_path = git_data_dir.path().to_path_buf();
|
|
let relay_data_path = relay_data_dir.path().to_path_buf();
|
|
let relay = TestRelay::start_with_existing_lmdb_paths(
|
|
git_data_path.clone(),
|
|
relay_data_path.clone(),
|
|
None,
|
|
false,
|
|
)
|
|
.await;
|
|
let client = AuditClient::new(relay.url(), AuditConfig::isolated())
|
|
.await
|
|
.expect("create audit client");
|
|
|
|
let (_announcement, repo_id, initial_state) =
|
|
publish_served_audit_repo_with_state(&client, "same-second-state").await;
|
|
// Keep the test sequence after the fixture's initial state event while
|
|
// deliberately making the two explicit versions share one timestamp.
|
|
let created_at = timestamp_after(initial_state.created_at);
|
|
let mut candidates = [
|
|
build_nonce_state_version(&client, &repo_id, created_at, 0),
|
|
build_nonce_state_version(&client, &repo_id, created_at, 1),
|
|
];
|
|
candidates.sort_by_key(|event| event.id);
|
|
let [replacement, existing] = candidates;
|
|
|
|
assert_eq!(existing.created_at, replacement.created_at);
|
|
assert!(
|
|
replacement.id < existing.id,
|
|
"ngit's equal-timestamp replacement must have the lower event ID"
|
|
);
|
|
|
|
client
|
|
.send_event(existing.clone())
|
|
.await
|
|
.expect("send existing state");
|
|
client
|
|
.send_event(replacement.clone())
|
|
.await
|
|
.expect("send NIP-01-preferred replacement state");
|
|
|
|
assert!(
|
|
!client
|
|
.is_event_on_relay(existing.id)
|
|
.await
|
|
.expect("query existing state by ID"),
|
|
"the larger-ID state must be removed after the same-second replacement"
|
|
);
|
|
assert!(
|
|
client
|
|
.is_event_on_relay(replacement.id)
|
|
.await
|
|
.expect("query replacement state by ID"),
|
|
"the lower-ID state must be returned as the NIP-01 winner"
|
|
);
|
|
|
|
relay.stop().await;
|
|
|
|
let restarted =
|
|
TestRelay::start_with_existing_lmdb_paths(git_data_path, relay_data_path, None, false)
|
|
.await;
|
|
let restarted_client = AuditClient::new(restarted.url(), AuditConfig::isolated())
|
|
.await
|
|
.expect("create client for restarted relay");
|
|
|
|
assert!(
|
|
!restarted_client
|
|
.is_event_on_relay(existing.id)
|
|
.await
|
|
.expect("query existing state by ID after restart"),
|
|
"the larger-ID state must remain absent after restart"
|
|
);
|
|
assert!(
|
|
restarted_client
|
|
.is_event_on_relay(replacement.id)
|
|
.await
|
|
.expect("query replacement state by ID after restart"),
|
|
"the lower-ID state must remain the NIP-01 winner after restart"
|
|
);
|
|
|
|
let deletion = EventBuilder::new(Kind::EventDeletion, "delete same-second winner")
|
|
.tag(Tag::event(replacement.id))
|
|
.custom_created_at(timestamp_after(replacement.created_at))
|
|
.finalize(client.keys())
|
|
.expect("sign deletion with original author");
|
|
restarted_client
|
|
.send_event(deletion)
|
|
.await
|
|
.expect("delete active winner");
|
|
common::wait_for_event_served(restarted.url(), &existing.id, Duration::from_secs(10))
|
|
.await
|
|
.expect("restore same-second predecessor from durable history");
|
|
assert!(!restarted_client
|
|
.is_event_on_relay(replacement.id)
|
|
.await
|
|
.unwrap());
|
|
|
|
restarted.stop().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn newer_30617_supersedes_older_and_preserves_old_in_history() {
|
|
let relay = TestRelay::start_with_lmdb().await;
|
|
let client = AuditClient::new(relay.url(), AuditConfig::isolated())
|
|
.await
|
|
.expect("create audit client");
|
|
|
|
let (first_announcement, repo_id) = publish_served_repo(&client, "history-30617").await;
|
|
let newer_announcement = build_announcement_version(
|
|
&client,
|
|
&repo_id,
|
|
Timestamp::from_secs(first_announcement.created_at.as_secs() + 30),
|
|
"re-announcement",
|
|
)
|
|
.await;
|
|
|
|
client
|
|
.send_event(newer_announcement.clone())
|
|
.await
|
|
.expect("send newer announcement");
|
|
tokio::time::sleep(Duration::from_millis(400)).await;
|
|
|
|
let coordinate = format!("30617:{}:{}", client.public_key().to_hex(), repo_id);
|
|
let history = open_history(relay.relay_data_path()).await;
|
|
let records = history
|
|
.superseded_records_for_coordinate_before(&coordinate, Timestamp::now())
|
|
.await;
|
|
|
|
assert!(
|
|
records.iter().any(|r| {
|
|
r.superseded_event_id == Some(first_announcement.id)
|
|
&& r.replaced_by == Some(newer_announcement.id)
|
|
}),
|
|
"history must contain old 30617 as superseded by the newer 30617"
|
|
);
|
|
assert!(
|
|
history
|
|
.event_by_id(&first_announcement.id)
|
|
.await
|
|
.expect("lookup old announcement in history")
|
|
.is_some(),
|
|
"history payload must persist the superseded 30617 event"
|
|
);
|
|
|
|
relay.stop().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn sync_style_admit_then_save_captures_superseded_history_once() {
|
|
use nostr_sdk::prelude::{WritePolicy, WritePolicyResult};
|
|
|
|
let git_dir = tempfile::tempdir().expect("git tempdir");
|
|
let db: SharedDatabase = Arc::new(nostr_memory::MemoryDatabase::unbounded());
|
|
let history = ReplaceableHistoryStore::in_memory();
|
|
let policy = build_policy_for_history_regression(db.clone(), history.clone(), git_dir.path());
|
|
let keys = Keys::generate();
|
|
let repo_id = format!("history-sync-once-{}", &keys.public_key().to_hex()[..8]);
|
|
let old_announcement = build_policy_announcement_version(
|
|
&keys,
|
|
&repo_id,
|
|
Timestamp::from_secs(Timestamp::now().as_secs() + 1),
|
|
"old",
|
|
);
|
|
let new_announcement = build_policy_announcement_version(
|
|
&keys,
|
|
&repo_id,
|
|
Timestamp::from_secs(old_announcement.created_at.as_secs() + 30),
|
|
"new",
|
|
);
|
|
|
|
db.save_event(&old_announcement)
|
|
.await
|
|
.expect("save old announcement");
|
|
|
|
let dummy_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
|
|
let result = policy.admit_event(&new_announcement, &dummy_addr).await;
|
|
assert!(
|
|
matches!(result, WritePolicyResult::Accept),
|
|
"new announcement must be accepted before central save"
|
|
);
|
|
|
|
// Cross a timestamp-second boundary before saving. The old persistence hook
|
|
// also captured history here, producing a distinct metadata event instead
|
|
// of collapsing onto the admission hook's metadata ID.
|
|
tokio::time::sleep(Duration::from_millis(1100)).await;
|
|
policy
|
|
.save_accepted_event(&new_announcement, SaveContext::RelaySync)
|
|
.await
|
|
.expect("save accepted announcement");
|
|
|
|
let coordinate = format!("30617:{}:{}", keys.public_key().to_hex(), repo_id);
|
|
let records = history
|
|
.superseded_records_for_coordinate_before(
|
|
&coordinate,
|
|
Timestamp::from_secs(new_announcement.created_at.as_secs() + 1),
|
|
)
|
|
.await;
|
|
let matching_records = records
|
|
.iter()
|
|
.filter(|r| {
|
|
r.superseded_event_id == Some(old_announcement.id)
|
|
&& r.replaced_by == Some(new_announcement.id)
|
|
})
|
|
.count();
|
|
|
|
assert_eq!(
|
|
matching_records, 1,
|
|
"sync-style admit then save must not duplicate superseded history metadata"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn replaceable_history_survives_restart_lmdb() {
|
|
let relay = TestRelay::start_with_lmdb().await;
|
|
let client = AuditClient::new(relay.url(), AuditConfig::isolated())
|
|
.await
|
|
.expect("create audit client");
|
|
|
|
let (first_announcement, repo_id) = publish_served_repo(&client, "history-restart").await;
|
|
let newer_announcement = build_announcement_version(
|
|
&client,
|
|
&repo_id,
|
|
Timestamp::from_secs(first_announcement.created_at.as_secs() + 45),
|
|
"restart-reannouncement",
|
|
)
|
|
.await;
|
|
client
|
|
.send_event(newer_announcement.clone())
|
|
.await
|
|
.expect("send newer announcement");
|
|
tokio::time::sleep(Duration::from_millis(400)).await;
|
|
|
|
let coordinate = format!("30617:{}:{}", client.public_key().to_hex(), repo_id);
|
|
let history_before_restart = open_history(relay.relay_data_path()).await;
|
|
let pre_restart_records = history_before_restart
|
|
.superseded_records_for_coordinate_before(&coordinate, Timestamp::now())
|
|
.await;
|
|
assert!(
|
|
pre_restart_records.iter().any(|r| {
|
|
r.superseded_event_id == Some(first_announcement.id)
|
|
&& r.replaced_by == Some(newer_announcement.id)
|
|
}),
|
|
"history metadata must exist before restart"
|
|
);
|
|
|
|
let relay_data_path = relay.relay_data_path().clone();
|
|
let git_data_path = relay.git_data_path().clone();
|
|
let owner_hex = client.public_key().to_hex();
|
|
relay.stop().await;
|
|
|
|
let restarted = TestRelay::start_with_existing_lmdb_paths(
|
|
git_data_path,
|
|
relay_data_path.clone(),
|
|
None,
|
|
false,
|
|
)
|
|
.await;
|
|
restarted.stop().await;
|
|
|
|
let history = open_history(&relay_data_path).await;
|
|
let coordinate = format!("30617:{}:{}", owner_hex, repo_id);
|
|
let records = history
|
|
.superseded_records_for_coordinate_before(&coordinate, Timestamp::now())
|
|
.await;
|
|
|
|
assert!(
|
|
records.iter().any(|r| {
|
|
r.superseded_event_id == Some(first_announcement.id)
|
|
&& r.replaced_by == Some(newer_announcement.id)
|
|
}),
|
|
"history metadata must survive LMDB restart"
|
|
);
|
|
assert!(
|
|
history
|
|
.event_by_id(&first_announcement.id)
|
|
.await
|
|
.expect("lookup archived announcement after restart")
|
|
.is_some(),
|
|
"history payload must survive LMDB restart"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn serving_behavior_unchanged_latest_version_is_still_served() {
|
|
let relay = TestRelay::start_with_lmdb().await;
|
|
let client = AuditClient::new(relay.url(), AuditConfig::isolated())
|
|
.await
|
|
.expect("create audit client");
|
|
|
|
let (_announcement, repo_id) = publish_served_repo(&client, "history-serving").await;
|
|
// `publish_served_repo` already saved a 30618 for this coordinate. Keep the
|
|
// explicit state sequence in future seconds so the relay's second-granular
|
|
// replaceable ordering cannot tie it with the fixture event.
|
|
let base_ts = Timestamp::from_secs(Timestamp::now().as_secs() + 1);
|
|
let old_state = build_state_version(&client, &repo_id, base_ts, "state-old");
|
|
let new_state = build_state_version(
|
|
&client,
|
|
&repo_id,
|
|
Timestamp::from_secs(base_ts.as_secs() + 30),
|
|
"state-new",
|
|
);
|
|
|
|
client
|
|
.send_event(old_state.clone())
|
|
.await
|
|
.expect("send old state");
|
|
client
|
|
.send_event(new_state.clone())
|
|
.await
|
|
.expect("send new state");
|
|
tokio::time::sleep(Duration::from_millis(400)).await;
|
|
|
|
let by_id_old = client
|
|
.is_event_on_relay(old_state.id)
|
|
.await
|
|
.expect("query old state by id");
|
|
let by_id_new = client
|
|
.is_event_on_relay(new_state.id)
|
|
.await
|
|
.expect("query new state by id");
|
|
assert!(
|
|
!by_id_old,
|
|
"superseded state must remain non-served in the main relay behavior"
|
|
);
|
|
assert!(by_id_new, "latest state must still be served");
|
|
|
|
relay.stop().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn history_not_captured_for_purgatory_only_announcement() {
|
|
let relay = TestRelay::start_with_lmdb().await;
|
|
let client = AuditClient::new(relay.url(), AuditConfig::isolated())
|
|
.await
|
|
.expect("create audit client");
|
|
|
|
let relay_url = client
|
|
.relay_url()
|
|
.await
|
|
.expect("client should have relay url");
|
|
let relay_domain = relay_url
|
|
.trim_start_matches("ws://")
|
|
.trim_start_matches("wss://")
|
|
.to_string();
|
|
let npub = client.public_key().to_bech32().expect("pubkey to npub");
|
|
let repo_id = format!(
|
|
"history-purgatory-only-{}",
|
|
&Keys::generate().public_key().to_hex()[..8]
|
|
);
|
|
|
|
let announcement = EventBuilder::new(Kind::GitRepoAnnouncement, "")
|
|
.tags(vec![
|
|
Tag::identifier(&repo_id),
|
|
Tag::custom("name", vec![repo_id.clone()]),
|
|
Tag::custom(
|
|
"clone",
|
|
vec![format!("http://{}/{}/{}.git", relay_domain, npub, repo_id)],
|
|
),
|
|
Tag::custom("relays", vec![relay_url]),
|
|
])
|
|
.finalize(client.keys())
|
|
.expect("build purgatory-only announcement");
|
|
|
|
client
|
|
.send_event(announcement)
|
|
.await
|
|
.expect("send announcement to purgatory");
|
|
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
|
|
let coordinate = format!("30617:{}:{}", client.public_key().to_hex(), repo_id);
|
|
let history = open_history(relay.relay_data_path()).await;
|
|
let latest = history
|
|
.latest_superseded_before(&coordinate, Timestamp::now())
|
|
.await;
|
|
assert!(
|
|
latest.is_none(),
|
|
"purgatory-only announcements must not capture superseded history"
|
|
);
|
|
|
|
relay.stop().await;
|
|
}
|