haematite 0.4.1

Content-addressed, branchable, actor-native storage engine
Documentation
//! Unit tests for the branch ref store (HBR1). Split out of `refstore.rs` via
//! `#[path]` so that file stays within the branch module's 500-line cap as
//! coverage grows.

use std::collections::HashSet;
use std::fs;

use super::{
    BranchRefError, BranchRefRecord, BranchRefStore, BranchShardRef, encode_record, ref_file_name,
};
use crate::branch::persist::fail_next_parent_dir_sync;
use crate::tree::Hash;

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

/// A freshly created branch: heads sit at the fork anchors, seq 0.
fn new_branch(name: &str, created: u64, shards: &[(usize, u8)]) -> BranchRefRecord {
    BranchRefRecord {
        name: name.to_owned(),
        created,
        seq: 0,
        timestamp: created,
        shards: shards
            .iter()
            .map(|&(shard_id, anchor)| BranchShardRef {
                shard_id,
                fork_anchor: hash(anchor),
                head: hash(anchor),
            })
            .collect(),
        parents: Vec::new(),
    }
}

fn tempdir() -> Result<tempfile::TempDir, BranchRefError> {
    tempfile::tempdir().map_err(BranchRefError::Io)
}

#[test]
fn hbr1_round_trip_survives_reopen() -> Result<(), BranchRefError> {
    let dir = tempdir()?;
    let mut record = new_branch("sessions/alpha", 1_111, &[(0, 1), (3, 2), (7, 3)]);
    record.seq = 4;
    record.timestamp = 2_222;
    record.shards[1].head = hash(9);
    record.parents = vec![hash(4), hash(5)];
    {
        let mut store = BranchRefStore::open(dir.path())?;
        store.create(record.clone())?;
    }
    let reopened = BranchRefStore::open(dir.path())?;
    assert_eq!(reopened.get("sessions/alpha"), Some(&record));
    assert_eq!(reopened.list().count(), 1);
    Ok(())
}

#[test]
fn create_duplicate_name_is_typed_error() -> Result<(), BranchRefError> {
    let dir = tempdir()?;
    let mut store = BranchRefStore::open(dir.path())?;
    store.create(new_branch("dup", 10, &[(0, 1)]))?;
    let result = store.create(new_branch("dup", 20, &[(0, 2)]));
    assert!(matches!(result, Err(BranchRefError::DuplicateBranch(name)) if name == "dup"));
    // The original record is untouched.
    assert!(matches!(store.get("dup"), Some(record) if record.created == 10));
    Ok(())
}

#[test]
fn create_detects_on_disk_duplicate_via_noclobber() -> Result<(), BranchRefError> {
    // A record installed behind the store's back (same name, so same file):
    // the in-memory map misses, the no-clobber install loses, and the decoded
    // occupant's matching name makes it a duplicate — never a silent clobber.
    let dir = tempdir()?;
    let mut store = BranchRefStore::open(dir.path())?;
    let intruder = new_branch("shadow", 30, &[(0, 3)]);
    fs::write(
        dir.path().join(ref_file_name("shadow")),
        encode_record(&intruder),
    )?;
    let result = store.create(new_branch("shadow", 40, &[(0, 4)]));
    assert!(matches!(result, Err(BranchRefError::DuplicateBranch(name)) if name == "shadow"));
    Ok(())
}

#[test]
fn create_name_hash_collision_is_typed_error() -> Result<(), BranchRefError> {
    // Two distinct names colliding on blake3()[0..16] cannot be manufactured,
    // so simulate one: plant a record for a DIFFERENT name at the file the
    // requested name hashes to.
    let dir = tempdir()?;
    let mut store = BranchRefStore::open(dir.path())?;
    let occupant = new_branch("occupant", 50, &[(0, 5)]);
    fs::write(
        dir.path().join(ref_file_name("victim")),
        encode_record(&occupant),
    )?;
    let result = store.create(new_branch("victim", 60, &[(0, 6)]));
    assert!(matches!(
        result,
        Err(BranchRefError::NameHashCollision { requested, existing })
            if requested == "victim" && existing == "occupant"
    ));
    Ok(())
}

#[test]
fn create_rejects_duplicate_shard_ids() -> Result<(), BranchRefError> {
    let dir = tempdir()?;
    let mut store = BranchRefStore::open(dir.path())?;
    let result = store.create(new_branch("twice", 70, &[(1, 1), (1, 2)]));
    assert!(matches!(
        result,
        Err(BranchRefError::DuplicateShard { name, shard_id }) if name == "twice" && shard_id == 1
    ));
    assert!(store.get("twice").is_none());
    Ok(())
}

#[test]
fn advance_bumps_seq_and_stale_seq_is_typed_error() -> Result<(), BranchRefError> {
    let dir = tempdir()?;
    let mut store = BranchRefStore::open(dir.path())?;
    store.create(new_branch("work", 100, &[(0, 1)]))?;

    let seq = store.advance("work", 100, 0, &[(0, hash(2))], vec![hash(1)], 150)?;
    assert_eq!(seq, 1);

    // A second handle still holding seq 0 must not silently clobber.
    let stale = store.advance("work", 100, 0, &[(0, hash(3))], vec![hash(2)], 160);
    assert!(matches!(
        stale,
        Err(BranchRefError::StaleSeq { name, expected: 0, found: 1 }) if name == "work"
    ));
    assert!(
        matches!(store.get("work"), Some(record) if record.seq == 1 && record.shards[0].head == hash(2))
    );
    Ok(())
}

#[test]
fn advance_after_recreate_is_generation_mismatch() -> Result<(), BranchRefError> {
    // The §16.2 ABA: create → (handle binds created=200, seq 0) → remove →
    // recreate same name (also seq 0). The stale handle's seq CAS would pass;
    // the creation identity refuses it.
    let dir = tempdir()?;
    let mut store = BranchRefStore::open(dir.path())?;
    store.create(new_branch("lease", 200, &[(0, 1)]))?;
    store.remove("lease")?;
    store.create(new_branch("lease", 300, &[(0, 2)]))?;

    let result = store.advance("lease", 200, 0, &[(0, hash(9))], vec![hash(1)], 350);
    assert!(matches!(
        result,
        Err(BranchRefError::BranchGenerationMismatch {
            name,
            expected_created: 200,
            found_created: 300,
        }) if name == "lease"
    ));
    // The new generation is untouched.
    assert!(matches!(store.get("lease"), Some(record) if record.shards[0].head == hash(2)));
    Ok(())
}

#[test]
fn advance_after_remove_is_branch_removed() -> Result<(), BranchRefError> {
    let dir = tempdir()?;
    let mut store = BranchRefStore::open(dir.path())?;
    store.create(new_branch("gone", 400, &[(0, 1)]))?;
    store.remove("gone")?;
    let result = store.advance("gone", 400, 0, &[(0, hash(2))], vec![hash(1)], 450);
    assert!(matches!(result, Err(BranchRefError::BranchRemoved(name)) if name == "gone"));
    Ok(())
}

#[test]
fn advance_preserves_fork_anchor_verbatim() -> Result<(), BranchRefError> {
    // §16.3's codified invariant: heads move, anchors never do — and shards
    // the advance does not mention keep their head too.
    let dir = tempdir()?;
    let mut store = BranchRefStore::open(dir.path())?;
    store.create(new_branch("anchored", 500, &[(0, 1), (2, 2)]))?;

    store.advance("anchored", 500, 0, &[(0, hash(7))], vec![hash(1)], 550)?;
    store.advance("anchored", 500, 1, &[(0, hash(8))], vec![hash(7)], 560)?;

    let reopened = BranchRefStore::open(dir.path())?;
    let record = reopened
        .get("anchored")
        .ok_or_else(|| BranchRefError::BranchRemoved("anchored".to_owned()))?;
    assert_eq!(record.shards[0].fork_anchor, hash(1));
    assert_eq!(record.shards[0].head, hash(8));
    assert_eq!(record.shards[1].fork_anchor, hash(2));
    assert_eq!(record.shards[1].head, hash(2));
    assert_eq!(record.parents, vec![hash(7)]);
    Ok(())
}

#[test]
fn advance_unknown_shard_is_typed_error() -> Result<(), BranchRefError> {
    let dir = tempdir()?;
    let mut store = BranchRefStore::open(dir.path())?;
    store.create(new_branch("narrow", 600, &[(0, 1)]))?;
    let result = store.advance("narrow", 600, 0, &[(5, hash(2))], vec![hash(1)], 650);
    assert!(matches!(
        result,
        Err(BranchRefError::UnknownShard { name, shard_id: 5 }) if name == "narrow"
    ));
    // Refused before any install: seq unchanged.
    assert!(matches!(store.get("narrow"), Some(record) if record.seq == 0));
    Ok(())
}

#[test]
fn open_fails_loud_on_corrupt_record() -> Result<(), BranchRefError> {
    let dir = tempdir()?;
    {
        let mut store = BranchRefStore::open(dir.path())?;
        store.create(new_branch("healthy", 700, &[(0, 1)]))?;
    }
    fs::write(dir.path().join(ref_file_name("rotten")), b"bit rot")?;
    // One rotten record fails the whole open — a skipped record would be a
    // silently dropped prune pin.
    assert!(matches!(
        BranchRefStore::open(dir.path()),
        Err(BranchRefError::Corrupt(_))
    ));
    Ok(())
}

#[test]
fn open_fails_loud_on_misfiled_record() -> Result<(), BranchRefError> {
    // A structurally valid record sitting in a file its name does not hash to
    // is corruption too, not something to silently index.
    let dir = tempdir()?;
    fs::write(
        dir.path().join(ref_file_name("expected")),
        encode_record(&new_branch("actual", 800, &[(0, 1)])),
    )?;
    assert!(matches!(
        BranchRefStore::open(dir.path()),
        Err(BranchRefError::Corrupt(_))
    ));
    Ok(())
}

#[test]
fn open_sweeps_only_pinned_prefix_temp_files() -> Result<(), BranchRefError> {
    let dir = tempdir()?;
    // The §2.1 sweep pattern is `.branch-*.tmp` — exactly the prefix/suffix
    // pinned at the persist.rs call sites (§16.3). A temp name outside that
    // pattern is not ours and must survive.
    let orphaned = dir.path().join(".branch-a1b2c3.tmp");
    let foreign = dir.path().join(".other-a1b2c3.tmp");
    fs::write(&orphaned, b"torn write leftovers")?;
    fs::write(&foreign, b"someone else's file")?;

    let store = BranchRefStore::open(dir.path())?;
    assert!(!orphaned.exists());
    assert!(foreign.exists());
    assert_eq!(store.list().count(), 0);
    Ok(())
}

#[test]
fn protected_roots_is_union_of_anchors_and_heads() -> Result<(), BranchRefError> {
    let dir = tempdir()?;
    let mut store = BranchRefStore::open(dir.path())?;
    store.create(new_branch("one", 900, &[(0, 1)]))?;
    store.create(new_branch("two", 910, &[(0, 2), (1, 3)]))?;
    // Advance "one" so its anchor and head diverge: BOTH must stay pinned —
    // the anchor is the merge ancestor for the branch's whole life.
    store.advance("one", 900, 0, &[(0, hash(4))], vec![hash(1)], 950)?;

    let expected: HashSet<Hash> = [hash(1), hash(2), hash(3), hash(4)].into_iter().collect();
    assert_eq!(store.protected_roots(), expected);
    Ok(())
}

#[test]
fn remove_returns_record_and_survives_reopen() -> Result<(), BranchRefError> {
    let dir = tempdir()?;
    {
        let mut store = BranchRefStore::open(dir.path())?;
        store.create(new_branch("keep", 1_000, &[(0, 1)]))?;
        store.create(new_branch("drop", 1_010, &[(0, 2)]))?;
        let removed = store.remove("drop")?;
        assert!(matches!(removed, Some(record) if record.name == "drop"));
        assert!(store.get("drop").is_none());
    }
    let reopened = BranchRefStore::open(dir.path())?;
    assert!(reopened.get("drop").is_none());
    assert!(reopened.get("keep").is_some());
    Ok(())
}

#[test]
fn remove_unknown_name_is_none() -> Result<(), BranchRefError> {
    let dir = tempdir()?;
    let mut store = BranchRefStore::open(dir.path())?;
    assert!(matches!(store.remove("never-existed"), Ok(None)));
    Ok(())
}

#[test]
fn unfenced_advance_adopts_replacement_into_map() -> Result<(), BranchRefError> {
    let dir = tempdir()?;
    let mut store = BranchRefStore::open(dir.path())?;
    store.create(new_branch("layers/base", 1_000, &[(0, 1)]))?;

    // Drive the REAL install path into its post-rename fsync-failure tail:
    // the rename lands, then sync_parent_dir fails (adversarial-review
    // blocker).
    fail_next_parent_dir_sync();
    let result = store.advance(
        "layers/base",
        1_000,
        0,
        &[(0, hash(7))],
        vec![hash(1)],
        2_000,
    );
    assert!(
        matches!(result, Err(BranchRefError::Io(ref error)) if error.to_string().contains("injected")),
        "the unfenced install must still surface its error"
    );

    // The map adopted the replacement: the on-disk file already holds the new
    // heads, so protected_roots keeping the OLD record would under-pin and
    // let prune reclaim nodes a cold reopen still resolves.
    let record = store
        .get("layers/base")
        .ok_or_else(|| BranchRefError::Corrupt("record missing after unfenced install".into()))?;
    assert_eq!(record.seq, 1);
    assert_eq!(record.shards[0].head, hash(7));
    assert!(store.protected_roots().contains(&hash(7)));

    // Disk agrees: a cold reopen sees the advanced record.
    drop(store);
    let reopened = BranchRefStore::open(dir.path())?;
    let record = reopened
        .get("layers/base")
        .ok_or_else(|| BranchRefError::Corrupt("record missing after reopen".into()))?;
    assert_eq!(record.seq, 1);
    assert_eq!(record.shards[0].head, hash(7));
    Ok(())
}

#[test]
fn unfenced_create_adopts_record_into_map() -> Result<(), BranchRefError> {
    let dir = tempdir()?;
    let mut store = BranchRefStore::open(dir.path())?;

    fail_next_parent_dir_sync();
    let result = store.create(new_branch("layers/loss", 1_000, &[(0, 3)]));
    assert!(
        matches!(result, Err(BranchRefError::Io(ref error)) if error.to_string().contains("injected")),
        "the unfenced create must still surface its error"
    );
    // Adopted: the anchor/head are pinned the moment the file holds them.
    assert!(store.protected_roots().contains(&hash(3)));

    drop(store);
    let reopened = BranchRefStore::open(dir.path())?;
    assert!(reopened.get("layers/loss").is_some());
    Ok(())
}