use super::super::admin::admin_stop;
use super::super::router;
use super::test_state;
use axum::body::{to_bytes, Body};
use axum::extract::State;
use axum::http::{Request, StatusCode};
use axum::Json;
use serde_json::{json, Value};
use tower::util::ServiceExt;
use trusty_common::memory_core::palace::{Palace, PalaceId};
#[tokio::test]
async fn logs_tail_returns_recent_lines() {
let buffer = trusty_common::log_buffer::LogBuffer::new(100);
buffer.push("line one".to_string());
buffer.push("line two".to_string());
buffer.push("line three".to_string());
let state = test_state().with_log_buffer(buffer);
let app = router().with_state(state);
let resp = app
.oneshot(
Request::builder()
.uri("/api/v1/logs/tail?n=2")
.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 lines = v["lines"].as_array().expect("lines array");
assert_eq!(lines.len(), 2, "n=2 must return two lines");
assert_eq!(lines[0].as_str(), Some("line two"));
assert_eq!(lines[1].as_str(), Some("line three"));
assert_eq!(v["total"].as_u64(), Some(3));
}
#[tokio::test]
async fn logs_tail_clamps_n() {
let buffer = trusty_common::log_buffer::LogBuffer::new(100);
for i in 0..5 {
buffer.push(format!("l{i}"));
}
let state = test_state().with_log_buffer(buffer);
let app = router().with_state(state);
let resp = app
.clone()
.oneshot(
Request::builder()
.uri("/api/v1/logs/tail?n=0")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let bytes = to_bytes(resp.into_body(), 4096).await.unwrap();
let v: Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(v["lines"].as_array().expect("lines").len(), 1);
let resp = app
.oneshot(
Request::builder()
.uri("/api/v1/logs/tail?n=999999")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let bytes = to_bytes(resp.into_body(), 4096).await.unwrap();
let v: Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(v["lines"].as_array().expect("lines").len(), 5);
}
#[tokio::test]
async fn admin_stop_returns_ok() {
let state = test_state();
let Json(body) = admin_stop(State(state)).await;
assert_eq!(body["ok"], Value::Bool(true));
assert_eq!(body["message"].as_str(), Some("shutting down"));
}
#[tokio::test]
async fn admin_stop_does_not_exit_in_test() {
let state = test_state();
let Json(body) = admin_stop(State(state)).await;
tokio::task::yield_now().await;
assert_eq!(
body["ok"],
Value::Bool(true),
"admin_stop must return ok=true in test builds"
);
}
#[tokio::test]
async fn remember_async_returns_202_and_persists() {
let state = test_state();
let palace = Palace {
id: PalaceId::new("remember-async"),
name: "remember-async".to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: state.data_root.join("remember-async"),
};
state
.registry
.create_palace(&state.data_root, palace)
.expect("create_palace");
let app = router().with_state(state.clone());
let body = json!({
"content": "Trusty-memory note CLI ships a fire-and-forget HTTP endpoint for sub-agents.",
"palace": "remember-async",
"tags": ["docs", "note-cli"],
})
.to_string();
let resp = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/remember")
.header("content-type", "application/json")
.body(Body::from(body))
.unwrap(),
)
.await
.unwrap();
assert_eq!(
resp.status(),
StatusCode::ACCEPTED,
"remember endpoint must respond 202 immediately"
);
let bytes = to_bytes(resp.into_body(), 1024).await.unwrap();
let v: Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(v["status"], "queued");
let handle = state
.registry
.open_palace(&state.data_root, &PalaceId::new("remember-async"))
.expect("open palace");
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
loop {
let count = handle.drawers.read().len();
if count >= 1 {
break;
}
if std::time::Instant::now() >= deadline {
panic!("spawned remember task never persisted a drawer (count={count})");
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
}
#[tokio::test]
async fn remember_async_rejects_empty_content() {
let state = test_state();
let app = router().with_state(state);
for body in [json!({"content": ""}), json!({"content": " \n "})] {
let resp = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/remember")
.header("content-type", "application/json")
.body(Body::from(body.to_string()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(
resp.status(),
StatusCode::BAD_REQUEST,
"empty content must be rejected; body={body}"
);
}
}