use crate::cli::CliOutput;
use crate::cli::helpers::id_short;
use crate::models::field_names;
use crate::{db, validate};
use anyhow::Result;
use clap::{Args, Subcommand};
use std::path::Path;
#[derive(Args)]
pub struct ArchiveArgs {
#[command(subcommand)]
pub action: ArchiveAction,
}
#[derive(Subcommand)]
pub enum ArchiveAction {
List {
#[arg(long, short)]
namespace: Option<String>,
#[arg(long, default_value_t = 50)]
limit: usize,
#[arg(long, default_value_t = 0)]
offset: usize,
},
Restore { id: String },
Purge {
#[arg(long)]
older_than_days: Option<i64>,
},
Stats,
}
pub fn run(
db_path: &Path,
args: ArchiveArgs,
json_out: bool,
out: &mut CliOutput<'_>,
) -> Result<()> {
let conn = db::open(db_path)?;
match args.action {
ArchiveAction::List {
namespace,
limit,
offset,
} => {
let items = db::list_archived(&conn, namespace.as_deref(), limit, offset)?;
if json_out {
writeln!(
out.stdout,
"{}",
serde_json::json!({"archived": items, "count": items.len()})
)?;
} else if items.is_empty() {
writeln!(out.stdout, "no archived memories")?;
} else {
for item in &items {
writeln!(
out.stdout,
"[{}] {} (archived: {})",
id_short(item["id"].as_str().unwrap_or("")),
item["title"].as_str().unwrap_or(""),
item[field_names::ARCHIVED_AT].as_str().unwrap_or("")
)?;
}
writeln!(out.stdout, "{} archived memories", items.len())?;
}
}
ArchiveAction::Restore { id } => {
validate::validate_id(&id)?;
let restored = db::restore_archived(&conn, &id)?;
if json_out {
writeln!(
out.stdout,
"{}",
serde_json::json!({"restored": restored, "id": id})
)?;
} else if restored {
writeln!(out.stdout, "restored: {}", id_short(&id))?;
} else {
writeln!(out.stderr, "not found in archive: {id}")?;
std::process::exit(1);
}
}
ArchiveAction::Purge { older_than_days } => {
let caller = crate::identity::resolve_agent_id(None, None)
.unwrap_or_else(|_| format!("anonymous:pid-{}", std::process::id()));
crate::governance::audit::record_decision(
&caller,
"allow",
crate::governance::action_labels::ARCHIVE_PURGE,
"",
serde_json::json!({ (field_names::OLDER_THAN_DAYS): older_than_days }),
);
let purged = db::purge_archive(&conn, older_than_days)?;
if json_out {
writeln!(out.stdout, "{}", serde_json::json!({"purged": purged}))?;
} else {
writeln!(out.stdout, "purged {purged} archived memories")?;
}
}
ArchiveAction::Stats => {
let stats = db::archive_stats(&conn)?;
if json_out {
writeln!(out.stdout, "{stats}")?;
} else {
writeln!(out.stdout, "archived: {} total", stats["archived_total"])?;
if let Some(by_ns) = stats[field_names::BY_NAMESPACE].as_array() {
for ns in by_ns {
writeln!(
out.stdout,
" {}: {}",
ns["namespace"].as_str().unwrap_or(""),
ns["count"]
)?;
}
}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::test_utils::{TestEnv, seed_memory};
#[test]
fn test_archive_list_empty() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let args = ArchiveArgs {
action: ArchiveAction::List {
namespace: None,
limit: 50,
offset: 0,
},
};
{
let mut out = env.output();
run(&db, args, false, &mut out).unwrap();
}
assert!(env.stdout_str().contains("no archived memories"));
}
#[test]
fn test_archive_list_empty_json() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let args = ArchiveArgs {
action: ArchiveAction::List {
namespace: None,
limit: 50,
offset: 0,
},
};
{
let mut out = env.output();
run(&db, args, true, &mut out).unwrap();
}
let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
assert_eq!(v["count"].as_u64().unwrap(), 0);
assert!(v["archived"].is_array());
}
#[test]
fn test_archive_list_with_namespace_filter() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let args = ArchiveArgs {
action: ArchiveAction::List {
namespace: Some("nope".to_string()),
limit: 50,
offset: 0,
},
};
{
let mut out = env.output();
run(&db, args, false, &mut out).unwrap();
}
assert!(env.stdout_str().contains("no archived memories"));
}
#[test]
fn test_archive_restore_nonexistent_exits_via_stderr() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let id = seed_memory(&db, "ns", "t", "c");
let conn = db::open(&db).unwrap();
let _ = db::archive_memory(&conn, &id, None);
drop(conn);
let args = ArchiveArgs {
action: ArchiveAction::Restore { id: id.clone() },
};
{
let mut out = env.output();
run(&db, args, false, &mut out).unwrap();
}
assert!(env.stdout_str().contains("restored:"));
}
#[test]
fn test_archive_restore_json() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let id = seed_memory(&db, "ns", "t", "c");
let conn = db::open(&db).unwrap();
let _ = db::archive_memory(&conn, &id, None);
drop(conn);
let args = ArchiveArgs {
action: ArchiveAction::Restore { id: id.clone() },
};
{
let mut out = env.output();
run(&db, args, true, &mut out).unwrap();
}
let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
assert_eq!(v["restored"].as_bool().unwrap(), true);
}
#[test]
fn test_archive_purge_no_filter() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let args = ArchiveArgs {
action: ArchiveAction::Purge {
older_than_days: None,
},
};
{
let mut out = env.output();
run(&db, args, false, &mut out).unwrap();
}
assert!(env.stdout_str().contains("purged 0"));
}
#[test]
fn test_archive_purge_older_than_filter() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let args = ArchiveArgs {
action: ArchiveAction::Purge {
older_than_days: Some(30),
},
};
{
let mut out = env.output();
run(&db, args, true, &mut out).unwrap();
}
let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
assert_eq!(v["purged"].as_u64().unwrap(), 0);
}
#[test]
fn test_archive_stats() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let args = ArchiveArgs {
action: ArchiveAction::Stats,
};
{
let mut out = env.output();
run(&db, args, false, &mut out).unwrap();
}
assert!(env.stdout_str().contains("archived:"));
}
#[test]
fn test_archive_stats_json() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let args = ArchiveArgs {
action: ArchiveAction::Stats,
};
{
let mut out = env.output();
run(&db, args, true, &mut out).unwrap();
}
let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
assert!(v["archived_total"].is_number());
}
fn seed_and_archive(db: &std::path::Path, ns: &str, n: usize) -> Vec<String> {
let mut ids = Vec::with_capacity(n);
let conn = db::open(db).unwrap();
for i in 0..n {
let id = seed_memory(db, ns, &format!("title-{i}"), &format!("body-{i}"));
db::archive_memory(&conn, &id, None).unwrap();
ids.push(id);
}
ids
}
#[test]
fn test_archive_list_text_with_items() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
seed_and_archive(&db, "ns-arch", 2);
let args = ArchiveArgs {
action: ArchiveAction::List {
namespace: Some("ns-arch".to_string()),
limit: 50,
offset: 0,
},
};
{
let mut out = env.output();
run(&db, args, false, &mut out).unwrap();
}
let s = env.stdout_str();
assert!(s.contains("archived:"));
assert!(s.contains("title-0") || s.contains("title-1"));
assert!(s.contains("2 archived memories"));
}
#[test]
fn test_archive_list_json_with_items() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
seed_and_archive(&db, "ns-arch-j", 3);
let args = ArchiveArgs {
action: ArchiveAction::List {
namespace: Some("ns-arch-j".to_string()),
limit: 50,
offset: 0,
},
};
{
let mut out = env.output();
run(&db, args, true, &mut out).unwrap();
}
let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
assert_eq!(v["count"].as_u64().unwrap(), 3);
}
#[test]
fn test_archive_stats_text_with_namespace_breakdown() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
seed_and_archive(&db, "ns-stats-a", 1);
seed_and_archive(&db, "ns-stats-b", 2);
let args = ArchiveArgs {
action: ArchiveAction::Stats,
};
{
let mut out = env.output();
run(&db, args, false, &mut out).unwrap();
}
let s = env.stdout_str();
assert!(s.contains("archived:"));
assert!(
s.contains("ns-stats-a") || s.contains("ns-stats-b"),
"stats text missing namespace breakdown, got: {s}"
);
}
#[test]
fn test_archive_purge_clears_with_filter() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
seed_and_archive(&db, "ns-purge", 2);
let args = ArchiveArgs {
action: ArchiveAction::Purge {
older_than_days: Some(0),
},
};
{
let mut out = env.output();
run(&db, args, false, &mut out).unwrap();
}
let s = env.stdout_str();
assert!(s.contains("purged"));
}
}