holger-server-lib 0.6.9

Holger server library: config, wiring, gRPC service, Rust API
//! End-to-end coverage for `Holger::plan_repo_retention` — the adapter that
//! gathers a repository's real listing and runs the pure retention planner over
//! it. The planner's own algebra is unit-tested in
//! `holger_server_lib::retention`; this test proves the wiring: a live writable
//! `rust` repo → `list()` → `ArtifactRecord`s → a `keep_last_n` plan whose
//! reclaim set matches the versions actually stored.

use holger_server_lib::retention::{RetentionPolicy, RetentionReason};
use holger_server_lib::{read_ron_config, wire_holger};
use traits::ArtifactId;

/// Round-trip through a temp file so the config takes the exact `read_ron_config`
/// path the server uses.
fn from_ron(ron: &str) -> holger_server_lib::Holger {
    use std::io::Write;
    let mut f = tempfile::NamedTempFile::new().unwrap();
    f.write_all(ron.as_bytes()).unwrap();
    read_ron_config(f.path()).expect("RON must parse")
}

#[test]
fn plan_repo_retention_keeps_last_n_over_a_live_listing() {
    let dir = tempfile::tempdir().unwrap();
    let d = dir.path().display();
    let ron = format!(
        r#"(
    exposed_endpoints: [ ( ron_name: "dev", ron_url: "127.0.0.1:0", ron_http_url: None ) ],
    storage_endpoints: [ ( ron_name: "inert", ron_storage_type: "znippy", ron_path: "{d}/inert/" ) ],
    repositories: [
        ( ron_name: "rust-dev", ron_repo_type: "rust", ron_data_dir: Some("{d}/rust-dev"), ron_base_url: Some("http://x"), ron_upstreams: [], ron_in: None, ron_out: Some(( ron_storage_endpoint: "inert", ron_exposed_endpoint: "dev" )) ),
    ],
)
"#
    );

    let mut holger = from_ron(&ron);
    holger.instantiate_backends().expect("backend builds");
    wire_holger(&mut holger).expect("wires");

    // Store five versions of `serde` + two of `tokio`.
    for v in ["1.0.0", "1.1.0", "1.2.0", "1.10.0", "2.0.0"] {
        let id = ArtifactId { namespace: Some("crates".into()), name: "serde".into(), version: v.into() };
        holger.put("rust-dev", &id, format!("serde-{v}").as_bytes()).unwrap();
    }
    for v in ["0.1.0", "0.2.0"] {
        let id = ArtifactId { namespace: Some("crates".into()), name: "tokio".into(), version: v.into() };
        holger.put("rust-dev", &id, format!("tokio-{v}").as_bytes()).unwrap();
    }

    // keep_last 2 → per name: serde keeps 2 newest (2.0.0, 1.10.0), reclaims 3;
    // tokio keeps 2 (both), reclaims 0. Two names total.
    let policy = RetentionPolicy::new().keep_last(2);
    let plan = holger
        .plan_repo_retention("rust-dev", &policy, 1_700_000_000)
        .expect("plan builds");

    assert_eq!(plan.name_count, 2, "serde + tokio");
    assert_eq!(plan.delete_count, 3, "3 old serde versions reclaimed");
    assert_eq!(plan.kept_count, 4, "2 serde + 2 tokio kept");

    // The reclaimed serde versions are the three oldest — and numeric ordering put
    // 1.10.0 ABOVE 1.2.0 (proving version compare isn't lexical).
    let mut reclaimed: Vec<String> = plan
        .to_delete()
        .filter(|r| r.id.name == "serde")
        .map(|r| r.id.version.clone())
        .collect();
    reclaimed.sort();
    assert_eq!(reclaimed, vec!["1.0.0", "1.1.0", "1.2.0"]);

    // Every reclaimed record is annotated superseded-by-newer.
    assert!(plan
        .decisions
        .iter()
        .filter(|dcn| dcn.delete)
        .all(|dcn| dcn.reasons.contains(&RetentionReason::SupersededByNewer)));
}

/// End-to-end property pin (property-policy §): a blob tagged with the
/// `protect_property` custom property SURVIVES a `keep_last_n=1` sweep that
/// reclaims its untagged siblings — proving the adapter resolves the `PropertyStore`
/// and sets `protected_by_property`, which the planner then honours.
#[test]
fn plan_repo_retention_property_pin_survives_sweep() {
    let dir = tempfile::tempdir().unwrap();
    let d = dir.path().display();
    let ron = format!(
        r#"(
    exposed_endpoints: [ ( ron_name: "dev", ron_url: "127.0.0.1:0", ron_http_url: None ) ],
    storage_endpoints: [ ( ron_name: "inert", ron_storage_type: "znippy", ron_path: "{d}/inert/" ) ],
    repositories: [
        ( ron_name: "rust-dev", ron_repo_type: "rust", ron_data_dir: Some("{d}/rust-dev"), ron_base_url: Some("http://x"), ron_upstreams: [], ron_in: None, ron_out: Some(( ron_storage_endpoint: "inert", ron_exposed_endpoint: "dev" )) ),
    ],
    audit: ( dir: "{d}/data/audit" ),
)
"#
    );
    let mut holger = from_ron(&ron);
    holger.instantiate_backends().unwrap();
    wire_holger(&mut holger).unwrap();

    // Three versions of serde; keep_last_n=1 would normally reclaim 1.0.0 + 2.0.0.
    for v in ["1.0.0", "2.0.0", "3.0.0"] {
        let id = ArtifactId { namespace: Some("crates".into()), name: "serde".into(), version: v.into() };
        holger.put("rust-dev", &id, format!("serde-{v}").as_bytes()).unwrap();
    }
    // Pin the OLD 1.0.0 with `keep=true` (the property store shares the config's
    // data root, so the adapter sees it).
    let pinned = ArtifactId { namespace: Some("crates".into()), name: "serde".into(), version: "1.0.0".into() };
    holger.set_artifact_property("rust-dev", &pinned, "keep", vec!["true".into()]).unwrap();

    let policy = RetentionPolicy::new()
        .keep_last(1)
        .protect_latest(false)
        .protect_property_kv("keep", "true");
    let plan = holger.plan_repo_retention("rust-dev", &policy, 1_700_000_000).expect("plan builds");

    // The pinned 1.0.0 is KEPT (protected-by-property); the untagged 2.0.0 is reclaimed.
    let v1 = plan.decisions.iter().find(|d| d.record.id.version == "1.0.0").unwrap();
    assert!(!v1.delete, "the keep=true-pinned 1.0.0 must survive the sweep");
    assert!(v1.reasons.contains(&RetentionReason::ProtectedByProperty), "kept for the pin");
    let v2 = plan.decisions.iter().find(|d| d.record.id.version == "2.0.0").unwrap();
    assert!(v2.delete, "the untagged 2.0.0 IS reclaimed (control)");
}

#[test]
fn plan_repo_retention_noop_policy_keeps_all() {
    let dir = tempfile::tempdir().unwrap();
    let d = dir.path().display();
    let ron = format!(
        r#"(
    exposed_endpoints: [ ( ron_name: "dev", ron_url: "127.0.0.1:0", ron_http_url: None ) ],
    storage_endpoints: [ ( ron_name: "inert", ron_storage_type: "znippy", ron_path: "{d}/inert/" ) ],
    repositories: [
        ( ron_name: "rust-dev", ron_repo_type: "rust", ron_data_dir: Some("{d}/rust-dev"), ron_base_url: Some("http://x"), ron_upstreams: [], ron_in: None, ron_out: Some(( ron_storage_endpoint: "inert", ron_exposed_endpoint: "dev" )) ),
    ],
)
"#
    );
    let mut holger = from_ron(&ron);
    holger.instantiate_backends().unwrap();
    wire_holger(&mut holger).unwrap();
    for v in ["1.0.0", "2.0.0", "3.0.0"] {
        let id = ArtifactId { namespace: Some("crates".into()), name: "serde".into(), version: v.into() };
        holger.put("rust-dev", &id, b"x").unwrap();
    }
    let plan = holger
        .plan_repo_retention("rust-dev", &RetentionPolicy::new(), 1_700_000_000)
        .unwrap();
    assert_eq!(plan.delete_count, 0);
    assert_eq!(plan.kept_count, 3);
}