use chrono::{Duration as ChronoDuration, Utc};
use std::sync::Arc;
use tempfile::tempdir;
use trusty_common::memory_core::dream::{DreamConfig, Dreamer};
use trusty_common::memory_core::palace::{DrawerType, Palace, PalaceId, RoomType};
use trusty_common::memory_core::retrieval::{
PalaceHandle, RememberOptions, seed_shared_embedder_with_mock,
};
fn open_palace(name: &str) -> (Arc<PalaceHandle>, std::path::PathBuf) {
seed_shared_embedder_with_mock();
let dir = tempdir().expect("tempdir");
let data_dir = dir.path().join(name);
std::fs::create_dir_all(&data_dir).expect("mkdir");
let palace = Palace {
id: PalaceId::new(name),
name: name.into(),
description: None,
created_at: Utc::now(),
data_dir: data_dir.clone(),
};
let handle = PalaceHandle::open(&palace).expect("open palace");
std::mem::forget(dir); (handle, data_dir)
}
async fn remember_typed(
handle: &Arc<PalaceHandle>,
content: &str,
importance: f32,
drawer_type: DrawerType,
) -> uuid::Uuid {
handle
.remember_with_options(
content.to_string(),
RoomType::General,
vec![],
importance,
RememberOptions {
force: true,
classify_as: Some(drawer_type),
..RememberOptions::default()
},
)
.await
.expect("remember")
}
#[tokio::test]
async fn task_drawers_survive_full_dream_cycle() {
let (handle, _dir) = open_palace("task-survival");
let goal = "Goal: ship the trusty-memory chat session manager to production";
remember_typed(&handle, goal, 0.01, DrawerType::Task).await;
remember_typed(&handle, goal, 0.01, DrawerType::Task).await; let ordinary_id = remember_typed(
&handle,
"an ordinary stale note nobody reads",
0.01,
DrawerType::UserFact,
)
.await;
{
let mut drawers = handle.drawers.write();
for d in drawers.iter_mut() {
d.created_at = Utc::now() - ChronoDuration::days(60);
}
}
assert_eq!(handle.drawers.read().len(), 3, "three drawers seeded");
let dreamer = Dreamer::new(DreamConfig::default());
dreamer.dream_cycle(&handle).await.expect("dream cycle");
let drawers = handle.drawers.read();
let task_count = drawers
.iter()
.filter(|d| d.drawer_type == DrawerType::Task)
.count();
assert_eq!(
task_count, 2,
"both Task drawers must survive eviction + dedup; got {task_count}"
);
assert!(
!drawers.iter().any(|d| d.id == ordinary_id),
"ordinary aged low-importance drawer should have been pruned"
);
}
#[test]
fn drawer_type_postcard_indices_are_stable() {
let cases: &[(DrawerType, u8)] = &[
(DrawerType::UserFact, 0),
(DrawerType::SessionEvent, 1),
(DrawerType::AgentNote, 2),
(DrawerType::Commit, 3),
(DrawerType::Unknown, 4), (DrawerType::Task, 5), ];
for (variant, expected_index) in cases {
let encoded = postcard::to_allocvec(variant)
.unwrap_or_else(|e| panic!("postcard encode {variant:?}: {e}"));
assert_eq!(
encoded[0], *expected_index,
"DrawerType::{variant:?} must serialize to byte {expected_index} \
(got {}); changing this breaks existing redb data",
encoded[0]
);
let decoded: DrawerType = postcard::from_bytes(&encoded)
.unwrap_or_else(|e| panic!("postcard decode {variant:?}: {e}"));
assert_eq!(
decoded, *variant,
"DrawerType::{variant:?} must round-trip through postcard"
);
}
}
#[tokio::test]
async fn task_completed_at_round_trips_through_reopen() {
let (handle, data_dir) = open_palace("task-completed");
let id = remember_typed(&handle, "Goal: cut the v2 release", 0.5, DrawerType::Task).await;
let done = Utc::now();
let updated = {
let mut drawers = handle.drawers.write();
let d = drawers.iter_mut().find(|d| d.id == id).expect("drawer");
d.completed_at = Some(done);
d.clone()
};
handle
.kg
.upsert_drawer_sync(&updated)
.expect("persist completed_at");
handle.flush().expect("flush");
drop(handle);
let palace = Palace {
id: PalaceId::new("task-completed"),
name: "task-completed".into(),
description: None,
created_at: Utc::now(),
data_dir,
};
let reopened = PalaceHandle::open(&palace).expect("reopen palace");
let drawers = reopened.drawers.read();
let d = drawers
.iter()
.find(|d| d.id == id)
.expect("drawer survived");
assert_eq!(d.drawer_type, DrawerType::Task);
let got = d.completed_at.expect("completed_at persisted");
assert_eq!(got.timestamp_millis(), done.timestamp_millis());
}