#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)]
use prikk_error::Result;
use super::sender_test_support::{
adopt, author_signer, cleanup, create_file_patch, maintainer_signer, repo, seal_patches_onto,
write_blob,
};
use super::{SyncArtifactOutcome, build_sync_artifact};
use crate::patch_set_digest::compute_patch_set_digest_and_count_from_block;
use crate::sync_negotiation::{
DEFAULT_HAVE_LIST_MAX_PATCH_COUNT, DEFAULT_HAVE_LIST_MAX_TOTAL_BYTES, build_have_list,
decode_have_list,
};
use crate::tag_travel::create_local_tag;
use crate::{AcceptOptions, FileObjectStore, accept_exchange_artifact, seal_from_accepted_claim};
const TARGET_REF: &str = "heads/main";
#[test]
fn a_sender_absent_ref_reports_already_in_sync_not_a_refusal() -> Result<()> {
let sender = repo("sender-stage3-absent-sender")?;
let signer = maintainer_signer(0x10)?;
adopt(&sender, &signer)?;
let receiver = repo("sender-stage3-absent-sender-receiver")?;
let have_list_bytes = build_have_list(&receiver, TARGET_REF)?;
let outcome = build_sync_artifact(&sender, TARGET_REF, &have_list_bytes, &signer)?;
match outcome {
SyncArtifactOutcome::AlreadyInSync { ref_name } => assert_eq!(ref_name, TARGET_REF),
other => panic!("expected AlreadyInSync, got {other:?}"),
}
cleanup(&sender);
cleanup(&receiver);
Ok(())
}
#[test]
fn a_receiver_absent_ref_produces_the_full_reachable_set_as_the_delta() -> Result<()> {
let sender = repo("sender-stage3-absent-receiver")?;
let signer = maintainer_signer(0x11)?;
adopt(&sender, &signer)?;
let author = author_signer(0x12)?;
let mut objects = FileObjectStore::new(sender.clone());
let blob_a = write_blob(&mut objects, b"stage3 a\n")?;
let patch_a = create_file_patch(&author, "a.txt", 0x13, blob_a)?;
let blob_b = write_blob(&mut objects, b"stage3 b\n")?;
let patch_b = create_file_patch(&author, "b.txt", 0x14, blob_b)?;
seal_patches_onto(
&sender,
TARGET_REF,
&[patch_a.clone(), patch_b.clone()],
&signer,
)?;
let receiver = repo("sender-stage3-absent-receiver-side")?;
let have_list_bytes = build_have_list(&receiver, TARGET_REF)?;
let outcome = build_sync_artifact(&sender, TARGET_REF, &have_list_bytes, &signer)?;
match outcome {
SyncArtifactOutcome::Artifact { report, .. } => {
assert_eq!(report.delta_patch_count, 2);
assert_eq!(report.export_report.patch_count, 2);
}
other => panic!("expected Artifact, got {other:?}"),
}
cleanup(&sender);
cleanup(&receiver);
Ok(())
}
#[test]
fn an_already_synced_ref_reports_already_in_sync() -> Result<()> {
let sender = repo("sender-stage3-already-synced-sender")?;
let sender_signer = maintainer_signer(0x15)?;
adopt(&sender, &sender_signer)?;
let author = author_signer(0x16)?;
let mut sender_objects = FileObjectStore::new(sender.clone());
let blob = write_blob(&mut sender_objects, b"stage3 synced\n")?;
let patch = create_file_patch(&author, "synced.txt", 0x17, blob)?;
seal_patches_onto(
&sender,
TARGET_REF,
std::slice::from_ref(&patch),
&sender_signer,
)?;
let receiver = repo("sender-stage3-already-synced-receiver")?;
let receiver_signer = maintainer_signer(0x18)?;
adopt(&receiver, &receiver_signer)?;
let mut receiver_objects = FileObjectStore::new(receiver.clone());
let receiver_blob = write_blob(&mut receiver_objects, b"stage3 synced\n")?;
assert_eq!(receiver_blob, blob);
let receiver_patch = create_file_patch(&author, "synced.txt", 0x17, receiver_blob)?;
assert_eq!(receiver_patch.object_id(), patch.object_id());
seal_patches_onto(&receiver, TARGET_REF, &[receiver_patch], &receiver_signer)?;
let have_list_bytes = build_have_list(&receiver, TARGET_REF)?;
let outcome = build_sync_artifact(&sender, TARGET_REF, &have_list_bytes, &sender_signer)?;
assert!(matches!(outcome, SyncArtifactOutcome::AlreadyInSync { .. }));
cleanup(&sender);
cleanup(&receiver);
Ok(())
}
#[test]
fn a_tag_on_an_already_synced_ref_still_travels_even_with_an_empty_delta() -> Result<()> {
let sender = repo("sender-stage3-tag-travel-sender")?;
let sender_signer = maintainer_signer(0x19)?;
adopt(&sender, &sender_signer)?;
let author = author_signer(0x1a)?;
let mut sender_objects = FileObjectStore::new(sender.clone());
let blob = write_blob(&mut sender_objects, b"stage3 tag travel\n")?;
let patch = create_file_patch(&author, "tag-travel.txt", 0x1b, blob)?;
let tip = seal_patches_onto(
&sender,
TARGET_REF,
std::slice::from_ref(&patch),
&sender_signer,
)?;
let receiver = repo("sender-stage3-tag-travel-receiver")?;
let receiver_signer = maintainer_signer(0x1c)?;
adopt(&receiver, &receiver_signer)?;
let mut receiver_objects = FileObjectStore::new(receiver.clone());
let receiver_blob = write_blob(&mut receiver_objects, b"stage3 tag travel\n")?;
let receiver_patch = create_file_patch(&author, "tag-travel.txt", 0x1b, receiver_blob)?;
seal_patches_onto(&receiver, TARGET_REF, &[receiver_patch], &receiver_signer)?;
let (digest, count) = compute_patch_set_digest_and_count_from_block(&sender_objects, tip)?;
let mut write_session = crate::object_store::ObjectWriteSession::open(&sender)?;
create_local_tag(
&sender,
&mut write_session,
"tags/v1",
tip,
None,
digest,
count,
&sender_signer,
)?;
drop(write_session);
let have_list_bytes = build_have_list(&receiver, TARGET_REF)?;
let outcome = build_sync_artifact(&sender, TARGET_REF, &have_list_bytes, &sender_signer)?;
match outcome {
SyncArtifactOutcome::Artifact { report, .. } => {
assert_eq!(
report.delta_patch_count, 0,
"the patch delta itself must still be empty -- only the tag is new"
);
assert_eq!(report.tag_count, 1);
assert_eq!(report.export_report.tag_count, 1);
}
other => panic!(
"expected an Artifact carrying just the tag, got {other:?} -- tag-travel must not be \
gated on a non-empty patch delta"
),
}
cleanup(&sender);
cleanup(&receiver);
Ok(())
}
#[test]
fn the_artifact_carries_exactly_the_delta_not_the_full_reachable_set() -> Result<()> {
let sender = repo("sender-stage3-row1-sender")?;
let sender_signer = maintainer_signer(0x20)?;
adopt(&sender, &sender_signer)?;
let author = author_signer(0x21)?;
let mut sender_objects = FileObjectStore::new(sender.clone());
let blob_a = write_blob(&mut sender_objects, b"row1 a\n")?;
let patch_a = create_file_patch(&author, "row1-a.txt", 0x22, blob_a)?;
let blob_b = write_blob(&mut sender_objects, b"row1 b\n")?;
let patch_b = create_file_patch(&author, "row1-b.txt", 0x23, blob_b)?;
seal_patches_onto(
&sender,
TARGET_REF,
&[patch_a.clone(), patch_b.clone()],
&sender_signer,
)?;
let receiver = repo("sender-stage3-row1-receiver")?;
let receiver_signer = maintainer_signer(0x24)?;
adopt(&receiver, &receiver_signer)?;
let mut receiver_objects = FileObjectStore::new(receiver.clone());
let receiver_blob_a = write_blob(&mut receiver_objects, b"row1 a\n")?;
let receiver_patch_a = create_file_patch(&author, "row1-a.txt", 0x22, receiver_blob_a)?;
assert_eq!(receiver_patch_a.object_id(), patch_a.object_id());
seal_patches_onto(&receiver, TARGET_REF, &[receiver_patch_a], &receiver_signer)?;
let have_list_bytes = build_have_list(&receiver, TARGET_REF)?;
let outcome = build_sync_artifact(&sender, TARGET_REF, &have_list_bytes, &sender_signer)?;
match outcome {
SyncArtifactOutcome::Artifact { report, .. } => {
assert_eq!(
report.delta_patch_count, 1,
"only patch_b is missing from the receiver"
);
assert_eq!(
report.export_report.patch_count, 1,
"the artifact's own patches section must carry only the delta, not the full \
two-patch block"
);
}
other => panic!("expected Artifact, got {other:?}"),
}
cleanup(&sender);
cleanup(&receiver);
Ok(())
}
#[test]
fn claims_carry_the_blocks_full_verbatim_patch_ids() -> Result<()> {
let sender = repo("sender-stage3-row2-sender")?;
let sender_signer = maintainer_signer(0x30)?;
adopt(&sender, &sender_signer)?;
let author = author_signer(0x31)?;
let mut sender_objects = FileObjectStore::new(sender.clone());
let blob_a = write_blob(&mut sender_objects, b"row2 a\n")?;
let patch_a = create_file_patch(&author, "row2-a.txt", 0x32, blob_a)?;
let blob_b = write_blob(&mut sender_objects, b"row2 b\n")?;
let patch_b = create_file_patch(&author, "row2-b.txt", 0x33, blob_b)?;
seal_patches_onto(
&sender,
TARGET_REF,
&[patch_a.clone(), patch_b.clone()],
&sender_signer,
)?;
let receiver = repo("sender-stage3-row2-receiver")?;
let receiver_signer = maintainer_signer(0x34)?;
adopt(&receiver, &receiver_signer)?;
let mut receiver_objects = FileObjectStore::new(receiver.clone());
let receiver_blob_a = write_blob(&mut receiver_objects, b"row2 a\n")?;
let receiver_patch_a = create_file_patch(&author, "row2-a.txt", 0x32, receiver_blob_a)?;
let receiver_blob_b = write_blob(&mut receiver_objects, b"row2 b\n")?;
let receiver_patch_b = create_file_patch(&author, "row2-b.txt", 0x33, receiver_blob_b)?;
assert_eq!(receiver_patch_a.object_id(), patch_a.object_id());
assert_eq!(receiver_patch_b.object_id(), patch_b.object_id());
seal_patches_onto(
&receiver,
TARGET_REF,
std::slice::from_ref(&receiver_patch_a),
&receiver_signer,
)?;
seal_patches_onto(
&receiver,
"heads/decoy",
&[receiver_patch_a, receiver_patch_b],
&receiver_signer,
)?;
let have_list_bytes = build_have_list(&receiver, TARGET_REF)?; let outcome = build_sync_artifact(&sender, TARGET_REF, &have_list_bytes, &sender_signer)?;
let bytes = match outcome {
SyncArtifactOutcome::Artifact { bytes, .. } => bytes,
other => panic!("expected Artifact, got {other:?}"),
};
let accept_result =
accept_exchange_artifact(&receiver, &bytes, &AcceptOptions::default_limits());
let report = accept_result.expect(
"a verbatim claim about a block the receiver already holds must read Consistent, not \
Contradicted",
);
assert_eq!(report.claim_count, 1);
cleanup(&sender);
cleanup(&receiver);
Ok(())
}
#[test]
fn claims_carry_the_blocks_full_verbatim_parent_block_ids() -> Result<()> {
let sender = repo("sender-stage3-row2-parent-sender")?;
let sender_signer = maintainer_signer(0x35)?;
adopt(&sender, &sender_signer)?;
let author = author_signer(0x36)?;
let mut sender_objects = FileObjectStore::new(sender.clone());
let blob_a = write_blob(&mut sender_objects, b"row2-parent a\n")?;
let patch_a = create_file_patch(&author, "row2-parent-a.txt", 0x37, blob_a)?;
let blob_b = write_blob(&mut sender_objects, b"row2-parent b\n")?;
let patch_b = create_file_patch(&author, "row2-parent-b.txt", 0x38, blob_b)?;
let block_a = seal_patches_onto(
&sender,
TARGET_REF,
std::slice::from_ref(&patch_a),
&sender_signer,
)?;
let block_b = seal_patches_onto(
&sender,
TARGET_REF,
std::slice::from_ref(&patch_b),
&sender_signer,
)?;
assert_ne!(block_a, block_b);
let receiver = repo("sender-stage3-row2-parent-receiver")?;
let receiver_signer = maintainer_signer(0x39)?;
adopt(&receiver, &receiver_signer)?;
let mut receiver_objects = FileObjectStore::new(receiver.clone());
let receiver_blob_a = write_blob(&mut receiver_objects, b"row2-parent a\n")?;
let receiver_patch_a = create_file_patch(&author, "row2-parent-a.txt", 0x37, receiver_blob_a)?;
let receiver_blob_b = write_blob(&mut receiver_objects, b"row2-parent b\n")?;
let receiver_patch_b = create_file_patch(&author, "row2-parent-b.txt", 0x38, receiver_blob_b)?;
assert_eq!(receiver_patch_a.object_id(), patch_a.object_id());
assert_eq!(receiver_patch_b.object_id(), patch_b.object_id());
let decoy_a = seal_patches_onto(
&receiver,
"heads/decoy",
std::slice::from_ref(&receiver_patch_a),
&receiver_signer,
)?;
let decoy_b = seal_patches_onto(
&receiver,
"heads/decoy",
std::slice::from_ref(&receiver_patch_b),
&receiver_signer,
)?;
assert_eq!(decoy_a, block_a);
assert_eq!(decoy_b, block_b);
let have_list_bytes = build_have_list(&receiver, TARGET_REF)?; let outcome = build_sync_artifact(&sender, TARGET_REF, &have_list_bytes, &sender_signer)?;
let bytes = match outcome {
SyncArtifactOutcome::Artifact { report, bytes } => {
assert_eq!(report.claim_count, 2, "both A and B qualify");
bytes
}
other => panic!("expected Artifact, got {other:?}"),
};
let accept_result =
accept_exchange_artifact(&receiver, &bytes, &AcceptOptions::default_limits());
let report = accept_result.expect(
"verbatim claims about blocks the receiver already holds must read Consistent, not \
Contradicted",
);
assert_eq!(report.claim_count, 2);
cleanup(&sender);
cleanup(&receiver);
Ok(())
}
#[test]
fn an_untrusted_signer_cannot_produce_an_artifact() -> Result<()> {
let sender = repo("sender-stage3-row3")?;
let signer = maintainer_signer(0x40)?;
adopt(&sender, &signer)?;
let author = author_signer(0x41)?;
let mut objects = FileObjectStore::new(sender.clone());
let blob = write_blob(&mut objects, b"row3\n")?;
let patch = create_file_patch(&author, "row3.txt", 0x42, blob)?;
seal_patches_onto(&sender, TARGET_REF, &[patch], &signer)?;
let receiver = repo("sender-stage3-row3-receiver")?;
let have_list_bytes = build_have_list(&receiver, TARGET_REF)?;
let unadopted = maintainer_signer(0x43)?; let result = build_sync_artifact(&sender, TARGET_REF, &have_list_bytes, &unadopted);
assert!(result.is_err(), "an unadopted signer must refuse to build");
cleanup(&sender);
cleanup(&receiver);
Ok(())
}
#[test]
fn building_an_artifact_adopts_no_key_and_changes_no_trust() -> Result<()> {
let sender = repo("sender-stage3-row5")?;
let signer = maintainer_signer(0x50)?;
adopt(&sender, &signer)?;
let author = author_signer(0x51)?;
let mut objects = FileObjectStore::new(sender.clone());
let blob = write_blob(&mut objects, b"row5\n")?;
let patch = create_file_patch(&author, "row5.txt", 0x52, blob)?;
seal_patches_onto(&sender, TARGET_REF, &[patch], &signer)?;
let receiver = repo("sender-stage3-row5-receiver")?;
let have_list_bytes = build_have_list(&receiver, TARGET_REF)?;
let before = crate::trust::load_maintainer_trust_policy(&sender)?;
let outcome = build_sync_artifact(&sender, TARGET_REF, &have_list_bytes, &signer)?;
assert!(matches!(outcome, SyncArtifactOutcome::Artifact { .. }));
let after = crate::trust::load_maintainer_trust_policy(&sender)?;
assert_eq!(before, after, "the adopted-maintainer set must not change");
cleanup(&sender);
cleanup(&receiver);
Ok(())
}
#[test]
fn round_trip_build_accept_seal_lands_the_delta() -> Result<()> {
let sender = repo("sender-stage3-row6-sender")?;
let sender_signer = maintainer_signer(0x60)?;
adopt(&sender, &sender_signer)?;
let author = author_signer(0x61)?;
let mut sender_objects = FileObjectStore::new(sender.clone());
let blob = write_blob(&mut sender_objects, b"row6\n")?;
let patch = create_file_patch(&author, "row6.txt", 0x62, blob)?;
seal_patches_onto(
&sender,
TARGET_REF,
std::slice::from_ref(&patch),
&sender_signer,
)?;
let receiver = repo("sender-stage3-row6-receiver")?;
let receiver_signer = maintainer_signer(0x63)?;
adopt(&receiver, &receiver_signer)?;
let have_list_bytes = build_have_list(&receiver, TARGET_REF)?;
let outcome = build_sync_artifact(&sender, TARGET_REF, &have_list_bytes, &sender_signer)?;
let bytes = match outcome {
SyncArtifactOutcome::Artifact { bytes, .. } => bytes,
other => panic!("expected Artifact, got {other:?}"),
};
let accept_report =
accept_exchange_artifact(&receiver, &bytes, &AcceptOptions::default_limits())?;
assert_eq!(accept_report.claim_count, 1);
let (claim_id, _) = accept_report.claim_signature_outcomes[0];
let seal_outcome = seal_from_accepted_claim(&receiver, TARGET_REF, claim_id, &receiver_signer)?;
assert!(matches!(
seal_outcome,
crate::SealFromAcceptedOutcome::Sealed { .. }
));
let receiver_have_list_bytes = build_have_list(&receiver, TARGET_REF)?;
let receiver_have_list = decode_have_list(
&receiver_have_list_bytes,
DEFAULT_HAVE_LIST_MAX_TOTAL_BYTES,
DEFAULT_HAVE_LIST_MAX_PATCH_COUNT,
)?;
assert_eq!(
receiver_have_list.patch_ids,
vec![patch.object_id()],
"the synced patch must be reachable from the receiver's own ref tip"
);
cleanup(&sender);
cleanup(&receiver);
Ok(())
}
#[test]
fn a_have_list_naming_a_different_ref_is_refused() -> Result<()> {
let sender = repo("sender-stage3-ref-mismatch")?;
let signer = maintainer_signer(0x70)?;
adopt(&sender, &signer)?;
let receiver = repo("sender-stage3-ref-mismatch-receiver")?;
let have_list_bytes = build_have_list(&receiver, "heads/other")?;
let result = build_sync_artifact(&sender, TARGET_REF, &have_list_bytes, &signer);
assert!(
result.is_err(),
"a have-list naming a different ref than requested must be refused"
);
cleanup(&sender);
cleanup(&receiver);
Ok(())
}