server/lib/tests/retention_adapter.rs
//! 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.
#[cfg(feature = "supply-chain")]
#[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);
}