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::{Palace, PalaceId};
#[tokio::test]
async fn status_endpoint_returns_payload() {
let state = test_state();
let app = router().with_state(state);
let resp = app
.oneshot(
Request::builder()
.uri("/api/v1/status")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = to_bytes(resp.into_body(), 1024).await.unwrap();
let v: Value = serde_json::from_slice(&bytes).unwrap();
assert!(v["version"].is_string());
assert_eq!(v["palace_count"], 0);
}
#[tokio::test]
async fn unknown_api_returns_404() {
let state = test_state();
let app = router().with_state(state);
let resp = app
.oneshot(
Request::builder()
.uri("/api/v1/does-not-exist")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn memories_alias_routes_to_drawers() {
let state = test_state();
let palace = Palace {
id: PalaceId::new("alias-test"),
name: "alias-test".to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: state.data_root.join("alias-test"),
};
state
.registry
.create_palace(&state.data_root, palace)
.expect("create_palace");
let app = router().with_state(state);
let resp = app
.oneshot(
Request::builder()
.uri("/api/v1/palaces/alias-test/memories")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(
resp.status(),
StatusCode::OK,
"the /memories alias must resolve to list_drawers, not 404"
);
let bytes = to_bytes(resp.into_body(), 4096).await.unwrap();
let v: Value = serde_json::from_slice(&bytes).unwrap();
assert!(
v.is_array(),
"the alias must return the list-drawers array shape, got {v:?}"
);
}
#[tokio::test]
async fn http_create_drawer_runs_auto_kg_extraction() {
let state = test_state();
let palace = Palace {
id: PalaceId::new("kgauto-http"),
name: "kgauto-http".to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: state.data_root.join("kgauto-http"),
};
state
.registry
.create_palace(&state.data_root, palace)
.expect("create_palace");
let app = router().with_state(state.clone());
let body = json!({
"content": "trusty-memory is a Rust crate that ships an MCP server. \
It tracks #mcp and #rust topics with care.",
"room": "Backend",
"tags": ["backend", "kg"],
"importance": 0.5,
})
.to_string();
let resp = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/palaces/kgauto-http/drawers")
.header("content-type", "application/json")
.body(Body::from(body))
.unwrap(),
)
.await
.unwrap();
assert_eq!(
resp.status(),
StatusCode::OK,
"create_drawer must return 200 OK"
);
let resp = app
.oneshot(
Request::builder()
.uri("/api/v1/palaces/kgauto-http/kg/graph")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = to_bytes(resp.into_body(), 64 * 1024).await.unwrap();
let v: Value = serde_json::from_slice(&bytes).unwrap();
let triples = v["triples"].as_array().expect("triples array");
assert!(
!triples.is_empty(),
"HTTP-origin drawer must populate the KG; got empty graph"
);
let auto: Vec<&Value> = triples
.iter()
.filter(|t| t["provenance"].as_str() == Some(crate::kg_extract::AUTO_PROVENANCE))
.collect();
assert!(
!auto.is_empty(),
"expected at least one auto-extracted triple in HTTP-populated KG; got: {triples:?}"
);
assert!(
auto.iter()
.any(|t| t["subject"].as_str() == Some("tag:backend")),
"expected `tag:backend` auto-extracted edge, got: {auto:?}"
);
assert!(
auto.iter()
.any(|t| t["predicate"].as_str() == Some("mentioned-in")),
"expected at least one #hashtag mention triple, got: {auto:?}"
);
}
#[tokio::test]
async fn create_then_list_palace() {
let state = test_state();
let app = router().with_state(state.clone());
let body = json!({"name": "web-test", "description": "from test"}).to_string();
let resp = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/palaces")
.header("content-type", "application/json")
.body(Body::from(body))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let resp = app
.oneshot(
Request::builder()
.uri("/api/v1/palaces")
.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(|p| p["id"] == "web-test"));
}
#[tokio::test]
async fn delete_palace_removes_dir_when_empty() {
let state = test_state();
let app = router().with_state(state.clone());
let body = json!({"name": "to-delete"}).to_string();
let resp = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/palaces")
.header("content-type", "application/json")
.body(Body::from(body))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let resp = app
.clone()
.oneshot(
Request::builder()
.method("DELETE")
.uri("/api/v1/palaces/to-delete")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NO_CONTENT);
let resp = app
.oneshot(
Request::builder()
.uri("/api/v1/palaces/to-delete")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let palace_dir = state.data_root.join("to-delete");
assert!(
!palace_dir.exists(),
"palace dir should be removed: {}",
palace_dir.display()
);
}
#[tokio::test]
async fn delete_palace_refuses_when_drawers_present() {
let state = test_state();
let app = router().with_state(state.clone());
let resp = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/palaces")
.header("content-type", "application/json")
.body(Body::from(json!({"name": "keep-me"}).to_string()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let resp = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/palaces/keep-me/drawers")
.header("content-type", "application/json")
.body(Body::from(
json!({
"content": "Important fact that should not be deleted accidentally.",
"tags": [],
})
.to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let resp = app
.clone()
.oneshot(
Request::builder()
.method("DELETE")
.uri("/api/v1/palaces/keep-me")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::CONFLICT);
let resp = app
.oneshot(
Request::builder()
.uri("/api/v1/palaces/keep-me")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}