prikk-store 0.24.0

Prikk storage crate scaffold.
Documentation
use super::*;

#[test]
fn confluent_report_includes_required_baseline_and_sequence_summaries() {
    let mut baseline = NodeLifecycleState::new();
    seed_binary(&mut baseline, node(1), "left.bin", blob(1), MODE_REGULAR);
    seed_binary(&mut baseline, node(2), "right.bin", blob(2), MODE_REGULAR);
    let left = [replace_binary(10, node(1), blob(1), blob(3))];
    let right = [change_perm(20, node(2), MODE_REGULAR, MODE_EXECUTABLE)];
    let evidence = TestTextResolver::empty().with_blob(blob(3), BlobKind::Binary, b"new".to_vec());

    let report = analyze_merge_evidence(
        blob(0xb0),
        Some(blob(0xa0)),
        &baseline,
        &evidence,
        EvidenceScope::SealedCandidateRequired,
        &left,
        &right,
    );

    assert_eq!(report.baseline_block_id, blob(0xb0));
    assert_eq!(report.replay_horizon, Some(blob(0xa0)));
    assert_eq!(report.outcome, MergeEvidenceOutcome::Confluent);
    assert_eq!(report.left_sequence.label, "left");
    assert_eq!(report.left_sequence.operation_count, 1);
    let left_operation = report
        .left_sequence
        .operations
        .first()
        .expect("left operation summary");
    assert_eq!(
        left_operation.operation_kind,
        MergeEvidenceOperationKind::ReplaceBinary
    );
    assert_eq!(left_operation.op_seq, 10);
    assert_eq!(
        first_item(&report.items).reason_code,
        MergeEvidenceReasonCode::ProvenConfluent
    );
}

#[test]
fn concrete_conflict_report_is_not_generic_not_confluent() {
    let mut baseline = NodeLifecycleState::new();
    seed_binary(&mut baseline, node(1), "tool", blob(1), MODE_REGULAR);
    let left = [change_perm(1, node(1), MODE_REGULAR, MODE_EXECUTABLE)];
    let right = [change_perm(2, node(1), MODE_REGULAR, 0o100600)];

    let report = analyze_merge_evidence(
        blob(0xb0),
        None,
        &baseline,
        &TestTextResolver::empty(),
        EvidenceScope::SealedCandidateRequired,
        &left,
        &right,
    );

    assert_eq!(report.outcome, MergeEvidenceOutcome::Conflict);
    assert_eq!(
        first_item(&report.items).reason_code,
        MergeEvidenceReasonCode::PairConflict
    );
    let item = first_item(&report.items);
    assert_eq!(item.operation_index, Some(0));
    assert_eq!(item.peer_operation_index, Some(0));
    assert_eq!(item.op_seq, Some(1));
    assert_eq!(item.peer_op_seq, Some(2));
}

#[test]
fn ordered_dependency_report_has_dedicated_outcome() {
    let mut baseline = NodeLifecycleState::new();
    seed_binary(&mut baseline, node(1), "same.bin", blob(1), MODE_REGULAR);
    let left = delete_file(
        1,
        "same.bin",
        node(1),
        NodeKind::BinaryFile,
        blob(1),
        MODE_REGULAR,
    );
    let right = create_file(2, "same.bin", node(2), blob(2), MODE_REGULAR);
    let evidence =
        TestTextResolver::empty().with_blob(blob(2), BlobKind::Binary, b"fresh".to_vec());

    let report = analyze_pair_merge_evidence(
        blob(0xb0),
        None,
        &baseline,
        &evidence,
        EvidenceScope::UnsealedCandidateOptional,
        &left,
        &right,
    );

    assert_eq!(report.outcome, MergeEvidenceOutcome::OrderedDependency);
    assert_eq!(
        first_item(&report.items).reason_code,
        MergeEvidenceReasonCode::OrderedDependency
    );
}

#[test]
fn unsupported_operation_report_does_not_expose_unknown() {
    let mut baseline = NodeLifecycleState::new();
    seed_binary(&mut baseline, node(1), "left.bin", blob(1), MODE_REGULAR);
    seed_binary(&mut baseline, node(2), "right.bin", blob(2), MODE_REGULAR);
    let left = [rename_path(1, node(1), "left.bin", "moved.bin")];
    let right = [replace_binary(2, node(2), blob(2), blob(3))];
    let evidence = TestTextResolver::empty().with_blob(blob(3), BlobKind::Binary, b"new".to_vec());

    let report = analyze_merge_evidence(
        blob(0xb0),
        None,
        &baseline,
        &evidence,
        EvidenceScope::SealedCandidateRequired,
        &left,
        &right,
    );

    assert_eq!(report.outcome, MergeEvidenceOutcome::Unsupported);
    assert_eq!(
        first_item(&report.items).reason_code,
        MergeEvidenceReasonCode::UnsupportedOperation
    );
    assert!(!format!("{report:?}").contains("Unknown"));
}

#[test]
fn same_node_text_transform_is_deferred_not_not_confluent() {
    let old = b"alpha beta gamma";
    let mut baseline = NodeLifecycleState::new();
    seed_text(&mut baseline, node(1), "doc.txt", old, MODE_REGULAR);
    let left = [edit_text(1, node(1), old, b"alpha BETA gamma")];
    let right = [edit_text(2, node(1), old, b"alpha beta GAMMA")];
    let evidence = TestTextResolver::new([(node(1), old.to_vec())]);

    let report = analyze_merge_evidence(
        blob(0xb0),
        None,
        &baseline,
        &evidence,
        EvidenceScope::SealedCandidateRequired,
        &left,
        &right,
    );

    assert_eq!(report.outcome, MergeEvidenceOutcome::Deferred);
    assert_eq!(
        first_item(&report.items).reason_code,
        MergeEvidenceReasonCode::SameNodeTextTransformDeferred
    );
}

#[test]
fn required_sealed_evidence_failure_has_highest_public_outcome() {
    let mut baseline = NodeLifecycleState::new();
    seed_binary(&mut baseline, node(1), "left.bin", blob(1), MODE_REGULAR);
    seed_binary(&mut baseline, node(2), "right.bin", blob(2), MODE_REGULAR);
    let left = [rename_path(1, node(1), "left.bin", "moved.bin")];
    let right = [replace_binary(2, node(2), blob(2), blob(3))];

    let report = analyze_merge_evidence(
        blob(0xb0),
        None,
        &baseline,
        &TestTextResolver::empty(),
        EvidenceScope::SealedCandidateRequired,
        &left,
        &right,
    );

    assert_eq!(report.outcome, MergeEvidenceOutcome::EvidenceFailure);
    assert_eq!(
        first_item(&report.items).evidence_scope,
        Some(MergeEvidenceScope::SealedCandidate)
    );
    assert_eq!(
        first_item(&report.items).reason_code,
        MergeEvidenceReasonCode::MissingRequiredEvidence
    );
}

#[test]
fn optional_unsealed_candidate_evidence_failure_is_invalid_candidate() {
    let mut baseline = NodeLifecycleState::new();
    seed_binary(&mut baseline, node(1), "bin", blob(1), MODE_REGULAR);
    let left = [replace_binary(1, node(1), blob(1), blob(2))];
    let right = [change_perm(2, node(1), MODE_REGULAR, MODE_EXECUTABLE)];

    let report = analyze_merge_evidence(
        blob(0xb0),
        None,
        &baseline,
        &TestTextResolver::empty(),
        EvidenceScope::UnsealedCandidateOptional,
        &left,
        &right,
    );

    assert_eq!(report.outcome, MergeEvidenceOutcome::InvalidCandidate);
    assert_eq!(
        first_item(&report.items).reason_code,
        MergeEvidenceReasonCode::InsufficientUnsealedCandidateEvidence
    );
}

#[test]
fn malformed_unsealed_candidate_operation_is_invalid_candidate() {
    let baseline = NodeLifecycleState::new();
    let left = [create_file(1, "/absolute", node(1), blob(1), MODE_REGULAR)];
    let right = [change_perm(2, node(2), MODE_REGULAR, MODE_EXECUTABLE)];

    let report = analyze_merge_evidence(
        blob(0xb0),
        None,
        &baseline,
        &TestTextResolver::empty(),
        EvidenceScope::UnsealedCandidateOptional,
        &left,
        &right,
    );

    assert_eq!(report.outcome, MergeEvidenceOutcome::InvalidCandidate);
    assert_eq!(
        first_item(&report.items).reason_code,
        MergeEvidenceReasonCode::InvalidUnsealedCandidate
    );
    assert_eq!(
        report
            .left_sequence
            .operations
            .first()
            .expect("left operation summary")
            .path,
        None
    );
}

fn first_item(items: &[MergeEvidenceItem]) -> &MergeEvidenceItem {
    items.first().expect("report item")
}