prikk-store 0.0.1

Prikk storage crate scaffold.
Documentation
//! Patch inverse planning tests.

use prikk_object::{
    BlobPayload, BlockKind, BlockPayload, CanonicalEncode, CreateFile, DeleteFile, EditText,
    MerkleRoot, ObjectEnvelope, ObjectType, Operation, OperationKind, PatchPayload, ReplaceBinary,
    text_span_hash,
};

use crate::{
    FileObjectStore, ObjectWriter, PatchInverseOperationKind, RefPublication, RefStore, RepoPath,
    RepositoryLayout, SnapshotEntry, SnapshotManifest, prepare_patch_inverse_plan,
};

use super::helpers::{
    dummy_signature, maintainer_signature, signed_ref_state_envelope, signed_ref_update_envelope,
    unique_temp_dir,
};

#[test]
fn inverse_plan_reverses_supported_file_operations() {
    let root = unique_temp_dir("patch-inverse-file-ops");
    let layout = RepositoryLayout::init(root.clone());
    assert!(layout.is_ok());
    if let Ok(layout) = layout {
        let result = publish_snapshot_then_patch_block(&layout);
        assert!(result.is_ok());
        let plan = prepare_patch_inverse_plan(&layout, "heads/main");
        assert!(plan.is_ok());
        if let Ok(plan) = plan {
            assert_eq!(plan.block_count, 2);
            assert_eq!(plan.patch_count, 1);
            assert_eq!(plan.original_operation_count, 3);
            assert_eq!(plan.inverse_operation_count, 3);
            let labels: Vec<&str> = plan
                .operations
                .iter()
                .map(|operation| operation.kind.as_str())
                .collect();
            assert_eq!(labels, vec!["delete-file", "create-file", "replace-binary"]);
            let paths: Vec<&str> = plan
                .operations
                .iter()
                .map(|operation| operation.path.as_str())
                .collect();
            assert_eq!(paths, vec!["extra.txt", "old.txt", "README.md"]);
            let seqs: Vec<u32> = plan
                .operations
                .iter()
                .map(|operation| operation.op_seq)
                .collect();
            assert_eq!(seqs, vec![1, 2, 3]);
            assert_eq!(plan.inverse_payload.operations.len(), 3);
        }
    }
    let _ = std::fs::remove_dir_all(root);
}

#[test]
fn inverse_plan_builds_full_file_text_inverse() {
    let root = unique_temp_dir("patch-inverse-edit-text");
    let layout = RepositoryLayout::init(root.clone());
    assert!(layout.is_ok());
    if let Ok(layout) = layout {
        let result = publish_text_edit_block(&layout);
        assert!(result.is_ok());
        let plan = prepare_patch_inverse_plan(&layout, "heads/main");
        assert!(plan.is_ok());
        if let Ok(plan) = plan {
            assert_eq!(plan.original_operation_count, 1);
            assert_eq!(plan.inverse_operation_count, 1);
            let mut operations = plan.inverse_payload.operations.iter();
            if let Some(operation) = operations.next() {
                assert_eq!(operation.op_seq, 1);
                match &operation.kind {
                    OperationKind::EditText(edit) => {
                        assert_eq!(edit.path, "note.txt");
                        assert_eq!(edit.anchor_id, "full-file");
                        assert_eq!(edit.old_span_hash, text_span_hash(b"changed text"));
                        assert_eq!(edit.replacement, "hello text\n");
                    }
                    _ => panic!("expected inverse EditText"),
                }
            } else {
                panic!("expected one inverse operation");
            }
            assert!(operations.next().is_none());
            let kinds: Vec<PatchInverseOperationKind> = plan
                .operations
                .iter()
                .map(|operation| operation.kind)
                .collect();
            assert_eq!(kinds, vec![PatchInverseOperationKind::EditText]);
        }
    }
    let _ = std::fs::remove_dir_all(root);
}

fn publish_snapshot_then_patch_block(layout: &RepositoryLayout) -> prikk_error::Result<()> {
    let mut object_store = FileObjectStore::new(layout.clone());
    let readme_v1 = write_blob(&mut object_store, b"hello\n")?;
    let old_blob = write_blob(&mut object_store, b"old\n")?;
    let readme_v2 = write_blob(&mut object_store, b"changed\n")?;
    let extra_blob = write_blob(&mut object_store, b"extra\n")?;

    let snapshot_manifest = SnapshotManifest {
        files: vec![
            SnapshotEntry {
                path: RepoPath::parse("README.md")?,
                bytes: b"hello\n".to_vec(),
            },
            SnapshotEntry {
                path: RepoPath::parse("old.txt")?,
                bytes: b"old\n".to_vec(),
            },
        ],
    };
    let snapshot_blob = BlobPayload {
        bytes: snapshot_manifest.encode()?,
    };
    let snapshot_bytes = snapshot_blob.to_canonical_bytes()?;
    let mut snapshot_envelope = ObjectEnvelope::unsigned(ObjectType::Blob, 1, snapshot_bytes);
    snapshot_envelope.add_signature(maintainer_signature())?;
    let snapshot_blob_id = object_store.write_object(&snapshot_envelope)?;

    let root_block = signed_block(
        BlockKind::Root,
        Vec::new(),
        Vec::new(),
        Some(snapshot_blob_id),
    );
    let root_block_id = object_store.write_object(&root_block)?;

    let patch_payload = PatchPayload {
        operations: vec![
            Operation {
                op_seq: 1,
                op_id: None,
                preconditions: Vec::new(),
                kind: OperationKind::ReplaceBinary(ReplaceBinary {
                    path: "README.md".to_string(),
                    old_blob_id: readme_v1,
                    new_blob_id: readme_v2,
                }),
            },
            Operation {
                op_seq: 2,
                op_id: None,
                preconditions: Vec::new(),
                kind: OperationKind::DeleteFile(DeleteFile {
                    path: "old.txt".to_string(),
                    old_blob_id: old_blob,
                }),
            },
            Operation {
                op_seq: 3,
                op_id: None,
                preconditions: Vec::new(),
                kind: OperationKind::CreateFile(CreateFile {
                    path: "extra.txt".to_string(),
                    blob_id: extra_blob,
                    mode: 0o100644,
                }),
            },
        ],
        parent_patch_ids: Vec::new(),
        intent: None,
        preconditions: Vec::new(),
    };
    let patch_id = write_patch(&mut object_store, patch_payload)?;
    publish_root_then_patch_ref(layout, root_block_id, patch_id)
}

fn publish_text_edit_block(layout: &RepositoryLayout) -> prikk_error::Result<()> {
    let mut object_store = FileObjectStore::new(layout.clone());
    let original = b"hello text\n";
    let snapshot_manifest = SnapshotManifest {
        files: vec![SnapshotEntry {
            path: RepoPath::parse("note.txt")?,
            bytes: original.to_vec(),
        }],
    };
    let snapshot_blob = BlobPayload {
        bytes: snapshot_manifest.encode()?,
    };
    let snapshot_bytes = snapshot_blob.to_canonical_bytes()?;
    let mut snapshot_envelope = ObjectEnvelope::unsigned(ObjectType::Blob, 1, snapshot_bytes);
    snapshot_envelope.add_signature(maintainer_signature())?;
    let snapshot_blob_id = object_store.write_object(&snapshot_envelope)?;

    let root_block = signed_block(
        BlockKind::Root,
        Vec::new(),
        Vec::new(),
        Some(snapshot_blob_id),
    );
    let root_block_id = object_store.write_object(&root_block)?;

    let patch_payload = PatchPayload {
        operations: vec![Operation {
            op_seq: 1,
            op_id: None,
            preconditions: Vec::new(),
            kind: OperationKind::EditText(EditText {
                path: "note.txt".to_string(),
                anchor_id: "full-file".to_string(),
                old_span_hash: text_span_hash(original),
                replacement: "changed text".to_string(),
            }),
        }],
        parent_patch_ids: Vec::new(),
        intent: None,
        preconditions: Vec::new(),
    };
    let patch_id = write_patch(&mut object_store, patch_payload)?;
    publish_root_then_patch_ref(layout, root_block_id, patch_id)
}

fn write_patch(
    object_store: &mut FileObjectStore,
    payload: PatchPayload,
) -> prikk_error::Result<prikk_object::ObjectId> {
    let mut patch = ObjectEnvelope::unsigned(ObjectType::Patch, 1, payload.to_canonical_bytes()?);
    patch.add_signature(dummy_signature())?;
    object_store.write_object(&patch)
}

fn publish_root_then_patch_ref(
    layout: &RepositoryLayout,
    root_block_id: prikk_object::ObjectId,
    patch_id: prikk_object::ObjectId,
) -> prikk_error::Result<()> {
    let mut object_store = FileObjectStore::new(layout.clone());
    let patch_block = signed_block(BlockKind::Normal, vec![root_block_id], vec![patch_id], None);
    let patch_block_id = object_store.write_object(&patch_block)?;
    let ref_store = RefStore::new(layout.clone());
    let root_ref_state = signed_ref_state_envelope("heads/main", None, root_block_id, 1);
    let root_ref_state_id = root_ref_state.object_id();
    let root_ref_update =
        signed_ref_update_envelope("heads/main", None, root_ref_state_id, root_block_id, 1);
    ref_store.publish(&RefPublication {
        ref_name: "heads/main".to_string(),
        expected_previous_ref_state_id: None,
        ref_state: root_ref_state,
        ref_update: root_ref_update,
    })?;

    let patch_ref_state =
        signed_ref_state_envelope("heads/main", Some(root_ref_state_id), patch_block_id, 2);
    let patch_ref_state_id = patch_ref_state.object_id();
    let patch_ref_update = signed_ref_update_envelope(
        "heads/main",
        Some(root_ref_state_id),
        patch_ref_state_id,
        patch_block_id,
        2,
    );
    ref_store
        .publish(&RefPublication {
            ref_name: "heads/main".to_string(),
            expected_previous_ref_state_id: Some(root_ref_state_id),
            ref_state: patch_ref_state,
            ref_update: patch_ref_update,
        })
        .map(|_object_id| ())
}

fn write_blob(
    store: &mut FileObjectStore,
    bytes: &[u8],
) -> prikk_error::Result<prikk_object::ObjectId> {
    let payload = BlobPayload {
        bytes: bytes.to_vec(),
    };
    let mut envelope = ObjectEnvelope::unsigned(ObjectType::Blob, 1, payload.to_canonical_bytes()?);
    envelope.add_signature(maintainer_signature())?;
    store.write_object(&envelope)
}

fn signed_block(
    kind: BlockKind,
    parent_block_ids: Vec<prikk_object::ObjectId>,
    patch_ids: Vec<prikk_object::ObjectId>,
    snapshot_blob_ref: Option<prikk_object::ObjectId>,
) -> ObjectEnvelope {
    let payload = BlockPayload {
        parent_block_ids,
        kind,
        patch_ids,
        state_merkle_root: MerkleRoot([0_u8; 32]),
        snapshot_blob_ref,
    };
    let payload_bytes = payload.to_canonical_bytes();
    assert!(payload_bytes.is_ok());
    let mut envelope =
        ObjectEnvelope::unsigned(ObjectType::Block, 1, payload_bytes.unwrap_or_default());
    assert!(envelope.add_signature(maintainer_signature()).is_ok());
    envelope
}