use serde_json::json;
use velixar::{Error, SearchOpts, StoreOpts, Velixar};
use wiremock::matchers::{body_json, header, method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};
async fn client(server: &MockServer) -> Velixar {
Velixar::builder("vlx_test")
.base_url(server.uri())
.build()
.unwrap()
}
#[tokio::test]
async fn store_sends_the_key_and_reads_the_flat_id() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/memory"))
.and(header("authorization", "Bearer vlx_test"))
.and(body_json(json!({
"content": "Alex is a vegetarian.",
"user_id": "alex",
"tags": ["diet"]
})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"id": "6024acec-6533-4fef-bc2c-a32e2cdf50cb",
"stored": true
})))
.mount(&server)
.await;
let id = client(&server)
.await
.store(
"Alex is a vegetarian.",
StoreOpts::new().user("alex").tags(["diet"]),
)
.await
.unwrap();
assert_eq!(id, "6024acec-6533-4fef-bc2c-a32e2cdf50cb");
}
#[tokio::test]
async fn store_omits_absent_options_entirely() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/memory"))
.and(body_json(json!({ "content": "no options" })))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"id": "m1", "stored": true})))
.mount(&server)
.await;
client(&server)
.await
.store("no options", StoreOpts::new())
.await
.unwrap();
}
#[tokio::test]
async fn search_scopes_to_the_end_user() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/memory/search"))
.and(query_param("query", "dinner"))
.and(query_param("user_id", "alex")) .and(query_param("limit", "5"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"memories": [{
"id": "m1",
"content": "Alex is a vegetarian.",
"score": 0.3644,
"tier": 2,
"tags": ["diet"],
"user_id": "alex",
"source_class": "agent",
"created_at": "2026-07-13T02:25:55.476943+00:00"
}],
"count": 1
})))
.mount(&server)
.await;
let r = client(&server)
.await
.search("dinner", SearchOpts::new().user("alex").limit(5))
.await
.unwrap();
assert_eq!(r.count, 1);
let m = &r.memories[0];
assert_eq!(m.user_id.as_deref(), Some("alex"));
assert_eq!(m.source_class.as_deref(), Some("agent"));
assert!((m.score.unwrap() - 0.3644).abs() < 1e-4);
}
#[tokio::test]
async fn a_hit_missing_optional_fields_still_decodes() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/memory/search"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"memories": [{ "id": "m1", "content": "bare minimum" }],
"count": 1
})))
.mount(&server)
.await;
let r = client(&server)
.await
.search("x", SearchOpts::new())
.await
.unwrap();
assert_eq!(r.memories[0].score, None);
assert!(r.memories[0].tags.is_empty());
}
#[tokio::test]
async fn delete_without_the_scope_is_a_scope_error_not_a_not_found() {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/memory/m1"))
.respond_with(ResponseTemplate::new(403).set_body_json(json!({
"error": {"code": "SCOPE_003", "message": "Insufficient scope: memory:delete required"}
})))
.mount(&server)
.await;
let err = client(&server).await.delete("m1").await.unwrap_err();
assert!(matches!(err, Error::Scope(_)), "got {err:?}");
assert_eq!(err.code(), Some("SCOPE_003"));
}
#[tokio::test]
async fn each_documented_status_maps_to_its_own_variant() {
for (status, code, want_scope) in [
(401u16, "AUTH_003", false),
(400, "VALIDATION_001", false),
(404, "NOT_FOUND_003", false),
(429, "RATE_LIMIT", false),
(403, "SCOPE_003", true),
] {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/memory/m1"))
.respond_with(ResponseTemplate::new(status).set_body_json(json!({
"error": {"code": code, "message": "boom"}
})))
.mount(&server)
.await;
let err = client(&server).await.delete("m1").await.unwrap_err();
assert_eq!(err.code(), Some(code), "status {status}");
assert_eq!(matches!(err, Error::Scope(_)), want_scope);
}
}
#[tokio::test]
async fn a_non_json_error_body_does_not_masquerade_as_success() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/memory/search"))
.respond_with(ResponseTemplate::new(502).set_body_string("<html>bad gateway</html>"))
.mount(&server)
.await;
let err = client(&server)
.await
.search("x", SearchOpts::new())
.await
.unwrap_err();
assert_eq!(err.code(), Some("HTTP_502"));
}
#[tokio::test]
async fn a_trailing_slash_on_the_base_url_does_not_produce_a_double_slash() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/health"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"status": "healthy"})))
.mount(&server)
.await;
let c = Velixar::builder("k")
.base_url(format!("{}/", server.uri()))
.build()
.unwrap();
assert!(c.health().await.is_ok());
}