velixar 0.1.0

Governed, auditable memory for AI agents. Official Rust client for the Velixar API.
Documentation
//! The wire contract, pinned.
//!
//! Every shape below was CAPTURED FROM LIVE PRODUCTION before a line of this crate
//! was written — not inferred from a schema, and not copied from another SDK.
//! Two of them are counter-intuitive and would have been "tidied" into something
//! wrong by anyone guessing:
//!
//!   * POST /v1/memory returns a FLAT {"id":…,"stored":true} — NOT {"data":{"id":…}}
//!   * a scope failure is 403 SCOPE_003, distinct from 404 NOT_FOUND_003
//!
//! These tests exist so that if the API changes, the crate fails loudly here rather
//! than silently mis-decoding in a user's service.

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"]
        })))
        // The real shape. Flat.
        .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() {
    // Not `"user_id": null` — the API treats a present-but-null user_id differently
    // from an absent one (absent = attribute to the key owner).
    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")) // the whole multi-user model
        .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() {
    // Fields the API omits must not become decode errors. This is the fake-fidelity
    // lesson: model what the server ACTUALLY sends, not the prettiest schema.
    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() {
    // These are DIFFERENT problems with different fixes: widen the key's scopes, vs.
    // the memory does not exist. Collapsing them is how a whole GDPR-erasure scare
    // got invented earlier in this project — a 404 from an under-privileged key was
    // read as "the feature does not work".
    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() {
    // A proxy 502 returns HTML. We must surface it, not decode-fail into something
    // that looks like an empty result.
    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());
}