use anyhow::{Context, Result};
use trusty_common::memory_core::maintenance_log::{read_journal, MaintenanceDeletion};
use trusty_common::memory_core::palace::Palace;
use uuid::Uuid;
pub(crate) fn deletions_report(
name: &str,
palace: &Palace,
drawer: Option<Uuid>,
limit: usize,
json: bool,
) -> Result<String> {
let journal = read_journal(&palace.data_dir)
.with_context(|| format!("read deletion journal in {}", palace.data_dir.display()))?;
let total = journal.records.len();
let matching: Vec<&MaintenanceDeletion> = journal
.records
.iter()
.filter(|r| drawer.is_none_or(|id| r.drawer_id == id || r.survivor_id == Some(id)))
.collect();
let shown = &matching[matching.len().saturating_sub(limit)..];
if json {
return Ok(format!("{}\n", serde_json::to_string_pretty(shown)?));
}
let mut out = format!(
"palace={name} journal={} records={total} matching={} shown={}\n",
palace.data_dir.display(),
matching.len(),
shown.len()
);
if journal.malformed > 0 {
out.push_str(&format!(
" note: {} unparseable line(s) skipped\n",
journal.malformed
));
}
if shown.is_empty() {
out.push_str(" no maintenance deletions recorded\n");
}
for r in shown {
out.push_str(&format!(
"{} {} drawer={}",
r.at.to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
r.reason,
r.drawer_id
));
if let Some(survivor) = r.survivor_id {
out.push_str(&format!(" survivor={survivor}"));
}
if let Some(score) = r.score {
out.push_str(&format!(" score={score:.4}"));
}
out.push_str(&format!(" pid={}\n", r.pid));
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use trusty_common::memory_core::maintenance_log::{record, DeletionReason};
use trusty_common::memory_core::palace::PalaceId;
#[test]
fn palace_deletions_shows_the_survivor_and_score_for_a_removed_drawer() {
let dir = tempfile::tempdir().expect("tempdir");
let palace = Palace {
id: PalaceId::new("trail"),
name: "trail".into(),
description: None,
created_at: chrono::Utc::now(),
data_dir: dir.path().to_path_buf(),
};
let (removed, survivor, other) = (Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4());
let dedup = MaintenanceDeletion::new(&palace.id, removed, DeletionReason::DreamDedup)
.with_survivor(survivor, Some(0.9731));
let prune = MaintenanceDeletion::new(&palace.id, other, DeletionReason::DreamPrune);
record(Some(&palace.data_dir), &dedup);
record(Some(&palace.data_dir), &prune);
let text = deletions_report("trail", &palace, Some(removed), 50, false).expect("report");
assert!(text.contains("records=2 matching=1 shown=1"), "{text}");
assert!(
text.contains(&format!(
"dream_dedup drawer={removed} survivor={survivor} score=0.9731"
)),
"{text}"
);
assert!(!text.contains(&other.to_string()), "{text}");
let json = deletions_report("trail", &palace, None, 1, true).expect("json");
let parsed: serde_json::Value = serde_json::from_str(&json).expect("parse");
assert_eq!(parsed.as_array().map(Vec::len), Some(1));
assert_eq!(parsed[0]["reason"], "dream_prune", "newest record is kept");
}
}