haematite-cli 0.21.1

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
//! STOR-003 H1b: a read holds what it captured, and only a head is retried.

use std::error::Error;

use haematite::db::{DatabaseConfig, ReadHoldRefused};
use haematite::store::NodeCacheBudget;
use haematite::tree::Hash;

use super::super::Storage;
use super::{AfterRefusal, after_refusal};
use crate::serve::protocol::Selection;

type TestResult = Result<(), Box<dyn Error>>;

fn not_retained() -> ReadHoldRefused {
    ReadHoldRefused::NotRetained {
        shard: 0,
        root: Hash::from_bytes([1; 32]),
    }
}

fn missing() -> ReadHoldRefused {
    ReadHoldRefused::Missing {
        shard: 0,
        root: Hash::from_bytes([1; 32]),
    }
}

/// Fails while a named version can be retried, a head retried twice, or a
/// released version answered as an internal error: a caller that named a
/// version is answered `version_released`, never from a newer one.
#[test]
fn only_a_head_is_captured_again_and_only_once() {
    assert_eq!(
        after_refusal(true, false, &not_retained()),
        AfterRefusal::CaptureAgain
    );
    assert_eq!(
        after_refusal(true, true, &not_retained()),
        AfterRefusal::Released
    );
    assert_eq!(
        after_refusal(false, false, &not_retained()),
        AfterRefusal::Released
    );
    // RX2 (6B): a root not on its shard is missing, never released.
    assert_eq!(
        after_refusal(false, false, &missing()),
        AfterRefusal::Missing
    );
    assert_eq!(
        after_refusal(true, false, &missing()),
        AfterRefusal::Missing
    );
    let closed = ReadHoldRefused::DatabaseClosed;
    assert_eq!(after_refusal(true, false, &closed), AfterRefusal::Refused);
}

/// Fails while a capture's hold outlives it or is never taken: one hold lives
/// while the capture does, none once it drops, and none after a capture that
/// failed.
#[test]
fn a_capture_is_held_while_it_lives_and_let_go_after() -> TestResult {
    let directory = tempfile::tempdir()?;
    let storage = Storage::create(
        DatabaseConfig {
            data_dir: directory.path().join("store"),
            shard_count: 2,
            distributed: None,
            executor_threads: Some(1),
            node_cache_budget: Some(NodeCacheBudget::Unlimited),
            retention: haematite::Retention::reclaim(0),
        },
        NodeCacheBudget::Unlimited,
        fixture_long_wait_interval(),
    )?;
    let catalogue = &storage.visible;
    let selection = Selection {
        branch: "main".to_owned(),
        version: None,
    };
    let held = catalogue.capture_held(&selection)?;
    assert_eq!(catalogue.holder.status().live, 1);
    drop(held);
    assert_eq!(catalogue.holder.status().live, 0);
    let nowhere = Selection {
        branch: "nowhere".to_owned(),
        version: None,
    };
    assert!(catalogue.capture_held(&nowhere).is_err());
    assert_eq!(catalogue.holder.status().live, 0);
    Ok(())
}

const fn fixture_long_wait_interval() -> std::time::Duration {
    std::time::Duration::from_secs(1)
}