use super::super::router;
use super::test_state;
use axum::body::{to_bytes, Body};
use axum::http::{Request, StatusCode};
use serde_json::{json, Value};
use tower::util::ServiceExt;
use trusty_common::memory_core::palace::PalaceId;
#[tokio::test]
async fn providers_endpoint_returns_payload() {
let state = test_state();
let app = router().with_state(state);
let resp = app
.oneshot(
Request::builder()
.uri("/api/v1/chat/providers")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = to_bytes(resp.into_body(), 4096).await.unwrap();
let v: Value = serde_json::from_slice(&bytes).unwrap();
let arr = v["providers"].as_array().expect("providers array");
assert_eq!(arr.len(), 2);
let names: Vec<&str> = arr.iter().filter_map(|p| p["name"].as_str()).collect();
assert!(names.contains(&"ollama"));
assert!(names.contains(&"openrouter"));
assert!(v.get("active").is_some());
}
#[tokio::test]
async fn chat_session_crud_round_trip() {
let state = test_state();
let palace = trusty_common::memory_core::Palace {
id: PalaceId::new("sess-test"),
name: "sess-test".to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: state.data_root.join("sess-test"),
};
state
.registry
.create_palace(&state.data_root, palace)
.expect("create_palace");
let app = router().with_state(state);
let resp = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/palaces/sess-test/chat/sessions")
.header("content-type", "application/json")
.body(Body::from(json!({"title":"first chat"}).to_string()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = to_bytes(resp.into_body(), 4096).await.unwrap();
let v: Value = serde_json::from_slice(&bytes).unwrap();
let sid = v["id"].as_str().expect("session id").to_string();
let resp = app
.clone()
.oneshot(
Request::builder()
.uri("/api/v1/palaces/sess-test/chat/sessions")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = to_bytes(resp.into_body(), 4096).await.unwrap();
let v: Value = serde_json::from_slice(&bytes).unwrap();
let arr = v.as_array().expect("array");
assert!(arr.iter().any(|s| s["id"] == sid));
let resp = app
.clone()
.oneshot(
Request::builder()
.uri(format!("/api/v1/palaces/sess-test/chat/sessions/{sid}"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let resp = app
.clone()
.oneshot(
Request::builder()
.method("DELETE")
.uri(format!("/api/v1/palaces/sess-test/chat/sessions/{sid}"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NO_CONTENT);
let resp = app
.oneshot(
Request::builder()
.uri(format!("/api/v1/palaces/sess-test/chat/sessions/{sid}"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn messages_endpoint_round_trip() {
let state = test_state();
let palace = trusty_common::memory_core::Palace {
id: PalaceId::new("msg-test"),
name: "msg-test".to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: state.data_root.join("msg-test"),
};
state
.registry
.create_palace(&state.data_root, palace)
.expect("create_palace");
let app = router().with_state(state);
let resp = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/messages")
.header("content-type", "application/json")
.body(Body::from(
json!({
"to_palace": "msg-test",
"from_palace": "sender-palace",
"purpose": "task",
"content": "please refresh schema"
})
.to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = to_bytes(resp.into_body(), 4096).await.unwrap();
let send_resp: Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(send_resp["status"], "sent");
let drawer_id = send_resp["drawer_id"]
.as_str()
.expect("drawer_id")
.to_string();
let resp = app
.clone()
.oneshot(
Request::builder()
.uri("/api/v1/messages?palace=msg-test&unread_only=true")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = to_bytes(resp.into_body(), 8192).await.unwrap();
let list: Vec<Value> = serde_json::from_slice(&bytes).unwrap();
assert_eq!(list.len(), 1);
assert_eq!(list[0]["id"], drawer_id);
assert_eq!(list[0]["from_palace"], "sender-palace");
assert_eq!(list[0]["to_palace"], "msg-test");
assert_eq!(list[0]["purpose"], "task");
assert_eq!(list[0]["content"], "please refresh schema");
assert_eq!(list[0]["read"], false);
assert!(list[0]["formatted"]
.as_str()
.unwrap()
.contains("sender-palace"));
let resp = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/messages/mark_read")
.header("content-type", "application/json")
.body(Body::from(
json!({"palace": "msg-test", "drawer_id": drawer_id}).to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = to_bytes(resp.into_body(), 1024).await.unwrap();
let mark: Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(mark["flipped"], true);
let resp = app
.clone()
.oneshot(
Request::builder()
.uri("/api/v1/messages?palace=msg-test&unread_only=true")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = to_bytes(resp.into_body(), 4096).await.unwrap();
let list: Vec<Value> = serde_json::from_slice(&bytes).unwrap();
assert!(list.is_empty(), "inbox cleared after mark_read");
let resp = app
.oneshot(
Request::builder()
.uri("/api/v1/messages?palace=msg-test&unread_only=false")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = to_bytes(resp.into_body(), 8192).await.unwrap();
let history: Vec<Value> = serde_json::from_slice(&bytes).unwrap();
assert_eq!(history.len(), 1);
assert_eq!(history[0]["read"], true);
}
#[test]
fn all_tools_returns_expected_set() {
let tools = crate::chat::all_tools();
let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
assert_eq!(
names,
vec![
"list_palaces",
"get_palace",
"recall_memories",
"list_drawers",
"kg_query",
"get_config",
"get_status",
"get_dream_status",
"get_palace_dream_status",
"create_memory",
"kg_assert",
"memory_recall_all",
]
);
for t in &tools {
assert_eq!(
t.parameters["type"], "object",
"tool {} schema type",
t.name
);
assert!(
t.parameters["required"].is_array(),
"tool {} required not array",
t.name
);
}
}
#[tokio::test]
async fn execute_tool_dispatches_known_tools() {
let state = test_state();
let result = crate::chat::execute_tool("list_palaces", "{}", &state).await;
assert!(
result.is_array(),
"list_palaces should be array, got {result}"
);
assert_eq!(result.as_array().unwrap().len(), 0);
let unknown = crate::chat::execute_tool("not_a_tool", "{}", &state).await;
assert!(
unknown["error"]
.as_str()
.unwrap_or("")
.contains("unknown tool"),
"expected unknown-tool error, got {unknown}"
);
let missing = crate::chat::execute_tool("get_palace", "{}", &state).await;
assert!(
missing["error"]
.as_str()
.unwrap_or("")
.contains("palace_id"),
"expected missing-arg error, got {missing}"
);
}