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}"
);
}
fn seed_palace(state: &crate::AppState, name: &str) {
let palace = trusty_common::memory_core::Palace {
id: PalaceId::new(name),
name: name.to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: state.data_root.join(name),
};
state
.registry
.create_palace(&state.data_root, palace)
.expect("create palace");
}
#[tokio::test]
async fn chat_kg_assert_refreshes_prompt_cache() {
let state = test_state();
seed_palace(&state, "chatassert");
let args = json!({
"palace_id": "chatassert",
"subject": "masa",
"predicate": "has_convention",
"object": "always branch off a freshly fetched origin/main",
})
.to_string();
let result = crate::chat::execute_tool("kg_assert", &args, &state).await;
assert_eq!(result["status"], "asserted", "got {result}");
let guard = state.prompt_context_cache.read().await;
assert!(
guard
.formatted
.contains("always branch off a freshly fetched origin/main"),
"chat kg_assert did not reach the prompt cache; got: {:?}",
guard.formatted
);
}
#[tokio::test]
async fn chat_kg_assert_reports_tier_s_refusal_without_writing() {
let state = test_state();
seed_palace(&state, "chatreject");
let over_long = "x".repeat(crate::prompt_facts::TIER_S_MAX_OBJECT_CHARS + 1);
let args = json!({
"palace_id": "chatreject",
"subject": "masa",
"predicate": "has_convention",
"object": over_long,
})
.to_string();
let result = crate::chat::execute_tool("kg_assert", &args, &state).await;
let err = result["error"].as_str().unwrap_or_default();
assert!(
err.contains("Tier S fact rejected"),
"expected the Tier S refusal text, got {result}"
);
let handle = state
.registry
.get(&PalaceId::new("chatreject"))
.expect("palace handle");
let stored = handle.kg.query_active("masa").await.expect("query");
assert!(
stored.is_empty(),
"refused write reached storage: {stored:?}"
);
assert!(
state.prompt_context_cache.read().await.triples.is_empty(),
"refused write reached the prompt cache"
);
}