remem-ai 0.5.207

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use axum::{
    body::to_bytes,
    extract::{Query, State},
    http::StatusCode,
    response::IntoResponse,
};
use serde_json::Value;

use crate::db;
use crate::db::test_support::ScopedTestDataDir;

use super::super::handlers::handle_list_candidates;
use super::super::types::CandidateParams;
use super::super::DbState;

#[tokio::test]
async fn list_candidates_defaults_to_pending_review() -> anyhow::Result<()> {
    let _test_dir = ScopedTestDataDir::new("api-candidates-pending-review");
    let conn = db::open_db()?;
    conn.execute(
        "INSERT INTO memory_candidates
          (scope, memory_type, topic_key, text, evidence_event_ids, confidence,
           risk_class, review_status, created_at_epoch, updated_at_epoch)
         VALUES
          ('project', 'decision', 'candidate-topic', 'needs review', '[]', 0.7,
           'low', 'pending_review', 1, 1)",
        [],
    )?;
    drop(conn);

    let response = handle_list_candidates(
        State(DbState),
        Query(CandidateParams {
            project: None,
            status: None,
            memory_type: None,
            block_reason: None,
            topic_key: None,
            contains: None,
            min_confidence: None,
            older_than_days: None,
            limit: Some(10),
            offset: None,
        }),
    )
    .await
    .into_response();
    assert_eq!(response.status(), StatusCode::OK);

    let body = to_bytes(response.into_body(), usize::MAX).await?;
    let payload: Value = serde_json::from_slice(&body)?;
    assert_eq!(payload["meta"]["total"], 1);
    assert_eq!(payload["data"][0]["review_status"], "pending_review");
    Ok(())
}

#[tokio::test]
async fn list_candidates_filters_by_project_when_requested() -> anyhow::Result<()> {
    let _test_dir = ScopedTestDataDir::new("api-candidates-project-filter");
    let conn = db::open_db()?;
    conn.execute(
        "INSERT INTO workspaces (id, root_path, created_at_epoch, updated_at_epoch)
         VALUES (1, 'workspace-a', 1, 1)",
        [],
    )?;
    conn.execute(
        "INSERT INTO projects (id, workspace_id, project_path, project_key, created_at_epoch, updated_at_epoch)
         VALUES
          (10, 1, 'proj-a', 'proj-a', 1, 1),
          (11, 1, 'proj-b', 'proj-b', 1, 1)",
        [],
    )?;
    conn.execute(
        "INSERT INTO memory_candidates
          (project_id, scope, memory_type, topic_key, text, evidence_event_ids, confidence,
           risk_class, review_status, created_at_epoch, updated_at_epoch)
         VALUES
          (10, 'project', 'decision', 'candidate-a', 'project candidate', '[]', 0.7,
           'low', 'pending_review', 3, 3),
          (11, 'project', 'decision', 'candidate-b', 'other candidate', '[]', 0.7,
           'low', 'pending_review', 2, 2)",
        [],
    )?;
    conn.execute(
        "INSERT INTO memory_candidates
          (project_id, scope, memory_type, topic_key, text, evidence_event_ids, confidence,
           risk_class, review_status, source_project, target_project, owner_scope, owner_key,
           created_at_epoch, updated_at_epoch)
         VALUES
          (11, 'project', 'decision', 'candidate-routed', 'routed candidate', '[]', 0.7,
           'low', 'pending_review', 'proj-b', 'proj-a', 'repo', 'proj-a', 4, 4)",
        [],
    )?;
    drop(conn);

    let response = handle_list_candidates(
        State(DbState),
        Query(CandidateParams {
            project: Some("proj-a".to_string()),
            status: None,
            memory_type: None,
            block_reason: None,
            topic_key: None,
            contains: None,
            min_confidence: None,
            older_than_days: None,
            limit: Some(10),
            offset: None,
        }),
    )
    .await
    .into_response();
    assert_eq!(response.status(), StatusCode::OK);

    let body = to_bytes(response.into_body(), usize::MAX).await?;
    let payload: Value = serde_json::from_slice(&body)?;
    let texts: Vec<&str> = payload["data"]
        .as_array()
        .expect("data should be array")
        .iter()
        .map(|item| item["text"].as_str().expect("text should be string"))
        .collect();
    assert!(texts.contains(&"project candidate"));
    assert!(texts.contains(&"routed candidate"));
    assert!(!texts.contains(&"other candidate"));
    assert_eq!(payload["data"][0]["project"], "proj-a");
    assert_eq!(payload["meta"]["total"], 2);
    Ok(())
}

#[tokio::test]
async fn list_candidates_contains_filter_treats_like_wildcards_literally() -> anyhow::Result<()> {
    let _test_dir = ScopedTestDataDir::new("api-candidates-like-escape");
    let conn = db::open_db()?;
    conn.execute(
        "INSERT INTO memory_candidates
          (scope, memory_type, topic_key, text, evidence_event_ids, confidence,
           risk_class, review_status, created_at_epoch, updated_at_epoch)
         VALUES
          ('project', 'decision', 'percent', 'contains 100% literal', '[]', 0.7,
           'low', 'pending_review', 1, 1),
          ('project', 'decision', 'ordinary', 'contains ordinary text', '[]', 0.7,
           'low', 'pending_review', 2, 2)",
        [],
    )?;
    drop(conn);

    let response = handle_list_candidates(
        State(DbState),
        Query(CandidateParams {
            project: None,
            status: None,
            memory_type: None,
            block_reason: None,
            topic_key: None,
            contains: Some("%".to_string()),
            min_confidence: None,
            older_than_days: None,
            limit: Some(10),
            offset: None,
        }),
    )
    .await
    .into_response();
    assert_eq!(response.status(), StatusCode::OK);

    let body = to_bytes(response.into_body(), usize::MAX).await?;
    let payload: Value = serde_json::from_slice(&body)?;
    assert_eq!(payload["meta"]["total"], 1);
    assert_eq!(payload["data"][0]["topic_key"], Value::Null);
    assert_eq!(payload["data"][0]["text"], "contains 100% literal");
    Ok(())
}

#[tokio::test]
async fn list_candidates_rejects_negative_older_than() -> anyhow::Result<()> {
    let _test_dir = ScopedTestDataDir::new("api-candidates-negative-age");
    let response = handle_list_candidates(
        State(DbState),
        Query(CandidateParams {
            project: None,
            status: None,
            memory_type: None,
            block_reason: None,
            topic_key: None,
            contains: None,
            min_confidence: None,
            older_than_days: Some(-1),
            limit: Some(10),
            offset: None,
        }),
    )
    .await
    .into_response();
    assert_eq!(response.status(), StatusCode::BAD_REQUEST);

    let body = to_bytes(response.into_body(), usize::MAX).await?;
    let payload: Value = serde_json::from_slice(&body)?;
    assert_eq!(payload["error"]["code"], "candidate_filter_invalid");
    Ok(())
}

#[tokio::test]
async fn list_candidates_rejects_oversized_older_than() -> anyhow::Result<()> {
    let _test_dir = ScopedTestDataDir::new("api-candidates-oversized-age");
    let response = handle_list_candidates(
        State(DbState),
        Query(CandidateParams {
            project: None,
            status: None,
            memory_type: None,
            block_reason: None,
            topic_key: None,
            contains: None,
            min_confidence: None,
            older_than_days: Some(i64::MAX),
            limit: Some(10),
            offset: None,
        }),
    )
    .await
    .into_response();
    assert_eq!(response.status(), StatusCode::BAD_REQUEST);

    let body = to_bytes(response.into_body(), usize::MAX).await?;
    let payload: Value = serde_json::from_slice(&body)?;
    assert_eq!(payload["error"]["code"], "candidate_filter_invalid");
    assert!(payload["error"]["message"]
        .as_str()
        .expect("error message should be string")
        .contains("too large"));
    Ok(())
}

#[tokio::test]
async fn list_candidates_rejects_out_of_range_min_confidence() -> anyhow::Result<()> {
    let _test_dir = ScopedTestDataDir::new("api-candidates-invalid-confidence");
    for min_confidence in [-0.1, 1.1, f64::NAN] {
        let response = handle_list_candidates(
            State(DbState),
            Query(CandidateParams {
                project: None,
                status: None,
                memory_type: None,
                block_reason: None,
                topic_key: None,
                contains: None,
                min_confidence: Some(min_confidence),
                older_than_days: None,
                limit: Some(10),
                offset: None,
            }),
        )
        .await
        .into_response();
        assert_eq!(response.status(), StatusCode::BAD_REQUEST);

        let body = to_bytes(response.into_body(), usize::MAX).await?;
        let payload: Value = serde_json::from_slice(&body)?;
        assert_eq!(payload["error"]["code"], "candidate_filter_invalid");
    }
    Ok(())
}