use chrono::Utc;
use serde_json::json;
use tempfile::TempDir;
use trusty_common::memory_core::dream::{DreamConfig, Dreamer};
use trusty_common::memory_core::palace::{DrawerType, Palace, PalaceId};
use trusty_common::memory_core::retrieval::{seed_shared_embedder_with_mock, PalaceHandle};
use trusty_memory::tools::dispatch_tool;
use trusty_memory::AppState;
fn ready_state(tmp: &TempDir) -> AppState {
let state = AppState::new(tmp.path().to_path_buf());
state.set_ready();
state
}
#[tokio::test]
async fn task_add_and_list_via_mcp() {
seed_shared_embedder_with_mock();
let tmp = tempfile::tempdir().expect("tempdir");
let state = ready_state(&tmp);
let cwd = tmp.path().to_string_lossy().to_string();
dispatch_tool(
&state,
"palace_create",
json!({ "name": "tasktest", "force": true, "cwd": cwd }),
)
.await
.expect("create palace");
let result = dispatch_tool(
&state,
"task_add",
json!({
"palace": "tasktest",
"content": "Ship v2 of the API before Friday",
"tags": ["deadline", "api"]
}),
)
.await
.expect("task_add");
assert_eq!(result["status"], "stored", "task_add must report stored");
assert_eq!(
result["drawer_type"], "Task",
"task_add must report drawer_type=Task"
);
let drawer_id = result["drawer_id"].as_str().expect("drawer_id").to_string();
assert!(!drawer_id.is_empty(), "drawer_id must be non-empty");
let list = dispatch_tool(&state, "task_list", json!({ "palace": "tasktest" }))
.await
.expect("task_list");
let tasks = list["tasks"].as_array().expect("tasks array");
assert_eq!(tasks.len(), 1, "exactly one task should be listed");
assert_eq!(tasks[0]["drawer_id"], drawer_id, "drawer_id must match");
assert_eq!(
tasks[0]["content"], "Ship v2 of the API before Friday",
"content must round-trip"
);
assert!(
tasks[0]["completed_at"].is_null(),
"open task must have null completed_at"
);
assert_eq!(
tasks[0]["drawer_type"], "Task",
"listed drawer must have drawer_type=Task"
);
}
#[tokio::test]
async fn task_drawer_survives_dream_cycle() {
seed_shared_embedder_with_mock();
let tmp = tempfile::tempdir().expect("tempdir");
let state = ready_state(&tmp);
let cwd = tmp.path().to_string_lossy().to_string();
dispatch_tool(
&state,
"palace_create",
json!({ "name": "dreamtask", "force": true, "cwd": cwd }),
)
.await
.expect("create palace");
let task_result = dispatch_tool(
&state,
"task_add",
json!({
"palace": "dreamtask",
"content": "Deploy to production by end of sprint"
}),
)
.await
.expect("task_add");
let task_id = task_result["drawer_id"]
.as_str()
.expect("task drawer_id")
.to_string();
dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "dreamtask",
"text": "Stale unimportant note that should be pruned by the dream cycle cleanup pass",
"force": true,
}),
)
.await
.expect("memory_remember");
let palace_obj = Palace {
id: PalaceId::new("dreamtask"),
name: "dreamtask".into(),
description: None,
created_at: Utc::now(),
data_dir: tmp.path().join("dreamtask"),
};
let handle = PalaceHandle::open(&palace_obj).expect("open palace handle");
{
let mut drawers = handle.drawers.write();
for d in drawers.iter_mut() {
if d.drawer_type != DrawerType::Task {
d.created_at = Utc::now() - chrono::Duration::days(60);
d.importance = 0.01;
}
}
}
let total_before = handle.drawers.read().len();
assert_eq!(total_before, 2, "should have 2 drawers before dream cycle");
let dreamer = Dreamer::new(DreamConfig {
dedup_threshold: 0.999, recall_benchmark_enabled: false, ..DreamConfig::default()
});
let stats = dreamer
.dream_cycle(&handle)
.await
.expect("dream_cycle must not error");
assert!(
stats.pruned >= 1,
"expected at least one normal drawer to be pruned; got stats: pruned={}, merged={}",
stats.pruned,
stats.merged
);
let survivors = handle.drawers.read();
let task_survivor = survivors.iter().find(|d| d.id.to_string() == task_id);
assert!(
task_survivor.is_some(),
"the Task drawer must survive the dream cycle"
);
assert_eq!(
task_survivor.unwrap().drawer_type,
DrawerType::Task,
"surviving drawer must still be Task type"
);
}
#[tokio::test]
async fn task_complete_sets_completed_at() {
seed_shared_embedder_with_mock();
let tmp = tempfile::tempdir().expect("tempdir");
let state = ready_state(&tmp);
let cwd = tmp.path().to_string_lossy().to_string();
dispatch_tool(
&state,
"palace_create",
json!({ "name": "completetest", "force": true, "cwd": cwd }),
)
.await
.expect("create palace");
let result = dispatch_tool(
&state,
"task_add",
json!({
"palace": "completetest",
"content": "Write the release notes for v2"
}),
)
.await
.expect("task_add");
let drawer_id = result["drawer_id"].as_str().expect("drawer_id").to_string();
let list_before = dispatch_tool(&state, "task_list", json!({ "palace": "completetest" }))
.await
.expect("task_list before");
assert_eq!(
list_before["tasks"].as_array().expect("tasks").len(),
1,
"open task should appear in default list"
);
let completed = dispatch_tool(
&state,
"task_complete",
json!({ "palace": "completetest", "drawer_id": drawer_id }),
)
.await
.expect("task_complete");
assert_eq!(
completed["status"], "completed",
"task_complete must report status=completed"
);
assert_eq!(
completed["drawer_id"], drawer_id,
"task_complete must echo back the drawer_id"
);
assert!(
!completed["completed_at"].is_null(),
"task_complete must set completed_at"
);
let list_after = dispatch_tool(&state, "task_list", json!({ "palace": "completetest" }))
.await
.expect("task_list after");
assert_eq!(
list_after["tasks"].as_array().expect("tasks").len(),
0,
"completed task must not appear in default list (include_completed defaults to false)"
);
let list_with = dispatch_tool(
&state,
"task_list",
json!({ "palace": "completetest", "include_completed": true }),
)
.await
.expect("task_list with include_completed");
let tasks = list_with["tasks"].as_array().expect("tasks");
assert_eq!(
tasks.len(),
1,
"completed task must appear when include_completed=true"
);
assert!(
!tasks[0]["completed_at"].is_null(),
"completed task must have a non-null completed_at"
);
}
#[tokio::test]
async fn task_complete_rejects_non_task_drawer() {
seed_shared_embedder_with_mock();
let tmp = tempfile::tempdir().expect("tempdir");
let state = ready_state(&tmp);
let cwd = tmp.path().to_string_lossy().to_string();
dispatch_tool(
&state,
"palace_create",
json!({ "name": "notasktest", "force": true, "cwd": cwd }),
)
.await
.expect("create palace");
let note_result = dispatch_tool(
&state,
"memory_note",
json!({
"palace": "notasktest",
"content": "This is a regular fact, not a task"
}),
)
.await
.expect("memory_note");
let note_id = note_result["drawer_id"].as_str().expect("drawer_id");
let err = dispatch_tool(
&state,
"task_complete",
json!({ "palace": "notasktest", "drawer_id": note_id }),
)
.await;
assert!(
err.is_err(),
"task_complete must error on a non-Task drawer"
);
let msg = err.unwrap_err().to_string();
assert!(
msg.contains("not a Task"),
"error message must mention 'not a Task', got: {msg}"
);
}
#[tokio::test]
async fn task_complete_errors_on_missing_drawer() {
seed_shared_embedder_with_mock();
let tmp = tempfile::tempdir().expect("tempdir");
let state = ready_state(&tmp);
let cwd = tmp.path().to_string_lossy().to_string();
dispatch_tool(
&state,
"palace_create",
json!({ "name": "missingtest", "force": true, "cwd": cwd }),
)
.await
.expect("create palace");
let fake_id = "00000000-0000-0000-0000-000000000000";
let err = dispatch_tool(
&state,
"task_complete",
json!({ "palace": "missingtest", "drawer_id": fake_id }),
)
.await;
assert!(
err.is_err(),
"task_complete with unknown drawer_id must error"
);
let msg = err.unwrap_err().to_string();
assert!(
msg.contains("not found"),
"error message must mention 'not found', got: {msg}"
);
}