use serde_json::json;
use tempfile::TempDir;
use trusty_memory::tools::dispatch_tool;
use trusty_memory::AppState;
const PALACE: &str = "chat-app";
fn state_at(root: &TempDir) -> AppState {
let state = AppState::new(root.path().to_path_buf());
state.set_ready();
state
}
#[tokio::test]
async fn chat_session_create_returns_id() {
let tmp = tempfile::tempdir().expect("tempdir");
let state = state_at(&tmp);
let created = dispatch_tool(
&state,
"chat_session_create",
json!({ "palace": PALACE, "title": "First chat" }),
)
.await
.expect("create");
let sid = created["session_id"].as_str().expect("session_id");
assert!(!sid.is_empty(), "session id is non-empty");
assert_eq!(created["message_count"], 0);
assert!(created["created_at"].is_string(), "created_at present");
}
#[tokio::test]
async fn chat_session_create_with_explicit_id() {
let tmp = tempfile::tempdir().expect("tempdir");
let state = state_at(&tmp);
let first = dispatch_tool(
&state,
"chat_session_create",
json!({ "palace": PALACE, "session_id": "fixed-123" }),
)
.await
.expect("create");
assert_eq!(first["session_id"], "fixed-123");
dispatch_tool(
&state,
"chat_session_add_turn",
json!({ "palace": PALACE, "session_id": "fixed-123", "role": "user", "content": "hi" }),
)
.await
.expect("add turn");
let again = dispatch_tool(
&state,
"chat_session_create",
json!({ "palace": PALACE, "session_id": "fixed-123" }),
)
.await
.expect("re-create idempotent");
assert_eq!(again["session_id"], "fixed-123");
assert_eq!(again["message_count"], 1, "existing history preserved");
}
#[tokio::test]
async fn chat_session_add_turn_appends() {
let tmp = tempfile::tempdir().expect("tempdir");
let state = state_at(&tmp);
let sid = dispatch_tool(&state, "chat_session_create", json!({ "palace": PALACE }))
.await
.expect("create")["session_id"]
.as_str()
.expect("sid")
.to_string();
let r1 = dispatch_tool(
&state,
"chat_session_add_turn",
json!({ "palace": PALACE, "session_id": sid, "role": "user", "content": "What is redb?" }),
)
.await
.expect("turn 1");
assert_eq!(r1["message_count"], 1);
let r2 = dispatch_tool(
&state,
"chat_session_add_turn",
json!({ "palace": PALACE, "session_id": sid, "role": "assistant", "content": "An embedded KV store." }),
)
.await
.expect("turn 2");
assert_eq!(r2["message_count"], 2);
assert!(r2["updated_at"].is_string());
}
#[tokio::test]
async fn chat_session_add_turn_rejects_bad_role() {
let tmp = tempfile::tempdir().expect("tempdir");
let state = state_at(&tmp);
let err = dispatch_tool(
&state,
"chat_session_add_turn",
json!({ "palace": PALACE, "session_id": "s1", "role": "robot", "content": "x" }),
)
.await
.expect_err("bad role rejected");
assert!(format!("{err:#}").contains("invalid role"));
}
#[tokio::test]
async fn chat_session_get_round_trips() {
let tmp = tempfile::tempdir().expect("tempdir");
let state = state_at(&tmp);
let sid = dispatch_tool(&state, "chat_session_create", json!({ "palace": PALACE }))
.await
.expect("create")["session_id"]
.as_str()
.expect("sid")
.to_string();
for (role, content) in [("user", "a"), ("assistant", "b"), ("system", "c")] {
dispatch_tool(
&state,
"chat_session_add_turn",
json!({ "palace": PALACE, "session_id": sid, "role": role, "content": content }),
)
.await
.expect("turn");
}
let got = dispatch_tool(
&state,
"chat_session_get",
json!({ "palace": PALACE, "session_id": sid }),
)
.await
.expect("get");
let history = got["history"].as_array().expect("history");
assert_eq!(history.len(), 3);
assert_eq!(history[0]["role"], "user");
assert_eq!(history[0]["content"], "a");
assert_eq!(history[2]["role"], "system");
let missing = dispatch_tool(
&state,
"chat_session_get",
json!({ "palace": PALACE, "session_id": "does-not-exist" }),
)
.await;
assert!(missing.is_err(), "missing session must error");
}
#[tokio::test]
async fn chat_session_list_paginates() {
let tmp = tempfile::tempdir().expect("tempdir");
let state = state_at(&tmp);
for i in 0..3 {
dispatch_tool(
&state,
"chat_session_create",
json!({ "palace": PALACE, "session_id": format!("s{i}") }),
)
.await
.expect("create");
}
let all = dispatch_tool(&state, "chat_session_list", json!({ "palace": PALACE }))
.await
.expect("list");
assert_eq!(all["total_count"], 3);
assert_eq!(all["sessions"].as_array().expect("sessions").len(), 3);
let page = dispatch_tool(
&state,
"chat_session_list",
json!({ "palace": PALACE, "limit": 1, "offset": 1 }),
)
.await
.expect("list paged");
assert_eq!(page["total_count"], 3, "total ignores pagination");
assert_eq!(page["sessions"].as_array().expect("sessions").len(), 1);
}
#[tokio::test]
async fn turns_persist_across_restart() {
let tmp = tempfile::tempdir().expect("tempdir");
let sid = {
let state = state_at(&tmp);
let sid = dispatch_tool(
&state,
"chat_session_create",
json!({ "palace": PALACE, "session_id": "durable" }),
)
.await
.expect("create")["session_id"]
.as_str()
.expect("sid")
.to_string();
dispatch_tool(
&state,
"chat_session_add_turn",
json!({ "palace": PALACE, "session_id": sid, "role": "user", "content": "remember me" }),
)
.await
.expect("turn");
sid
};
let state2 = state_at(&tmp);
let got = dispatch_tool(
&state2,
"chat_session_get",
json!({ "palace": PALACE, "session_id": sid }),
)
.await
.expect("get after restart");
let history = got["history"].as_array().expect("history");
assert_eq!(history.len(), 1, "turn survived restart");
assert_eq!(history[0]["content"], "remember me");
}
#[tokio::test]
async fn chat_session_delete_removes_session() {
let tmp = tempfile::tempdir().expect("tempdir");
let state = state_at(&tmp);
let sid = dispatch_tool(&state, "chat_session_create", json!({ "palace": PALACE }))
.await
.expect("create")["session_id"]
.as_str()
.expect("sid")
.to_string();
dispatch_tool(
&state,
"chat_session_get",
json!({ "palace": PALACE, "session_id": sid }),
)
.await
.expect("get before delete");
let del = dispatch_tool(
&state,
"chat_session_delete",
json!({ "palace": PALACE, "session_id": sid }),
)
.await
.expect("delete");
assert_eq!(del["deleted"], sid);
let gone = dispatch_tool(
&state,
"chat_session_get",
json!({ "palace": PALACE, "session_id": sid }),
)
.await;
assert!(gone.is_err(), "deleted session must not be retrievable");
dispatch_tool(
&state,
"chat_session_delete",
json!({ "palace": PALACE, "session_id": sid }),
)
.await
.expect("double-delete idempotent");
}
#[tokio::test]
async fn chat_session_recall_returns_history() {
let tmp = tempfile::tempdir().expect("tempdir");
let state = state_at(&tmp);
let sid = dispatch_tool(&state, "chat_session_create", json!({ "palace": PALACE }))
.await
.expect("create")["session_id"]
.as_str()
.expect("sid")
.to_string();
dispatch_tool(
&state,
"chat_session_add_turn",
json!({ "palace": PALACE, "session_id": sid, "role": "user", "content": "recall me" }),
)
.await
.expect("turn");
let recalled = dispatch_tool(
&state,
"chat_session_recall",
json!({ "palace": PALACE, "session_id": sid }),
)
.await
.expect("recall");
let history = recalled["history"].as_array().expect("history");
assert_eq!(history.len(), 1);
assert_eq!(history[0]["content"], "recall me");
}
#[tokio::test]
async fn chat_turn_append_stores_pair() {
let tmp = tempfile::tempdir().expect("tempdir");
let state = state_at(&tmp);
let sid = dispatch_tool(&state, "chat_session_create", json!({ "palace": PALACE }))
.await
.expect("create")["session_id"]
.as_str()
.expect("sid")
.to_string();
let r = dispatch_tool(
&state,
"chat_turn_append",
json!({
"palace": PALACE,
"session_id": sid,
"prompt": "What is redb?",
"response": "An embedded key-value store backed by LMDB-like memory-mapped files."
}),
)
.await
.expect("append pair");
assert_eq!(r["message_count"], 2);
assert!(r["updated_at"].is_string());
let got = dispatch_tool(
&state,
"chat_session_get",
json!({ "palace": PALACE, "session_id": sid }),
)
.await
.expect("get");
let history = got["history"].as_array().expect("history");
assert_eq!(history.len(), 2);
assert_eq!(history[0]["role"], "user");
assert_eq!(history[0]["content"], "What is redb?");
assert_eq!(history[1]["role"], "assistant");
assert_eq!(
history[1]["content"],
"An embedded key-value store backed by LMDB-like memory-mapped files."
);
}