acorn-lib 0.3.2

ACORN library
#![allow(clippy::indexing_slicing)]
use super::*;
use std::env::temp_dir;

fn database() -> Database<Table> {
    Database::from_path(Some(temp_dir().join(format!("acorn-candidate-{}.db", nanoid!()))))
}
fn candidate(rad_json: Value, keys: &[&str]) -> ResearchActivityCandidate {
    ResearchActivityCandidate::new(
        rad_json,
        keys.iter().map(|value| (*value).to_string()).collect(),
        vec![serde_json::json!({"source": "test", "observed_at": "2026-01-01T00:00:00Z"})],
    )
}
#[test]
fn creates_round_trips_and_replays_candidate() {
    let database = database();
    let input = candidate(serde_json::json!({"title": "Example"}), &["doi:10.1234/example"]);
    let created = database.create_or_enrich(input.clone()).unwrap();
    assert_eq!(created.action, CandidateAction::Created);
    let iid = created.iid.unwrap();
    assert_eq!(
        database.research_activity_by_iid(&iid).unwrap().unwrap().iid.as_deref(),
        Some(iid.as_str())
    );
    assert_eq!(database.create_or_enrich(input).unwrap().action, CandidateAction::Unchanged);
    assert_eq!(database.row_count(Table::ResearchActivities).unwrap(), 1);
}
#[test]
fn enriches_arrays_and_retains_conflicting_values() {
    let database = database();
    database
        .create_or_enrich(candidate(
            serde_json::json!({"title": "Original", "meta": {"doi": ["10.1234/example"]}}),
            &["doi:10.1234/example"],
        ))
        .unwrap();
    let result = database
        .create_or_enrich(candidate(
            serde_json::json!({"title": "Different", "meta": {"doi": ["10.9999/related"], "raid": ["10.7777/raid"]}}),
            &["doi:10.1234/example", "raid:10.7777/raid"],
        ))
        .unwrap();
    assert_eq!(result.action, CandidateAction::Conflict);
    let row = database.research_activity_by_iid(result.iid.as_deref().unwrap()).unwrap().unwrap();
    let rad: Value = serde_json::from_str(row.rad_json.as_deref().unwrap()).unwrap();
    assert_eq!(rad["title"], "Original");
    assert_eq!(rad["meta"]["doi"], serde_json::json!(["10.1234/example", "10.9999/related"]));
    assert_eq!(rad["meta"]["raid"], serde_json::json!(["10.7777/raid"]));
}
#[test]
fn identity_normalization_preserves_case_sensitive_key_values() {
    let keys = normalize_identity_keys(vec![
        "DOI:10.1234/ABC".to_string(),
        "doi:10.1234/abc".to_string(),
        "ark:12345/ABC".to_string(),
        "ark:12345/abc".to_string(),
        "rad:file:/Bucket/Project".to_string(),
    ]);
    assert_eq!(keys.len(), 4);
    assert!(keys.contains(&"doi:10.1234/abc".to_string()));
    assert!(keys.contains(&"ark:12345/ABC".to_string()));
    assert!(keys.contains(&"rad:file:/Bucket/Project".to_string()));
}
#[test]
fn rejects_ambiguous_multi_row_match_without_mutation() {
    let database = database();
    database
        .create_or_enrich(candidate(serde_json::json!({"title": "One"}), &["doi:one"]))
        .unwrap();
    database
        .create_or_enrich(candidate(serde_json::json!({"title": "Two"}), &["raid:two"]))
        .unwrap();
    let result = database
        .create_or_enrich(candidate(serde_json::json!({"title": "Ambiguous"}), &["doi:one", "raid:two"]))
        .unwrap();
    assert_eq!(
        result,
        CandidatePersistence {
            iid: None,
            action: CandidateAction::Conflict
        }
    );
    assert_eq!(database.row_count(Table::ResearchActivities).unwrap(), 2);
}
#[test]
fn serializes_bucket_provenance_with_provider_tag() {
    let value = serde_json::to_value(Provenance::Bucket {
        bucket: Some("science".to_string()),
        repository: "file:/bucket".to_string(),
        relative_path: "project/index.json".to_string(),
        observed_at: "2026-01-01T00:00:00Z".to_string(),
    })
    .unwrap();
    assert_eq!(value["provider"], "bucket");
    assert_eq!(value["bucket"], "science");
    assert_eq!(value["relative_path"], "project/index.json");
}