purgatory: when state data recieved sync across repositoies

This commit is contained in:
DanConwayDev
2025-12-31 12:42:26 +00:00
parent 91815d7154
commit f5c8d167c3
3 changed files with 255 additions and 26 deletions
@@ -766,32 +766,159 @@ impl PushAuthorizationTests {
///
/// GRASP-01: "respecting the recursive maintainer set"
///
/// This test verifies that when a maintainer publishes a state event, it updates
/// the git repository state for all other maintainers' views. This ensures git
/// repositories always reflect the state according to nostr events (including state
/// from recursive maintainers).
/// This test verifies that when a maintainer publishes a state event, the purgatory
/// feature correctly copies git commits between repos so all authorized maintainers'
/// repositories always reflect the state according to nostr events.
///
/// ## Implementation Note
/// ## Implementation
///
/// This test is a stub for the purgatory feature. It will be implemented as part
/// of GRASP-02 purgatory functionality.
/// This test:
/// 1. Uses RecursiveMaintainerStateDataPushed fixture which:
/// - Creates owner repo + state (ValidRepo, OwnerStateDataPushed)
/// - Creates maintainer announcement (separate repo for maintainer)
/// - Pushes recursive maintainer's git data to owner's repo
/// 2. Clones the maintainer's repository (not the owner's)
/// 3. Verifies that the maintainer's repo contains the recursive maintainer's state
///
/// ## Fixture Compatibility
///
/// This test will use:
/// - `MaintainerStateDataPushed` - maintainer's state event with git data pushed
/// - Multiple maintainer clones to verify state propagation
#[allow(dead_code)]
/// This proves purgatory is working: git data was pushed to owner's repo, and purgatory
/// synced it to the maintainer's repo based on the state event.
pub async fn test_push_of_state_by_maintainer_updates_other_maintainer_repos(
_client: &AuditClient,
_relay_domain: &str,
client: &AuditClient,
relay_domain: &str,
) -> TestResult {
TestResult::new(
"test_push_of_state_by_maintainer_updates_other_maintainer_repos",
"GRASP-01:git-http:purgatory",
"Maintainer state updates propagate to other maintainer repos",
)
.fail("Not yet implemented - requires purgatory feature (GRASP-02)")
let test_name = "test_push_of_state_by_maintainer_updates_other_maintainer_repos";
let ctx = TestContext::new(client);
// Get the RecursiveMaintainerStateDataPushed fixture which:
// 1. Creates owner repo + owner state + git data
// 2. Creates maintainer announcement (separate repo)
// 3. Pushes recursive maintainer's git data to owner's repo
// 4. Purgatory should then sync to maintainer's repo
let recursive_state = match ctx
.get_fixture(FixtureKind::RecursiveMaintainerStateDataPushed)
.await
{
Ok(s) => s,
Err(e) => {
return TestResult::new(
test_name,
"GRASP-01:git-http:purgatory",
"Maintainer state updates propagate to other maintainer repos",
)
.fail(format!(
"Failed to get RecursiveMaintainerStateDataPushed fixture: {}",
e
))
}
};
// Small delay to ensure state processing completes
tokio::time::sleep(Duration::from_millis(200)).await;
// Extract repo_id from the recursive maintainer's state event
let repo_id = match recursive_state
.tags
.iter()
.find(|t| t.kind() == TagKind::d())
.and_then(|t| t.content())
{
Some(id) => id.to_string(),
None => {
return TestResult::new(
test_name,
"GRASP-01:git-http:purgatory",
"Maintainer state updates propagate to other maintainer repos",
)
.fail("No repo identifier in recursive maintainer state event")
}
};
// Get the maintainer's npub
let maintainer_npub = match client.maintainer_keys().public_key().to_bech32() {
Ok(npub) => npub,
Err(e) => {
return TestResult::new(
test_name,
"GRASP-01:git-http:purgatory",
"Maintainer state updates propagate to other maintainer repos",
)
.fail(format!(
"Failed to convert maintainer pubkey to npub: {}",
e
))
}
};
// Use the known recursive maintainer deterministic commit hash from the fixture
let expected_commit = crate::RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH;
// Clone the maintainer's repository (NOT the owner's)
// This is the key test: git data was pushed to owner's repo, does maintainer's repo have it?
let clone_path = match clone_repo(relay_domain, &maintainer_npub, &repo_id) {
Ok(path) => path,
Err(e) => {
return TestResult::new(
test_name,
"GRASP-01:git-http:purgatory",
"Maintainer state updates propagate to other maintainer repos",
)
.fail(format!("Failed to clone maintainer's repo: {}", e))
}
};
let cleanup = || {
let _ = fs::remove_dir_all(&clone_path);
};
// Verify that the maintainer's repo contains the recursive maintainer's commit
// This proves purgatory copied it from owner's repo
let commit_exists_output = Command::new("git")
.args(["cat-file", "-t", expected_commit])
.current_dir(&clone_path)
.output();
let commit_exists = match commit_exists_output {
Ok(output) => {
if output.status.success() {
let obj_type = String::from_utf8_lossy(&output.stdout);
obj_type.trim() == "commit"
} else {
false
}
}
Err(e) => {
cleanup();
return TestResult::new(
test_name,
"GRASP-01:git-http:purgatory",
"Maintainer state updates propagate to other maintainer repos",
)
.fail(format!("Failed to check if commit exists: {}", e));
}
};
cleanup();
if commit_exists {
TestResult::new(
test_name,
"GRASP-01:git-http:purgatory",
"Maintainer state updates propagate to other maintainer repos",
)
.pass()
} else {
TestResult::new(
test_name,
"GRASP-01:git-http:purgatory",
"Maintainer state updates propagate to other maintainer repos",
)
.fail(format!(
"Maintainer's repo does not contain recursive maintainer's commit {}. \
Git data was pushed to owner's repo, but purgatory did not copy it to \
maintainer's repo. This indicates the purgatory feature is not working correctly.",
expected_commit
))
}
}
/// Test that non-maintainer state event is ignored
+106 -1
View File
@@ -125,7 +125,27 @@ impl StatePolicy {
{
let repo_path =
self.ctx.git_data_path.join(annoucement.repo_path().clone());
// TODO - if repo_path != repo_with_git_data, pass as a datasource for missing data?
if !repo_path.exists() {
// eg if annoucement doesnt list repo (but stored as its in maintainer set)
continue;
}
// If repo_path != repo_with_git_data, copy missing oids first
if repo_path != repo_with_git_data {
if let Err(e) = self.copy_missing_oids(
&repo_with_git_data,
&repo_path,
&state,
) {
tracing::warn!(
"Failed to copy oids from {} to {}: {}",
repo_with_git_data.display(),
repo_path.display(),
e
);
}
}
let result = self.align_repository_with_state(&repo_path, &state);
repo_count += 1;
tracing::info!(
@@ -335,6 +355,91 @@ impl StatePolicy {
result
}
/// Copy missing OIDs from a source repository to a target repository
///
/// Identifies commits referenced in the state that are missing from the target
/// repository and copies them from the source repository using git fetch.
///
/// # Arguments
/// * `source_repo` - Path to repository containing the commits
/// * `target_repo` - Path to repository to receive the commits
/// * `state` - Repository state containing commit references
///
/// # Returns
/// Ok(()) on success, Err with error message on failure
fn copy_missing_oids(
&self,
source_repo: &Path,
target_repo: &Path,
state: &RepositoryState,
) -> Result<(), String> {
use std::process::Command;
// Collect all commits referenced in the state
let mut commits_to_check = Vec::new();
for branch in &state.branches {
if !branch.commit.starts_with("ref: ") {
commits_to_check.push(&branch.commit);
}
}
for tag in &state.tags {
if !tag.commit.starts_with("ref: ") {
commits_to_check.push(&tag.commit);
}
}
// Identify missing commits
let mut missing_commits = Vec::new();
for commit in commits_to_check {
if !git::oid_exists(target_repo, commit) {
missing_commits.push(commit);
}
}
if missing_commits.is_empty() {
tracing::debug!(
"No missing commits to copy from {} to {}",
source_repo.display(),
target_repo.display()
);
return Ok(());
}
tracing::info!(
"Copying {} missing commits from {} to {}",
missing_commits.len(),
source_repo.display(),
target_repo.display()
);
// Fetch each missing commit from source to target
for commit in &missing_commits {
let output = Command::new("git")
.args([
"fetch",
source_repo.to_str().ok_or("Invalid source path")?,
commit,
])
.current_dir(target_repo)
.output()
.map_err(|e| format!("Failed to execute git fetch: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!(
"git fetch failed for commit {}: {}",
commit, stderr
));
}
tracing::debug!("Copied commit {} to {}", commit, target_repo.display());
}
Ok(())
}
}
fn find_repo_with_git_data(
+1 -4
View File
@@ -72,7 +72,4 @@ isolated_push_test!(
);
isolated_push_test!(test_head_set_after_state_event_with_existing_commit);
isolated_push_test!(test_head_set_after_git_push_with_required_oids);
// Note: test_push_of_state_by_maintainer_updates_other_maintainer_repos is not included
// as it's a stub for the purgatory feature. It can be run manually once implemented:
// isolated_push_test!(test_push_of_state_by_maintainer_updates_other_maintainer_repos);
isolated_push_test!(test_push_of_state_by_maintainer_updates_other_maintainer_repos);