use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
time::Duration,
};
use clawdb::ClawDBConfig;
use clawdb_server::{
build_state,
grpc::service::proto::{
claw_db_service_client::ClawDbServiceClient, HealthRequest, RememberRequest, SearchRequest,
},
spawn_servers, ServerOptions,
};
use tempfile::tempdir;
use uuid::Uuid;
#[tokio::test]
async fn server_exposes_http_grpc_and_metrics() -> anyhow::Result<()> {
let temp = tempdir()?;
let mut config = ClawDBConfig::default_for_dir(temp.path());
config.guard.jwt_secret = "test-secret".to_string();
config.vector.enabled = false;
config.reflect.base_url = None;
config.reflect.api_key = None;
config.sync.hub_url = None;
let state = build_state(config).await?;
let session = state
.db
.session(
Uuid::new_v4(),
"agent",
vec!["memory:write".to_string(), "memory:read".to_string()],
)
.await?;
let servers = spawn_servers(
state,
ServerOptions {
grpc_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
http_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
metrics_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
},
)
.await?;
let http_base = format!("http://{}", servers.addresses.http);
let metrics_url = format!("http://{}/metrics", servers.addresses.metrics);
let grpc_url = format!("http://{}", servers.addresses.grpc);
let client = reqwest::Client::new();
let health = get_with_retry(&client, &format!("{http_base}/v1/health")).await?;
assert!(health.status().is_success());
let ready = get_with_retry(&client, &format!("{http_base}/v1/ready")).await?;
assert!(ready.status().is_success());
let unauthorized = client
.post(format!("{http_base}/v1/memories"))
.json(&serde_json::json!({ "content": "blocked" }))
.send()
.await?;
assert_eq!(unauthorized.status(), reqwest::StatusCode::UNAUTHORIZED);
let mut grpc = connect_grpc_with_retry(&grpc_url).await?;
let grpc_health = grpc.health(tonic::Request::new(HealthRequest {})).await?;
assert!(grpc_health.get_ref().ok);
assert!(!grpc_health.get_ref().request_id.is_empty());
let unauth = grpc
.remember(tonic::Request::new(RememberRequest {
content: "unauthorized".to_string(),
}))
.await
.expect_err("missing token should be rejected");
assert_eq!(unauth.code(), tonic::Code::Unauthenticated);
let mut remember_req = tonic::Request::new(RememberRequest {
content: "grpc memory".to_string(),
});
remember_req.metadata_mut().insert(
"x-claw-session",
tonic::metadata::MetadataValue::try_from(session.token.as_str())?,
);
let remember_resp = grpc.remember(remember_req).await?;
assert!(!remember_resp.get_ref().memory_id.is_empty());
let mut search_req = tonic::Request::new(SearchRequest {
query: "grpc memory".to_string(),
top_k: 5,
semantic: false,
filter_json: String::new(),
});
search_req.metadata_mut().insert(
"x-claw-session",
tonic::metadata::MetadataValue::try_from(session.token.as_str())?,
);
let search_resp = grpc.search(search_req).await?;
assert!(!search_resp.get_ref().hits.is_empty());
let metrics = get_with_retry(&client, &metrics_url).await?;
assert!(metrics.status().is_success());
let body = metrics.text().await?;
assert!(body.contains("clawdb_http_requests_total"));
assert!(body.contains("clawdb_grpc_requests_total"));
servers.shutdown(Duration::from_secs(5)).await?;
Ok(())
}
async fn get_with_retry(client: &reqwest::Client, url: &str) -> anyhow::Result<reqwest::Response> {
let mut last_error = None;
for _ in 0..20 {
match client.get(url).send().await {
Ok(response) => return Ok(response),
Err(error) => last_error = Some(error),
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
Err(last_error
.expect("retry loop should capture an error")
.into())
}
async fn connect_grpc_with_retry(
url: &str,
) -> anyhow::Result<ClawDbServiceClient<tonic::transport::Channel>> {
let mut last_error = None;
for _ in 0..20 {
match ClawDbServiceClient::connect(url.to_string()).await {
Ok(client) => return Ok(client),
Err(error) => last_error = Some(error),
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
Err(last_error
.expect("retry loop should capture an error")
.into())
}