haematite 0.6.2

Content-addressed, branchable, actor-native storage engine
Documentation
use std::fs;

use super::{
    BranchRefError, BranchRefRecord, BranchRefStore, BranchShardRef, decode_record, ref_file_name,
};
use crate::branch::persist::{push_bytes, push_u64};
use crate::branch::policy::BranchKind;
use crate::tree::Hash;

fn hash(byte: u8) -> Hash {
    Hash::from_bytes([byte; 32])
}

fn record(name: &str, kind: BranchKind, namespace_lineage: Option<String>) -> BranchRefRecord {
    BranchRefRecord {
        name: name.to_owned(),
        created: 10,
        kind,
        namespace_lineage,
        seq: 0,
        timestamp: 10,
        shards: vec![BranchShardRef {
            shard_id: 0,
            fork_anchor: hash(1),
            head: hash(1),
        }],
        parents: Vec::new(),
    }
}

fn hbk1_namespace_with_lineage_bytes() -> Vec<u8> {
    // Hand-built corpus vector: HBR1 base record, then HBK1 kind=1
    // (Namespace), lineage_tag=1, and the length-prefixed lineage "parent".
    let mut bytes = b"HBR1".to_vec();
    push_bytes(&mut bytes, b"invalid-namespace");
    push_u64(&mut bytes, 10); // created
    push_u64(&mut bytes, 0); // seq
    push_u64(&mut bytes, 10); // timestamp
    push_u64(&mut bytes, 1); // shard_count
    push_u64(&mut bytes, 0); // shard_id
    bytes.extend_from_slice(hash(1).as_bytes());
    bytes.extend_from_slice(hash(1).as_bytes());
    push_u64(&mut bytes, 0); // parent_count
    bytes.extend_from_slice(b"HBK1");
    push_u64(&mut bytes, 1); // kind = Namespace
    push_u64(&mut bytes, 1); // lineage_tag = present
    push_bytes(&mut bytes, b"parent");
    bytes
}

#[test]
fn hbk1_decode_rejects_namespace_with_lineage() {
    let result = decode_record(&hbk1_namespace_with_lineage_bytes());
    assert!(matches!(
        result,
        Err(BranchRefError::Corrupt(reason))
            if reason == "HBK1 record has kind=Namespace with lineage_tag=1"
    ));
}

#[test]
fn branch_advance_preserves_kind_and_lineage() -> Result<(), BranchRefError> {
    let dir = tempfile::tempdir().map_err(BranchRefError::Io)?;
    let mut store = BranchRefStore::open(dir.path())?;
    store.create(record(
        "namespace-work",
        BranchKind::Work,
        Some("namespace-a".to_owned()),
    ))?;
    store.advance("namespace-work", 10, 0, &[(0, hash(2))], vec![hash(1)], 11)?;
    drop(store);

    let reopened = BranchRefStore::open(dir.path())?;
    let advanced = reopened
        .get("namespace-work")
        .ok_or_else(|| BranchRefError::BranchRemoved("namespace-work".to_owned()))?;
    assert_eq!(advanced.kind, BranchKind::Work);
    assert_eq!(advanced.namespace_lineage.as_deref(), Some("namespace-a"));
    assert_eq!(advanced.shards[0].head, hash(2));
    Ok(())
}

#[test]
fn refstore_create_refuses_namespace_with_stored_lineage() -> Result<(), BranchRefError> {
    let dir = tempfile::tempdir().map_err(BranchRefError::Io)?;
    let mut store = BranchRefStore::open(dir.path())?;
    let result = store.create(record(
        "invalid-namespace",
        BranchKind::Namespace,
        Some("parent".to_owned()),
    ));

    assert!(matches!(
        result,
        Err(BranchRefError::InvalidKindLineage { name, kind: BranchKind::Namespace })
            if name == "invalid-namespace"
    ));
    assert!(store.get("invalid-namespace").is_none());
    assert!(!dir.path().join(ref_file_name("invalid-namespace")).exists());
    assert_eq!(
        fs::read_dir(dir.path())
            .map_err(BranchRefError::Io)?
            .count(),
        0,
        "the rejected create must leave no temp or ref file"
    );
    Ok(())
}