use super::*;
use std::collections::BTreeMap;
use std::path::Path;
use trusty_common::memory_core::palace::{Drawer, PalaceId, RoomType};
use trusty_common::memory_core::room_identity::room_to_uuid;
use trusty_common::memory_core::store::PalaceStore;
fn seed_palace(root: &Path, id: &str, rooms: &[RoomType]) -> Palace {
let palace = Palace {
id: PalaceId::new(id),
name: id.to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: root.join(id),
};
std::fs::create_dir_all(&palace.data_dir).expect("create palace dir");
PalaceStore::save_palace(&palace).expect("save palace");
let handle =
PalaceHandle::open_with_intent(&palace, OpenIntent::Writer).expect("open for seeding");
for (i, room) in rooms.iter().enumerate() {
let drawer = Drawer::new(room_to_uuid(room), format!("legacy drawer {i}"));
handle
.kg
.upsert_drawer_sync(&drawer)
.expect("seed legacy drawer");
}
drop(handle);
palace
}
type RoomRows = Vec<(String, String, bool)>;
type DrawerRooms = BTreeMap<String, String>;
fn registry_snapshot(palace: &Palace) -> (RoomRows, DrawerRooms) {
let handle = PalaceHandle::open_with_intent(palace, OpenIntent::ReadOnlyClient)
.expect("reopen for snapshot");
let rooms = handle
.kg
.store()
.list_rooms()
.expect("list rooms")
.into_iter()
.map(|(id, r)| (id.to_string(), r.label, r.resolved))
.collect();
let drawers = handle
.drawers
.read()
.iter()
.map(|d| (d.id.to_string(), d.room_id.to_string()))
.collect();
(rooms, drawers)
}
#[test]
fn dry_run_reports_without_writing() {
let tmp = tempfile::tempdir().expect("tempdir");
let rooms = [
RoomType::General,
RoomType::Custom("status".to_string()),
RoomType::Custom("qqqqqqqqqqqqqqqqqq".to_string()),
];
let palace = seed_palace(tmp.path(), "audit", &rooms);
let before = registry_snapshot(&palace);
assert!(
before.0.is_empty(),
"no rooms registered before the dry run"
);
assert_eq!(before.1.len(), rooms.len(), "one drawer per room seeded");
let audits = audit_palaces(tmp.path(), None, false).expect("dry run");
assert_eq!(
before,
registry_snapshot(&palace),
"a --dry-run audit must leave the room registry and every drawer untouched"
);
assert_eq!(audits.len(), 1);
let audit = &audits[0];
assert!(audit.error.is_none(), "{:?}", audit.error);
assert_eq!(audit.inserted, None, "a dry run never reports a write");
assert_eq!(audit.entries.len(), rooms.len());
assert_eq!(audit.pending(), rooms.len(), "every room is still pending");
let labels: Vec<String> = audit.entries.iter().map(|e| e.label()).collect();
assert!(labels.contains(&"General".to_string()));
assert!(labels.contains(&"status".to_string()));
assert!(
labels.iter().any(|l| l.starts_with("unresolved-")),
"the un-invertible room is named, not dropped: {labels:?}"
);
}
#[test]
fn apply_writes_the_planned_rooms() {
let tmp = tempfile::tempdir().expect("tempdir");
let rooms = [RoomType::Planning, RoomType::Custom("work".to_string())];
let palace = seed_palace(tmp.path(), "applied", &rooms);
let planned = audit_palaces(tmp.path(), None, false).expect("dry run");
let planned_labels: Vec<String> = planned[0].entries.iter().map(|e| e.label()).collect();
let applied = audit_palaces(tmp.path(), None, true).expect("apply");
assert_eq!(applied[0].inserted, Some(rooms.len()));
assert_eq!(
applied[0]
.entries
.iter()
.map(|e| e.label())
.collect::<Vec<_>>(),
planned_labels,
"--apply must register exactly what --dry-run previewed"
);
let again = audit_palaces(tmp.path(), None, false).expect("second dry run");
assert_eq!(again[0].pending(), 0);
assert!(again[0].entries.iter().all(|e| !e.would_insert()));
let _ = palace;
}
#[test]
fn apply_under_a_lease_held_elsewhere_deletes_no_expired_row() {
use trusty_common::memory_core::store::kg_redb::KgStoreRedb;
let tmp = tempfile::tempdir().expect("tempdir");
let rooms = [RoomType::Planning];
let palace = seed_palace(tmp.path(), "leased", &rooms);
let kg_path = palace.data_dir.join("kg.redb");
let mut expired = Drawer::new(room_to_uuid(&RoomType::Planning), "an expired drawer");
expired.expires_at = Some(chrono::Utc::now() - chrono::Duration::days(1));
KgStoreRedb::open(&kg_path)
.expect("open kg")
.upsert_drawer(&expired)
.expect("seed expired drawer");
let holder = MaintenanceLease::new(tmp.path());
assert!(holder.try_hold().is_held());
let applied = audit_palaces(tmp.path(), None, true).expect("apply");
assert!(applied[0].error.is_none(), "{:?}", applied[0].error);
assert_eq!(
applied[0].inserted,
Some(rooms.len()),
"the apply still ran"
);
let ids = KgStoreRedb::open(&kg_path)
.expect("reopen kg")
.load_drawer_ids()
.expect("ids");
assert!(ids.contains(&expired.id), "the expired row was deleted");
}
#[test]
fn audit_can_be_scoped_to_one_palace() {
let tmp = tempfile::tempdir().expect("tempdir");
seed_palace(tmp.path(), "alpha", &[RoomType::General]);
seed_palace(tmp.path(), "beta", &[RoomType::Planning]);
let all = audit_palaces(tmp.path(), None, false).expect("all");
assert_eq!(all.len(), 2);
let one = audit_palaces(tmp.path(), Some("beta"), false).expect("one");
assert_eq!(one.len(), 1);
assert_eq!(one[0].palace_id, "beta");
}