server/lib/tests/retention_executor.rs
//! End-to-end coverage for the retention/GC **executor** — `Holger::execute_repo_retention`
//! (and the plan-then-execute convenience `run_repo_retention`). The planner's
//! algebra is pinned in `holger_server_lib::retention`; the adapter (list → plan)
//! in `retention_adapter.rs`. This file proves the *acting* side:
//!
//! * dry-run (the default `GcOptions`) reclaims **nothing** — the store is intact;
//! * an executed plan drops **exactly** the plan's delete-set and keeps the rest;
//! * a protected/latest generation is **never** dropped (even if a corrupted plan
//! flags it delete — defense in depth);
//! * every actual reclaim is **historized** to the audit warehouse.
use std::sync::Arc;
use holger_server_lib::audit::{AuditAction, MemoryAuditLog};
use holger_server_lib::retention::{
ArtifactRecord, GcOptions, GcOutcome, RetentionDecision, RetentionPlan, RetentionPolicy,
RetentionReason,
};
use holger_server_lib::{read_ron_config, wire_holger, Holger};
use traits::ArtifactId;
/// A live, writable file-backed `rust` repo in a temp dir, plus the temp guard
/// (dropped by the caller to clean up).
fn live_repo() -> (Holger, tempfile::TempDir) {
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" )) ),
],
)
"#
);
use std::io::Write;
let mut f = tempfile::NamedTempFile::new().unwrap();
f.write_all(ron.as_bytes()).unwrap();
let mut holger = read_ron_config(f.path()).expect("RON must parse");
holger.instantiate_backends().expect("backend builds");
wire_holger(&mut holger).expect("wires");
(holger, dir)
}
fn id(name: &str, version: &str) -> ArtifactId {
ArtifactId {
namespace: Some("crates".into()),
name: name.into(),
version: version.into(),
}
}
/// Seed five serde versions; return the Holger.
fn seed_serde(holger: &Holger) {
for v in ["1.0.0", "1.1.0", "1.2.0", "1.10.0", "2.0.0"] {
holger
.put("rust-dev", &id("serde", v), format!("serde-{v}").as_bytes())
.unwrap();
}
}
#[test]
fn dry_run_reclaims_nothing() {
let (holger, _guard) = live_repo();
seed_serde(&holger);
let policy = RetentionPolicy::new().keep_last(2); // would reclaim 3 oldest
let audit = MemoryAuditLog::new();
let report = holger
.run_repo_retention(
"rust-dev",
&policy,
1_700_000_000,
&GcOptions::dry_run(),
&audit,
None,
)
.expect("dry-run executes");
assert!(report.dry_run);
assert_eq!(report.deleted_count, 0, "dry-run deletes nothing");
assert_eq!(report.reclaimed_bytes, 0);
assert_eq!(
report.would_delete_count, 3,
"but reports it WOULD reclaim 3"
);
assert!(report
.actions
.iter()
.all(|a| matches!(a.outcome, GcOutcome::WouldDelete)
|| !a.reasons.contains(&RetentionReason::SupersededByNewer)));
// Every seeded version is still on disk.
for v in ["1.0.0", "1.1.0", "1.2.0", "1.10.0", "2.0.0"] {
assert!(
holger.fetch("rust-dev", &id("serde", v)).unwrap().is_some(),
"dry-run must leave {v} in the store"
);
}
// Nothing historized (nothing reclaimed).
assert!(audit.events().is_empty(), "dry-run historizes no delete");
}
#[test]
fn execute_drops_exactly_the_delete_set_and_keeps_the_rest() {
let (holger, _guard) = live_repo();
seed_serde(&holger);
// keep_last 2 → keep {2.0.0, 1.10.0}, reclaim {1.2.0, 1.1.0, 1.0.0}.
let policy = RetentionPolicy::new().keep_last(2);
let audit = MemoryAuditLog::new();
let plan = holger
.plan_repo_retention("rust-dev", &policy, 1_700_000_000)
.unwrap();
let report = holger
.execute_repo_retention("rust-dev", &plan, &GcOptions::execute(), &audit, None)
.expect("execute");
assert!(!report.dry_run);
assert_eq!(report.deleted_count, 3);
assert_eq!(report.failed_count, 0);
assert_eq!(report.skipped_count, 0);
assert!(report.reclaimed_bytes > 0);
// The delete-set is gone; the kept set survives.
for v in ["1.0.0", "1.1.0", "1.2.0"] {
assert!(
holger.fetch("rust-dev", &id("serde", v)).unwrap().is_none(),
"{v} must be reclaimed"
);
}
for v in ["1.10.0", "2.0.0"] {
assert!(
holger.fetch("rust-dev", &id("serde", v)).unwrap().is_some(),
"{v} (kept) must survive"
);
}
// Historized: exactly one Delete audit event per reclaimed artifact.
let evs = audit.events();
assert_eq!(evs.len(), 3, "one GC audit record per reclaim");
assert!(evs.iter().all(|e| e.action == AuditAction::Delete));
assert!(evs.iter().all(|e| e.repo == "rust-dev"));
assert!(evs.iter().all(|e| e.detail.contains("retention-gc")));
}
#[test]
fn execute_never_drops_a_protected_latest() {
let (holger, _guard) = live_repo();
// A single version: keep_last 1 with protection ON keeps it; nothing to drop.
holger
.put("rust-dev", &id("serde", "1.0.0"), b"only-one")
.unwrap();
let policy = RetentionPolicy::new().keep_last(1); // protect_latest defaults ON
let audit = MemoryAuditLog::new();
let report = holger
.run_repo_retention(
"rust-dev",
&policy,
1_700_000_000,
&GcOptions::execute(),
&audit,
None,
)
.unwrap();
assert_eq!(report.deleted_count, 0, "the lone latest is protected");
assert!(
holger
.fetch("rust-dev", &id("serde", "1.0.0"))
.unwrap()
.is_some(),
"protected latest survives execute"
);
}
/// Defense in depth: even a hand-built plan that WRONGLY marks a protected-latest
/// record for deletion must be refused by the executor — a protected generation
/// can never be dropped through this path.
#[test]
fn executor_refuses_a_delete_flag_on_a_protected_record() {
let (holger, _guard) = live_repo();
holger
.put("rust-dev", &id("serde", "1.0.0"), b"precious")
.unwrap();
// Forge a plan: delete=true but reasons say ProtectedLatest (a corrupt plan).
let rec = ArtifactRecord {
id: id("serde", "1.0.0"),
size_bytes: 8,
created_at: None,
protected_by_property: false,
};
let plan = RetentionPlan {
decisions: vec![RetentionDecision {
record: rec,
delete: true,
reasons: vec![RetentionReason::ProtectedLatest],
}],
kept_count: 0,
delete_count: 1,
kept_bytes: 0,
reclaimed_bytes: 8,
name_count: 1,
evaluated_at: 1_700_000_000,
};
let audit = MemoryAuditLog::new();
let report = holger
.execute_repo_retention("rust-dev", &plan, &GcOptions::execute(), &audit, None)
.unwrap();
assert_eq!(
report.deleted_count, 0,
"protected record must not be dropped"
);
assert_eq!(report.skipped_count, 1);
assert!(matches!(report.actions[0].outcome, GcOutcome::Skipped(_)));
assert!(
holger
.fetch("rust-dev", &id("serde", "1.0.0"))
.unwrap()
.is_some(),
"the protected artifact must survive a corrupt plan"
);
assert!(audit.events().is_empty());
}
/// The GC run is routed through the shared jera job ledger when one is attached
/// (the same durable seam promotion uses): a `retention-gc` job is recorded.
#[test]
fn execute_records_a_jera_job_when_a_ledger_is_attached() {
let (holger, _guard) = live_repo();
seed_serde(&holger);
let led_dir = tempfile::tempdir().unwrap();
let jobs = Arc::new(jera::jobs::JobStore::open(led_dir.path()).expect("open jera ledger"));
let policy = RetentionPolicy::new().keep_last(2);
let audit = MemoryAuditLog::new();
holger
.run_repo_retention(
"rust-dev",
&policy,
1_700_000_000,
&GcOptions::execute(),
&audit,
Some(&jobs),
)
.expect("execute with ledger");
let recorded = jobs
.list(&jera::jobs::JobSelector::Kind("retention-gc".into()))
.expect("list jobs");
assert_eq!(recorded.len(), 1, "one retention-gc job recorded");
assert!(recorded[0].is_terminal(), "the GC job settled");
}
/// ★ **Retention does not drop what the twin has not yet followed** (stinky SB
/// #35): with a twin enrolled on this primary's roster, every delete row WAITS
/// (skipped, `holger-twin-22`) until the twin has followed past the newest
/// write into the repository; then the same plan reclaims.
#[test]
fn a_reclaim_waits_until_the_twin_has_followed() {
use holger_server_lib::audit::{AuditEvent, AuditLog};
// Its OWN data root (an audit dir under the tempdir): the roster written here
// must not be read by the other tests of this binary.
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}/audit", enabled: true ),
)
"#
);
use std::io::Write;
let mut f = tempfile::NamedTempFile::new().unwrap();
f.write_all(ron.as_bytes()).unwrap();
let mut holger = read_ron_config(f.path()).expect("RON must parse");
holger.instantiate_backends().expect("backend builds");
wire_holger(&mut holger).expect("wires");
seed_serde(&holger);
let roster = holger.peers_root().join("holger-twins");
assert!(
roster.starts_with(dir.path()),
"the roster is this test's own: {}",
roster.display()
);
std::fs::create_dir_all(&roster).unwrap();
std::fs::write(roster.join("vm3"), "10.13.8.3:50051\n").unwrap();
let audit = MemoryAuditLog::new();
audit
.record(AuditEvent::new(
"adm",
AuditAction::Upload,
"rust-dev",
"serde 2.0.0",
"",
201,
9,
))
.unwrap();
let policy = RetentionPolicy::new().keep_last(2);
let waits = holger
.run_repo_retention(
"rust-dev",
&policy,
1_700_000_000,
&GcOptions::execute(),
&audit,
None,
)
.unwrap();
assert_eq!(
(waits.deleted_count, waits.skipped_count),
(0, 3),
"a twin that never followed: nothing is reclaimed"
);
assert!(waits.actions.iter().all(|a| matches!(&a.outcome, GcOutcome::Skipped(w) if w.contains("holger-twin-22") && w.contains("vm3"))), "{:?}", waits.actions);
for v in ["1.0.0", "1.1.0", "1.2.0"] {
assert!(
holger.fetch("rust-dev", &id("serde", v)).unwrap().is_some(),
"{v} is kept for the twin"
);
}
// The twin followed past the newest write: the same policy reclaims.
let later = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as i64
+ 60_000;
std::fs::write(
roster.join("vm3"),
format!("10.13.8.3:50051\nfollowed_to_ms {later}\n"),
)
.unwrap();
let done = holger
.run_repo_retention(
"rust-dev",
&policy,
1_700_000_000,
&GcOptions::execute(),
&audit,
None,
)
.unwrap();
assert_eq!(
(done.deleted_count, done.skipped_count),
(3, 0),
"{:?}",
done.actions
);
}
/// An audit sink that refuses every record (a full disk, a released file).
struct RefusingAudit;
impl holger_server_lib::audit::AuditLog for RefusingAudit {
fn record(
&self,
_event: holger_server_lib::audit::AuditEvent,
) -> Result<(), holger_server_lib::audit::AuditError> {
Err(holger_server_lib::audit::AuditError::Other(
"the audit file is released".into(),
))
}
}
/// ★ **A reclaim whose audit record cannot be written is counted, never swallowed** (A28 M17).
/// Before, `let _ = audit.record(...)` dropped the error: the artifact was gone and the
/// append-only trail of a security gateway said nothing, with no number to alert on.
#[test]
fn a_reclaim_whose_audit_record_is_lost_is_counted_and_published() {
let (holger, _guard) = live_repo();
seed_serde(&holger);
let policy = RetentionPolicy::new().keep_last(2);
let plan = holger
.plan_repo_retention("rust-dev", &policy, 1_700_000_000)
.unwrap();
let before = holger_server_lib::metrics::global().audit_missed();
let report = holger
.execute_repo_retention(
"rust-dev",
&plan,
&GcOptions::execute(),
&RefusingAudit,
None,
)
.expect("the reclaim itself succeeds");
assert_eq!(report.deleted_count, 3);
assert_eq!(
report.audit_missed, 3,
"every reclaim whose record was lost is in the report"
);
assert!(
holger_server_lib::metrics::global().audit_missed() >= before + 3,
"the misses are on the global counters /metrics renders"
);
let page = holger_server_lib::metrics::global().render_prometheus(&[]);
assert!(
page.contains("holger_audit_records_missed_total "),
"{page}"
);
}