// RLM API Integration Tests // Tests require SurrealDB: docker run -p 8000:8000 surrealdb/surrealdb:latest // start --bind 0.0.0.0:8000 use axum::{ body::Body, http::{Request, StatusCode}, Router, }; use serde_json::{json, Value}; use surrealdb::engine::remote::ws::Ws; use surrealdb::opt::auth::Root; use surrealdb::Surreal; use tower::ServiceExt; use vapora_backend::api::AppState; use vapora_backend::services::{ AgentService, ProjectService, ProposalService, ProviderAnalyticsService, TaskService, }; async fn setup_test_app() -> Router { // Connect to SurrealDB let db = Surreal::new::("127.0.0.1:8000") .await .expect("Failed to connect to SurrealDB"); db.signin(Root { username: "root", password: "root", }) .await .expect("Failed to sign in"); db.use_ns("test_rlm_api") .use_db("test_rlm_api") .await .expect("Failed to use namespace"); // Initialize services let project_service = ProjectService::new(db.clone()); let task_service = TaskService::new(db.clone()); let agent_service = AgentService::new(db.clone()); let proposal_service = ProposalService::new(db.clone()); let provider_analytics_service = ProviderAnalyticsService::new(db.clone()); // Create RLM engine let rlm_storage = vapora_rlm::storage::SurrealDBStorage::new(db.clone()); let rlm_bm25_index = std::sync::Arc::new(vapora_rlm::search::bm25::BM25Index::new().unwrap()); let rlm_engine = std::sync::Arc::new( vapora_rlm::RLMEngine::new(std::sync::Arc::new(rlm_storage), rlm_bm25_index).unwrap(), ); // Create application state let app_state = AppState::new( project_service, task_service, agent_service, proposal_service, provider_analytics_service, ) .with_rlm_engine(rlm_engine); // Build router with RLM endpoints Router::new() .route( "/api/v1/rlm/documents", axum::routing::post(vapora_backend::api::rlm::load_document), ) .route( "/api/v1/rlm/query", axum::routing::post(vapora_backend::api::rlm::query_document), ) .route( "/api/v1/rlm/analyze", axum::routing::post(vapora_backend::api::rlm::analyze_document), ) .with_state(app_state) } #[tokio::test] #[ignore] // Requires SurrealDB async fn test_load_document_endpoint() { let app = setup_test_app().await; let request = Request::builder() .method("POST") .uri("/api/v1/rlm/documents") .header("content-type", "application/json") .body(Body::from( json!({ "doc_id": "test-doc-1", "content": "Rust is a systems programming language. It provides memory safety without garbage collection. Rust uses ownership and borrowing.", "strategy": "semantic" }) .to_string(), )) .unwrap(); let response = app.oneshot(request).await.unwrap(); assert_eq!(response.status(), StatusCode::CREATED); let body = axum::body::to_bytes(response.into_body(), usize::MAX) .await .unwrap(); let json: Value = serde_json::from_slice(&body).unwrap(); assert_eq!(json["doc_id"], "test-doc-1"); assert_eq!(json["strategy"], "semantic"); assert!(json["chunk_count"].as_u64().unwrap() > 0); } #[tokio::test] #[ignore] // Requires SurrealDB async fn test_query_document_endpoint() { // First, load a document let load_request = Request::builder() .method("POST") .uri("/api/v1/rlm/documents") .header("content-type", "application/json") .body(Body::from( json!({ "doc_id": "test-doc-2", "content": "Rust ownership system ensures memory safety. \ The borrow checker validates references at compile time. \ Lifetimes track how long references are valid.", "strategy": "semantic" }) .to_string(), )) .unwrap(); let load_response = setup_test_app().await.oneshot(load_request).await.unwrap(); assert_eq!(load_response.status(), StatusCode::CREATED); // Small delay to ensure indexing completes tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; // Query the document let query_request = Request::builder() .method("POST") .uri("/api/v1/rlm/query") .header("content-type", "application/json") .body(Body::from( json!({ "doc_id": "test-doc-2", "query": "How does Rust ensure memory safety?", "limit": 3 }) .to_string(), )) .unwrap(); let response = setup_test_app().await.oneshot(query_request).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = axum::body::to_bytes(response.into_body(), usize::MAX) .await .unwrap(); let json: Value = serde_json::from_slice(&body).unwrap(); assert_eq!(json["query"], "How does Rust ensure memory safety?"); assert!(json["result_count"].as_u64().unwrap() > 0); assert!(json["chunks"].is_array()); } #[tokio::test] #[ignore] // Requires SurrealDB and LLM provider async fn test_analyze_document_endpoint() { // First, load a document let load_request = Request::builder() .method("POST") .uri("/api/v1/rlm/documents") .header("content-type", "application/json") .body(Body::from( json!({ "doc_id": "test-doc-3", "content": "Rust programming language features: \ 1. Memory safety without garbage collection. \ 2. Zero-cost abstractions. \ 3. Fearless concurrency. \ 4. Trait-based generics.", "strategy": "semantic" }) .to_string(), )) .unwrap(); let load_response = setup_test_app().await.oneshot(load_request).await.unwrap(); assert_eq!(load_response.status(), StatusCode::CREATED); // Small delay to ensure indexing completes tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; // Analyze the document (Note: This test requires LLM provider configured) let analyze_request = Request::builder() .method("POST") .uri("/api/v1/rlm/analyze") .header("content-type", "application/json") .body(Body::from( json!({ "doc_id": "test-doc-3", "query": "Summarize the key features of Rust", "limit": 5 }) .to_string(), )) .unwrap(); let response = setup_test_app() .await .oneshot(analyze_request) .await .unwrap(); // This might fail if no LLM provider is configured // We check for either success or expected error if response.status() == StatusCode::OK { let body = axum::body::to_bytes(response.into_body(), usize::MAX) .await .unwrap(); let json: Value = serde_json::from_slice(&body).unwrap(); assert_eq!(json["query"], "Summarize the key features of Rust"); assert!(json["result"].is_string()); assert!(json["chunks_used"].as_u64().unwrap() > 0); } else { // Expected if no LLM provider configured assert!( response.status().is_client_error() || response.status().is_server_error(), "Expected error status due to missing LLM provider" ); } } #[tokio::test] #[ignore] // Requires SurrealDB async fn test_load_document_validation() { let app = setup_test_app().await; // Test with missing doc_id let request = Request::builder() .method("POST") .uri("/api/v1/rlm/documents") .header("content-type", "application/json") .body(Body::from( json!({ "content": "Some content" }) .to_string(), )) .unwrap(); let response = app.oneshot(request).await.unwrap(); assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); } #[tokio::test] #[ignore] // Requires SurrealDB async fn test_query_nonexistent_document() { let app = setup_test_app().await; let request = Request::builder() .method("POST") .uri("/api/v1/rlm/query") .header("content-type", "application/json") .body(Body::from( json!({ "doc_id": "nonexistent-doc", "query": "test query", "limit": 5 }) .to_string(), )) .unwrap(); let response = app.oneshot(request).await.unwrap(); // Should return OK with empty results assert_eq!(response.status(), StatusCode::OK); let body = axum::body::to_bytes(response.into_body(), usize::MAX) .await .unwrap(); let json: Value = serde_json::from_slice(&body).unwrap(); assert_eq!(json["result_count"], 0); assert_eq!(json["chunks"].as_array().unwrap().len(), 0); }