mirror of
https://github.com/perstarkse/minne.git
synced 2026-05-28 10:29:30 +02:00
chore: index slicing and lowercase errors
This commit is contained in:
@@ -204,8 +204,9 @@ impl SurrealDbClient {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::{self, Context};
|
||||
#![allow(clippy::expect_used, clippy::must_use_candidate)]
|
||||
use crate::stored_object;
|
||||
use anyhow::{self, Context};
|
||||
|
||||
use super::*;
|
||||
use uuid::Uuid;
|
||||
@@ -300,7 +301,8 @@ mod tests {
|
||||
.get_item(&dummy.id)
|
||||
.await
|
||||
.with_context(|| "fetch after upsert".to_string())?;
|
||||
let fetched = fetched.ok_or_else(|| anyhow::anyhow!("Expected record to exist after upsert"))?;
|
||||
let fetched =
|
||||
fetched.ok_or_else(|| anyhow::anyhow!("Expected record to exist after upsert"))?;
|
||||
assert_eq!(fetched.name, "updated");
|
||||
|
||||
let new_record = Dummy {
|
||||
|
||||
@@ -175,10 +175,7 @@ pub async fn rebuild(db: &SurrealDbClient) -> Result<(), AppError> {
|
||||
.map_err(|err| AppError::InternalError(err.to_string()))
|
||||
}
|
||||
|
||||
async fn ensure_runtime_inner(
|
||||
db: &SurrealDbClient,
|
||||
embedding_dimension: usize,
|
||||
) -> Result<()> {
|
||||
async fn ensure_runtime_inner(db: &SurrealDbClient, embedding_dimension: usize) -> Result<()> {
|
||||
create_fts_analyzer(db).await?;
|
||||
|
||||
for spec in fts_index_specs() {
|
||||
@@ -629,7 +626,10 @@ fn parse_index_build_info(
|
||||
if total == 0 {
|
||||
0.0
|
||||
} else {
|
||||
((f64::from(u32::try_from(processed).unwrap_or(u32::MAX)) / f64::from(u32::try_from(total).unwrap_or(1))).min(1.0)) * 100.0
|
||||
((f64::from(u32::try_from(processed).unwrap_or(u32::MAX))
|
||||
/ f64::from(u32::try_from(total).unwrap_or(1)))
|
||||
.min(1.0))
|
||||
* 100.0
|
||||
}
|
||||
});
|
||||
|
||||
@@ -698,8 +698,9 @@ async fn index_exists(db: &SurrealDbClient, table: &str, index_name: &str) -> Re
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::{self, Context};
|
||||
#![allow(clippy::expect_used, clippy::must_use_candidate)]
|
||||
use crate::storage::db::SurrealDbClient;
|
||||
use anyhow::{self, Context};
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -716,8 +717,7 @@ mod tests {
|
||||
}
|
||||
});
|
||||
|
||||
let snapshot = parse_index_build_info(Some(info), Some(61081))
|
||||
.context("snapshot")?;
|
||||
let snapshot = parse_index_build_info(Some(info), Some(61081)).context("snapshot")?;
|
||||
assert_eq!(
|
||||
snapshot,
|
||||
IndexBuildSnapshot {
|
||||
@@ -738,8 +738,7 @@ mod tests {
|
||||
fn parse_index_build_info_defaults_to_ready_when_no_building_block() -> anyhow::Result<()> {
|
||||
// Surreal returns `{}` when the index exists but isn't building.
|
||||
let info = json!({});
|
||||
let snapshot = parse_index_build_info(Some(info), Some(10))
|
||||
.context("snapshot")?;
|
||||
let snapshot = parse_index_build_info(Some(info), Some(10)).context("snapshot")?;
|
||||
assert!(snapshot.is_ready());
|
||||
assert_eq!(snapshot.processed, 0);
|
||||
assert_eq!(snapshot.progress_pct, Some(0.0));
|
||||
@@ -764,9 +763,11 @@ mod tests {
|
||||
.await
|
||||
.context("migrations should succeed")?;
|
||||
|
||||
ensure_runtime(&db, 1536).await
|
||||
ensure_runtime(&db, 1536)
|
||||
.await
|
||||
.context("first call should succeed")?;
|
||||
ensure_runtime(&db, 1536).await
|
||||
ensure_runtime(&db, 1536)
|
||||
.await
|
||||
.context("second index creation")?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -783,9 +784,11 @@ mod tests {
|
||||
.await
|
||||
.context("migrations should succeed")?;
|
||||
|
||||
ensure_runtime(&db, 1536).await
|
||||
ensure_runtime(&db, 1536)
|
||||
.await
|
||||
.context("initial index creation")?;
|
||||
ensure_runtime(&db, 128).await
|
||||
ensure_runtime(&db, 128)
|
||||
.await
|
||||
.context("overwritten index creation")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+115
-35
@@ -275,6 +275,7 @@ async fn create_storage_backend(
|
||||
/// automatic memory backend setup and proper test isolation.
|
||||
#[cfg(test)]
|
||||
pub mod testing {
|
||||
#![allow(clippy::expect_used, clippy::must_use_candidate)]
|
||||
use super::*;
|
||||
use crate::utils::config::{AppConfig, PdfIngestMode};
|
||||
use uuid;
|
||||
@@ -450,10 +451,7 @@ pub mod testing {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
storage,
|
||||
temp_dir,
|
||||
})
|
||||
Ok(Self { storage, temp_dir })
|
||||
}
|
||||
|
||||
/// Get a reference to the underlying StorageManager.
|
||||
@@ -581,9 +579,10 @@ pub fn split_object_path(path: &str) -> AnyResult<(String, String)> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::must_use_candidate)]
|
||||
use super::*;
|
||||
use anyhow::Context;
|
||||
use crate::utils::config::{PdfIngestMode::LlmFirst, StorageKind};
|
||||
use anyhow::Context;
|
||||
use bytes::Bytes;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -637,14 +636,23 @@ mod tests {
|
||||
.put(location, Bytes::from(data.to_vec()))
|
||||
.await
|
||||
.with_context(|| "put".to_string())?;
|
||||
let retrieved = storage.get(location).await.with_context(|| "get".to_string())?;
|
||||
let retrieved = storage
|
||||
.get(location)
|
||||
.await
|
||||
.with_context(|| "get".to_string())?;
|
||||
assert_eq!(retrieved.as_ref(), data);
|
||||
|
||||
// Test exists
|
||||
assert!(storage.exists(location).await.with_context(|| "exists check".to_string())?);
|
||||
assert!(storage
|
||||
.exists(location)
|
||||
.await
|
||||
.with_context(|| "exists check".to_string())?);
|
||||
|
||||
// Test delete
|
||||
storage.delete_prefix("test/data/").await.with_context(|| "delete".to_string())?;
|
||||
storage
|
||||
.delete_prefix("test/data/")
|
||||
.await
|
||||
.with_context(|| "delete".to_string())?;
|
||||
assert!(!storage
|
||||
.exists(location)
|
||||
.await
|
||||
@@ -674,7 +682,10 @@ mod tests {
|
||||
.put(location, Bytes::from(data.to_vec()))
|
||||
.await
|
||||
.with_context(|| "put".to_string())?;
|
||||
let retrieved = storage.get(location).await.with_context(|| "get".to_string())?;
|
||||
let retrieved = storage
|
||||
.get(location)
|
||||
.await
|
||||
.with_context(|| "get".to_string())?;
|
||||
assert_eq!(retrieved.as_ref(), data);
|
||||
|
||||
let object_dir = resolved_base.join("test/data");
|
||||
@@ -683,10 +694,16 @@ mod tests {
|
||||
.with_context(|| "object directory exists after write".to_string())?;
|
||||
|
||||
// Test exists
|
||||
assert!(storage.exists(location).await.with_context(|| "exists check".to_string())?);
|
||||
assert!(storage
|
||||
.exists(location)
|
||||
.await
|
||||
.with_context(|| "exists check".to_string())?);
|
||||
|
||||
// Test delete
|
||||
storage.delete_prefix("test/data/").await.with_context(|| "delete".to_string())?;
|
||||
storage
|
||||
.delete_prefix("test/data/")
|
||||
.await
|
||||
.with_context(|| "delete".to_string())?;
|
||||
assert!(!storage
|
||||
.exists(location)
|
||||
.await
|
||||
@@ -723,7 +740,10 @@ mod tests {
|
||||
.with_context(|| "put first".to_string())?;
|
||||
|
||||
// Retrieve and verify first data
|
||||
let retrieved1 = storage.get(location).await.with_context(|| "get first".to_string())?;
|
||||
let retrieved1 = storage
|
||||
.get(location)
|
||||
.await
|
||||
.with_context(|| "get first".to_string())?;
|
||||
assert_eq!(retrieved1.as_ref(), data1);
|
||||
|
||||
// Overwrite with second data
|
||||
@@ -733,7 +753,10 @@ mod tests {
|
||||
.with_context(|| "put second".to_string())?;
|
||||
|
||||
// Retrieve and verify second data
|
||||
let retrieved2 = storage.get(location).await.with_context(|| "get second".to_string())?;
|
||||
let retrieved2 = storage
|
||||
.get(location)
|
||||
.await
|
||||
.with_context(|| "get second".to_string())?;
|
||||
assert_eq!(retrieved2.as_ref(), data2);
|
||||
|
||||
// Data persists across multiple operations using the same StorageManager
|
||||
@@ -764,11 +787,17 @@ mod tests {
|
||||
}
|
||||
|
||||
// Test listing without prefix
|
||||
let all_files = storage.list(None).await.with_context(|| "list all".to_string())?;
|
||||
let all_files = storage
|
||||
.list(None)
|
||||
.await
|
||||
.with_context(|| "list all".to_string())?;
|
||||
assert_eq!(all_files.len(), 3);
|
||||
|
||||
// Test listing with prefix
|
||||
let dir1_files = storage.list(Some("dir1/")).await.with_context(|| "list dir1".to_string())?;
|
||||
let dir1_files = storage
|
||||
.list(Some("dir1/"))
|
||||
.await
|
||||
.with_context(|| "list dir1".to_string())?;
|
||||
assert_eq!(dir1_files.len(), 2);
|
||||
assert!(dir1_files
|
||||
.iter()
|
||||
@@ -804,7 +833,10 @@ mod tests {
|
||||
.with_context(|| "put large data".to_string())?;
|
||||
|
||||
// Get as stream
|
||||
let mut stream = storage.get_stream(location).await.with_context(|| "get stream".to_string())?;
|
||||
let mut stream = storage
|
||||
.get_stream(location)
|
||||
.await
|
||||
.with_context(|| "get stream".to_string())?;
|
||||
let mut collected = Vec::new();
|
||||
|
||||
while let Some(chunk) = stream.next().await {
|
||||
@@ -833,10 +865,16 @@ mod tests {
|
||||
.put(location, Bytes::from(data.to_vec()))
|
||||
.await
|
||||
.with_context(|| "put".to_string())?;
|
||||
let retrieved = storage.get(location).await.with_context(|| "get".to_string())?;
|
||||
let retrieved = storage
|
||||
.get(location)
|
||||
.await
|
||||
.with_context(|| "get".to_string())?;
|
||||
assert_eq!(retrieved.as_ref(), data);
|
||||
|
||||
assert!(storage.exists(location).await.with_context(|| "exists".to_string())?);
|
||||
assert!(storage
|
||||
.exists(location)
|
||||
.await
|
||||
.with_context(|| "exists".to_string())?);
|
||||
assert_eq!(*storage.backend_kind(), StorageKind::Memory);
|
||||
|
||||
Ok(())
|
||||
@@ -879,12 +917,21 @@ mod tests {
|
||||
let data = b"test data with TestStorageManager";
|
||||
|
||||
// Test put and get
|
||||
test_storage.put(location, data).await.with_context(|| "put".to_string())?;
|
||||
let retrieved = test_storage.get(location).await.with_context(|| "get".to_string())?;
|
||||
test_storage
|
||||
.put(location, data)
|
||||
.await
|
||||
.with_context(|| "put".to_string())?;
|
||||
let retrieved = test_storage
|
||||
.get(location)
|
||||
.await
|
||||
.with_context(|| "get".to_string())?;
|
||||
assert_eq!(retrieved.as_ref(), data);
|
||||
|
||||
// Test existence check
|
||||
assert!(test_storage.exists(location).await.with_context(|| "exists".to_string())?);
|
||||
assert!(test_storage
|
||||
.exists(location)
|
||||
.await
|
||||
.with_context(|| "exists".to_string())?);
|
||||
|
||||
// Test list
|
||||
let files = test_storage
|
||||
@@ -915,13 +962,19 @@ mod tests {
|
||||
let location = "test/local/file.txt";
|
||||
let data = b"test data with local TestStorageManager";
|
||||
|
||||
test_storage.put(location, data).await
|
||||
test_storage
|
||||
.put(location, data)
|
||||
.await
|
||||
.with_context(|| "put".to_string())?;
|
||||
let retrieved = test_storage.get(location).await
|
||||
let retrieved = test_storage
|
||||
.get(location)
|
||||
.await
|
||||
.with_context(|| "get".to_string())?;
|
||||
assert_eq!(retrieved.as_ref(), data);
|
||||
|
||||
assert!(test_storage.exists(location).await
|
||||
assert!(test_storage
|
||||
.exists(location)
|
||||
.await
|
||||
.with_context(|| "exists".to_string())?);
|
||||
|
||||
Ok(())
|
||||
@@ -940,14 +993,22 @@ mod tests {
|
||||
let data1 = b"storage 1 data";
|
||||
let data2 = b"storage 2 data";
|
||||
|
||||
storage1.put(location, data1).await
|
||||
storage1
|
||||
.put(location, data1)
|
||||
.await
|
||||
.with_context(|| "put storage 1".to_string())?;
|
||||
storage2.put(location, data2).await
|
||||
storage2
|
||||
.put(location, data2)
|
||||
.await
|
||||
.with_context(|| "put storage 2".to_string())?;
|
||||
|
||||
let retrieved1 = storage1.get(location).await
|
||||
let retrieved1 = storage1
|
||||
.get(location)
|
||||
.await
|
||||
.with_context(|| "get storage 1".to_string())?;
|
||||
let retrieved2 = storage2.get(location).await
|
||||
let retrieved2 = storage2
|
||||
.get(location)
|
||||
.await
|
||||
.with_context(|| "get storage 2".to_string())?;
|
||||
|
||||
assert_eq!(retrieved1.as_ref(), data1);
|
||||
@@ -967,9 +1028,13 @@ mod tests {
|
||||
let location = "config/test.txt";
|
||||
let data = b"test data with custom config";
|
||||
|
||||
test_storage.put(location, data).await
|
||||
test_storage
|
||||
.put(location, data)
|
||||
.await
|
||||
.with_context(|| "put".to_string())?;
|
||||
let retrieved = test_storage.get(location).await
|
||||
let retrieved = test_storage
|
||||
.get(location)
|
||||
.await
|
||||
.with_context(|| "get".to_string())?;
|
||||
assert_eq!(retrieved.as_ref(), data);
|
||||
|
||||
@@ -1000,11 +1065,17 @@ mod tests {
|
||||
}
|
||||
|
||||
// Test get
|
||||
let retrieved = storage.get(&location).await.with_context(|| "get".to_string())?;
|
||||
let retrieved = storage
|
||||
.get(&location)
|
||||
.await
|
||||
.with_context(|| "get".to_string())?;
|
||||
assert_eq!(retrieved.as_ref(), data);
|
||||
|
||||
// Test exists
|
||||
assert!(storage.exists(&location).await.with_context(|| "exists".to_string())?);
|
||||
assert!(storage
|
||||
.exists(&location)
|
||||
.await
|
||||
.with_context(|| "exists".to_string())?);
|
||||
|
||||
// Test delete
|
||||
storage
|
||||
@@ -1040,11 +1111,17 @@ mod tests {
|
||||
|
||||
// List with prefix
|
||||
let list_prefix = format!("{prefix}/");
|
||||
let items = storage.list(Some(&list_prefix)).await.with_context(|| "list".to_string())?;
|
||||
let items = storage
|
||||
.list(Some(&list_prefix))
|
||||
.await
|
||||
.with_context(|| "list".to_string())?;
|
||||
assert_eq!(items.len(), 3);
|
||||
|
||||
// Cleanup
|
||||
storage.delete_prefix(&list_prefix).await.with_context(|| "cleanup".to_string())?;
|
||||
storage
|
||||
.delete_prefix(&list_prefix)
|
||||
.await
|
||||
.with_context(|| "cleanup".to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1063,7 +1140,10 @@ mod tests {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut stream = storage.get_stream(&location).await.with_context(|| "get stream".to_string())?;
|
||||
let mut stream = storage
|
||||
.get_stream(&location)
|
||||
.await
|
||||
.with_context(|| "get stream".to_string())?;
|
||||
let mut collected = Vec::new();
|
||||
while let Some(chunk) = stream.next().await {
|
||||
collected.extend_from_slice(&chunk.with_context(|| "chunk".to_string())?);
|
||||
|
||||
@@ -88,6 +88,7 @@ impl Analytics {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::must_use_candidate)]
|
||||
use super::*;
|
||||
use crate::stored_object;
|
||||
use anyhow::{self};
|
||||
|
||||
@@ -11,6 +11,7 @@ stored_object!(Conversation, "conversation", {
|
||||
});
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
pub struct SidebarConversation {
|
||||
#[serde(deserialize_with = "deserialize_sidebar_id")]
|
||||
pub id: String,
|
||||
@@ -144,8 +145,9 @@ impl Conversation {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::{self, Context};
|
||||
#![allow(clippy::expect_used, clippy::must_use_candidate)]
|
||||
use crate::storage::types::message::MessageRole;
|
||||
use anyhow::{self, Context};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -173,7 +175,8 @@ mod tests {
|
||||
.await
|
||||
.with_context(|| "Failed to retrieve conversation".to_string())?;
|
||||
|
||||
let retrieved = retrieved.ok_or_else(|| anyhow::anyhow!("Expected conversation to exist"))?;
|
||||
let retrieved =
|
||||
retrieved.ok_or_else(|| anyhow::anyhow!("Expected conversation to exist"))?;
|
||||
assert_eq!(retrieved.id, conversation.id);
|
||||
assert_eq!(retrieved.user_id, user_id);
|
||||
assert_eq!(retrieved.title, title);
|
||||
@@ -354,12 +357,15 @@ mod tests {
|
||||
.expect("Failed to get sidebar conversations");
|
||||
|
||||
assert_eq!(sidebar_items.len(), 3);
|
||||
assert_eq!(sidebar_items[0].id, newest.id);
|
||||
assert_eq!(sidebar_items[0].title, "Newest");
|
||||
assert_eq!(sidebar_items[1].id, middle.id);
|
||||
assert_eq!(sidebar_items[1].title, "Middle");
|
||||
assert_eq!(sidebar_items[2].id, oldest.id);
|
||||
assert_eq!(sidebar_items[2].title, "Oldest");
|
||||
let s0 = sidebar_items.first().expect("expected 3 items");
|
||||
let s1 = sidebar_items.get(1).expect("expected 3 items");
|
||||
let s2 = sidebar_items.get(2).expect("expected 3 items");
|
||||
assert_eq!(s0.id, newest.id);
|
||||
assert_eq!(s0.title, "Newest");
|
||||
assert_eq!(s1.id, middle.id);
|
||||
assert_eq!(s1.title, "Middle");
|
||||
assert_eq!(s2.id, oldest.id);
|
||||
assert_eq!(s2.title, "Oldest");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -389,7 +395,8 @@ mod tests {
|
||||
let before_patch = Conversation::get_user_sidebar_conversations(user_id, &db)
|
||||
.await
|
||||
.expect("Failed to get sidebar conversations before patch");
|
||||
assert_eq!(before_patch[0].id, second.id);
|
||||
let before = before_patch.first().expect("expected at least 1 item");
|
||||
assert_eq!(before.id, second.id);
|
||||
|
||||
Conversation::patch_title(&first.id, user_id, "First (renamed)", &db)
|
||||
.await
|
||||
@@ -398,8 +405,9 @@ mod tests {
|
||||
let after_patch = Conversation::get_user_sidebar_conversations(user_id, &db)
|
||||
.await
|
||||
.expect("Failed to get sidebar conversations after patch");
|
||||
assert_eq!(after_patch[0].id, first.id);
|
||||
assert_eq!(after_patch[0].title, "First (renamed)");
|
||||
let after = after_patch.first().expect("expected at least 1 item");
|
||||
assert_eq!(after.id, first.id);
|
||||
assert_eq!(after.title, "First (renamed)");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -451,8 +459,8 @@ mod tests {
|
||||
Conversation::get_complete_conversation(&conversation_id, user_id_1, &db).await;
|
||||
assert!(result.is_ok(), "Failed to retrieve complete conversation");
|
||||
|
||||
let (retrieved_conversation, retrieved_messages) = result
|
||||
.with_context(|| "Failed to retrieve complete conversation".to_string())?;
|
||||
let (retrieved_conversation, retrieved_messages) =
|
||||
result.with_context(|| "Failed to retrieve complete conversation".to_string())?;
|
||||
|
||||
assert_eq!(retrieved_conversation.id, conversation_id);
|
||||
assert_eq!(retrieved_conversation.user_id, user_id_1);
|
||||
@@ -460,8 +468,10 @@ mod tests {
|
||||
|
||||
assert_eq!(retrieved_messages.len(), 3);
|
||||
|
||||
let message_contents: Vec<&str> =
|
||||
retrieved_messages.iter().map(|m| m.content.as_str()).collect();
|
||||
let message_contents: Vec<&str> = retrieved_messages
|
||||
.iter()
|
||||
.map(|m| m.content.as_str())
|
||||
.collect();
|
||||
assert!(message_contents.contains(&"Hello, AI!"));
|
||||
assert!(message_contents.contains(&"Hello, human! How can I help you today?"));
|
||||
assert!(message_contents.contains(&"Tell me about Rust programming."));
|
||||
|
||||
@@ -320,6 +320,7 @@ impl FileInfo {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::must_use_candidate)]
|
||||
use anyhow::{self, Context};
|
||||
|
||||
use super::*;
|
||||
@@ -330,8 +331,12 @@ mod tests {
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
/// Creates a test temporary file with the given content
|
||||
fn create_test_file(content: &[u8], file_name: &str) -> anyhow::Result<FieldData<NamedTempFile>> {
|
||||
let mut temp_file = NamedTempFile::new().with_context(|| "Failed to create temp file".to_string())?;
|
||||
fn create_test_file(
|
||||
content: &[u8],
|
||||
file_name: &str,
|
||||
) -> anyhow::Result<FieldData<NamedTempFile>> {
|
||||
let mut temp_file =
|
||||
NamedTempFile::new().with_context(|| "Failed to create temp file".to_string())?;
|
||||
temp_file
|
||||
.write_all(content)
|
||||
.with_context(|| "Failed to write to temp file".to_string())?;
|
||||
@@ -356,7 +361,9 @@ mod tests {
|
||||
let db = SurrealDbClient::memory(namespace, database)
|
||||
.await
|
||||
.with_context(|| "Failed to start in-memory surrealdb".to_string())?;
|
||||
db.apply_migrations().await.with_context(|| "migrations".to_string())?;
|
||||
db.apply_migrations()
|
||||
.await
|
||||
.with_context(|| "migrations".to_string())?;
|
||||
|
||||
let content = b"This is a test file for StorageManager operations";
|
||||
let file_name = "storage_manager_test.txt";
|
||||
@@ -411,7 +418,9 @@ mod tests {
|
||||
let db = SurrealDbClient::memory(namespace, database)
|
||||
.await
|
||||
.with_context(|| "Failed to start in-memory surrealdb".to_string())?;
|
||||
db.apply_migrations().await.with_context(|| "migrations".to_string())?;
|
||||
db.apply_migrations()
|
||||
.await
|
||||
.with_context(|| "migrations".to_string())?;
|
||||
|
||||
let content = b"filename sanitization";
|
||||
let original_name = "Complex name (1).txt";
|
||||
@@ -444,7 +453,9 @@ mod tests {
|
||||
let db = SurrealDbClient::memory(namespace, database)
|
||||
.await
|
||||
.with_context(|| "Failed to start in-memory surrealdb".to_string())?;
|
||||
db.apply_migrations().await.with_context(|| "migrations".to_string())?;
|
||||
db.apply_migrations()
|
||||
.await
|
||||
.with_context(|| "migrations".to_string())?;
|
||||
|
||||
let content = b"This is a test file for StorageManager duplicate detection";
|
||||
let file_name = "storage_manager_duplicate.txt";
|
||||
@@ -462,7 +473,9 @@ mod tests {
|
||||
let original_file_info =
|
||||
FileInfo::new_with_storage(field_data, &db, user_id, test_storage.storage())
|
||||
.await
|
||||
.with_context(|| "Failed to create original file with StorageManager".to_string())?;
|
||||
.with_context(|| {
|
||||
"Failed to create original file with StorageManager".to_string()
|
||||
})?;
|
||||
|
||||
// Create another file with the same content but different name
|
||||
let duplicate_name = "storage_manager_duplicate_2.txt";
|
||||
@@ -472,7 +485,9 @@ mod tests {
|
||||
let duplicate_file_info =
|
||||
FileInfo::new_with_storage(field_data2, &db, user_id, test_storage.storage())
|
||||
.await
|
||||
.with_context(|| "Failed to process duplicate file with StorageManager".to_string())?;
|
||||
.with_context(|| {
|
||||
"Failed to process duplicate file with StorageManager".to_string()
|
||||
})?;
|
||||
|
||||
// Verify duplicate detection worked
|
||||
assert_eq!(duplicate_file_info.id, original_file_info.id);
|
||||
@@ -520,8 +535,7 @@ mod tests {
|
||||
.await
|
||||
.with_context(|| "create test storage manager".to_string())?;
|
||||
let file_info =
|
||||
FileInfo::new_with_storage(field_data, &db, user_id, test_storage.storage())
|
||||
.await?;
|
||||
FileInfo::new_with_storage(field_data, &db, user_id, test_storage.storage()).await?;
|
||||
|
||||
// Check essential properties
|
||||
assert!(!file_info.id.is_empty());
|
||||
@@ -876,7 +890,9 @@ mod tests {
|
||||
let db = SurrealDbClient::memory("test_ns", "test_file_storage_memory")
|
||||
.await
|
||||
.with_context(|| "Failed to start DB".to_string())?;
|
||||
db.apply_migrations().await.with_context(|| "migrations".to_string())?;
|
||||
db.apply_migrations()
|
||||
.await
|
||||
.with_context(|| "migrations".to_string())?;
|
||||
|
||||
let content = b"This is a test file for StorageManager";
|
||||
let field_data = create_test_file(content, "test_storage.txt")?;
|
||||
@@ -922,7 +938,9 @@ mod tests {
|
||||
let db = SurrealDbClient::memory("test_ns", "test_file_storage_local")
|
||||
.await
|
||||
.with_context(|| "Failed to start DB".to_string())?;
|
||||
db.apply_migrations().await.with_context(|| "migrations".to_string())?;
|
||||
db.apply_migrations()
|
||||
.await
|
||||
.with_context(|| "migrations".to_string())?;
|
||||
|
||||
let content = b"This is a test file for StorageManager with local storage";
|
||||
let field_data = create_test_file(content, "test_local.txt")?;
|
||||
@@ -968,7 +986,9 @@ mod tests {
|
||||
let db = SurrealDbClient::memory("test_ns", "test_file_persistence")
|
||||
.await
|
||||
.with_context(|| "Failed to start DB".to_string())?;
|
||||
db.apply_migrations().await.with_context(|| "migrations".to_string())?;
|
||||
db.apply_migrations()
|
||||
.await
|
||||
.with_context(|| "migrations".to_string())?;
|
||||
|
||||
let content = b"Test content for persistence";
|
||||
let field_data = create_test_file(content, "persistence_test.txt")?;
|
||||
@@ -1015,7 +1035,9 @@ mod tests {
|
||||
let db = SurrealDbClient::memory("test_ns", "test_file_equivalence")
|
||||
.await
|
||||
.with_context(|| "Failed to start DB".to_string())?;
|
||||
db.apply_migrations().await.with_context(|| "migrations".to_string())?;
|
||||
db.apply_migrations()
|
||||
.await
|
||||
.with_context(|| "migrations".to_string())?;
|
||||
|
||||
let content = b"Test content for equivalence testing";
|
||||
let field_data1 = create_test_file(content, "equivalence_test_1.txt")?;
|
||||
|
||||
@@ -103,6 +103,7 @@ impl IngestionPayload {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::must_use_candidate)]
|
||||
use anyhow::{self, Context};
|
||||
use chrono::Utc;
|
||||
|
||||
|
||||
@@ -247,11 +247,9 @@ impl IngestionTask {
|
||||
"#;
|
||||
|
||||
debug_assert!(lifecycle::pending().reserve().is_ok());
|
||||
debug_assert!(
|
||||
lifecycle::pending()
|
||||
.reserve()
|
||||
.is_ok_and(|m| m.start_processing().is_ok_and(|m| m.fail().is_ok_and(|m| m.reserve().is_ok())))
|
||||
);
|
||||
debug_assert!(lifecycle::pending().reserve().is_ok_and(|m| m
|
||||
.start_processing()
|
||||
.is_ok_and(|m| m.fail().is_ok_and(|m| m.reserve().is_ok()))));
|
||||
|
||||
let mut result = db
|
||||
.client
|
||||
@@ -277,7 +275,10 @@ impl IngestionTask {
|
||||
.bind(("reserved_state", TaskState::Reserved.as_str()))
|
||||
.bind(("now", SurrealDatetime::from(now)))
|
||||
.bind(("worker_id", worker_id.to_string()))
|
||||
.bind(("lease_secs", i64::try_from(lease_duration.as_secs()).unwrap_or(i64::MAX)))
|
||||
.bind((
|
||||
"lease_secs",
|
||||
i64::try_from(lease_duration.as_secs()).unwrap_or(i64::MAX),
|
||||
))
|
||||
.await?;
|
||||
|
||||
let task: Option<IngestionTask> = result.take(0)?;
|
||||
@@ -364,7 +365,8 @@ impl IngestionTask {
|
||||
let now = chrono::Utc::now();
|
||||
let retry_at = now
|
||||
.checked_add_signed(
|
||||
ChronoDuration::from_std(retry_delay).unwrap_or_else(|_| ChronoDuration::seconds(30)),
|
||||
ChronoDuration::from_std(retry_delay)
|
||||
.unwrap_or_else(|_| ChronoDuration::seconds(30)),
|
||||
)
|
||||
.unwrap_or(now);
|
||||
|
||||
@@ -509,6 +511,7 @@ impl IngestionTask {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::must_use_candidate)]
|
||||
use anyhow::{self, Context};
|
||||
|
||||
use super::*;
|
||||
@@ -576,11 +579,13 @@ mod tests {
|
||||
let user_id = "user123";
|
||||
let payload = create_payload(user_id);
|
||||
let task = IngestionTask::new(payload, user_id.to_string());
|
||||
db.store_item(task.clone()).await.with_context(|| "store".to_string())?;
|
||||
db.store_item(task.clone())
|
||||
.await
|
||||
.with_context(|| "store".to_string())?;
|
||||
|
||||
let worker_id = "worker-1";
|
||||
let now = chrono::Utc::now();
|
||||
let claimed = IngestionTask::claim_next_ready(&db, worker_id, now, Duration::from_secs(60))
|
||||
let claimed = IngestionTask::claim_next_ready(&db, worker_id, now, Duration::from_mins(1))
|
||||
.await
|
||||
.with_context(|| "claim".to_string())?
|
||||
.with_context(|| "task claimed".to_string())?;
|
||||
@@ -588,10 +593,16 @@ mod tests {
|
||||
assert_eq!(claimed.state, TaskState::Reserved);
|
||||
assert_eq!(claimed.worker_id.as_deref(), Some(worker_id));
|
||||
|
||||
let processing = claimed.mark_processing(&db).await.with_context(|| "processing".to_string())?;
|
||||
let processing = claimed
|
||||
.mark_processing(&db)
|
||||
.await
|
||||
.with_context(|| "processing".to_string())?;
|
||||
assert_eq!(processing.state, TaskState::Processing);
|
||||
|
||||
let succeeded = processing.mark_succeeded(&db).await.with_context(|| "succeeded".to_string())?;
|
||||
let succeeded = processing
|
||||
.mark_succeeded(&db)
|
||||
.await
|
||||
.with_context(|| "succeeded".to_string())?;
|
||||
assert_eq!(succeeded.state, TaskState::Succeeded);
|
||||
assert!(succeeded.worker_id.is_none());
|
||||
assert!(succeeded.locked_at.is_none());
|
||||
@@ -604,16 +615,21 @@ mod tests {
|
||||
let user_id = "user123";
|
||||
let payload = create_payload(user_id);
|
||||
let task = IngestionTask::new(payload, user_id.to_string());
|
||||
db.store_item(task.clone()).await.with_context(|| "store".to_string())?;
|
||||
db.store_item(task.clone())
|
||||
.await
|
||||
.with_context(|| "store".to_string())?;
|
||||
|
||||
let worker_id = "worker-dead";
|
||||
let now = chrono::Utc::now();
|
||||
let claimed = IngestionTask::claim_next_ready(&db, worker_id, now, Duration::from_secs(60))
|
||||
let claimed = IngestionTask::claim_next_ready(&db, worker_id, now, Duration::from_mins(1))
|
||||
.await
|
||||
.with_context(|| "claim".to_string())?
|
||||
.with_context(|| "claimed".to_string())?;
|
||||
|
||||
let processing = claimed.mark_processing(&db).await.with_context(|| "processing".to_string())?;
|
||||
let processing = claimed
|
||||
.mark_processing(&db)
|
||||
.await
|
||||
.with_context(|| "processing".to_string())?;
|
||||
|
||||
let error_info = TaskErrorInfo {
|
||||
code: Some("pipeline_error".into()),
|
||||
@@ -646,11 +662,13 @@ mod tests {
|
||||
let payload = create_payload(user_id);
|
||||
|
||||
let task = IngestionTask::new(payload.clone(), user_id.to_string());
|
||||
db.store_item(task.clone()).await.with_context(|| "store".to_string())?;
|
||||
db.store_item(task.clone())
|
||||
.await
|
||||
.with_context(|| "store".to_string())?;
|
||||
|
||||
let Err(err) = task
|
||||
.mark_processing(&db)
|
||||
.await else { anyhow::bail!("processing should fail without reservation") };
|
||||
let Err(err) = task.mark_processing(&db).await else {
|
||||
anyhow::bail!("processing should fail without reservation")
|
||||
};
|
||||
|
||||
match err {
|
||||
AppError::Validation(message) => {
|
||||
@@ -671,7 +689,9 @@ mod tests {
|
||||
let payload = create_payload(user_id);
|
||||
|
||||
let task = IngestionTask::new(payload.clone(), user_id.to_string());
|
||||
db.store_item(task.clone()).await.with_context(|| "store".to_string())?;
|
||||
db.store_item(task.clone())
|
||||
.await
|
||||
.with_context(|| "store".to_string())?;
|
||||
|
||||
let Err(err) = task
|
||||
.mark_failed(
|
||||
@@ -682,7 +702,10 @@ mod tests {
|
||||
Duration::from_secs(30),
|
||||
&db,
|
||||
)
|
||||
.await else { anyhow::bail!("failing should require processing state") };
|
||||
.await
|
||||
else {
|
||||
anyhow::bail!("failing should require processing state")
|
||||
};
|
||||
|
||||
match err {
|
||||
AppError::Validation(message) => {
|
||||
@@ -703,11 +726,13 @@ mod tests {
|
||||
let payload = create_payload(user_id);
|
||||
|
||||
let task = IngestionTask::new(payload.clone(), user_id.to_string());
|
||||
db.store_item(task.clone()).await.with_context(|| "store".to_string())?;
|
||||
db.store_item(task.clone())
|
||||
.await
|
||||
.with_context(|| "store".to_string())?;
|
||||
|
||||
let Err(err) = task
|
||||
.release(&db)
|
||||
.await else { anyhow::bail!("release should require reserved state") };
|
||||
let Err(err) = task.release(&db).await else {
|
||||
anyhow::bail!("release should require reserved state")
|
||||
};
|
||||
|
||||
match err {
|
||||
AppError::Validation(message) => {
|
||||
|
||||
@@ -572,9 +572,10 @@ impl KnowledgeEntity {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::{self, Context};
|
||||
#![allow(clippy::expect_used, clippy::must_use_candidate)]
|
||||
use super::*;
|
||||
use crate::storage::types::knowledge_entity_embedding::KnowledgeEntityEmbedding;
|
||||
use anyhow::{self, Context};
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -734,7 +735,10 @@ mod tests {
|
||||
.with_context(|| "Query failed".to_string())?
|
||||
.take(0)
|
||||
.with_context(|| "Failed to get query results".to_string())?;
|
||||
assert!(remaining.is_empty(), "All entities with the source_id should be deleted");
|
||||
assert!(
|
||||
remaining.is_empty(),
|
||||
"All entities with the source_id should be deleted"
|
||||
);
|
||||
|
||||
let different_query = format!(
|
||||
"SELECT * FROM {} WHERE source_id = '{}'",
|
||||
@@ -754,7 +758,10 @@ mod tests {
|
||||
"Entity with different source_id should still exist"
|
||||
);
|
||||
assert_eq!(
|
||||
different_remaining.first().context("Expected entity to exist")?.id,
|
||||
different_remaining
|
||||
.first()
|
||||
.context("Expected entity to exist")?
|
||||
.id,
|
||||
different_entity.id
|
||||
);
|
||||
|
||||
@@ -991,11 +998,19 @@ mod tests {
|
||||
|
||||
assert_eq!(results.len(), 2);
|
||||
assert_eq!(
|
||||
results.first().context("Expected at least one result")?.entity.id,
|
||||
results
|
||||
.first()
|
||||
.context("Expected at least one result")?
|
||||
.entity
|
||||
.id,
|
||||
e2.id
|
||||
);
|
||||
assert_eq!(
|
||||
results.get(1).context("Expected at least two results")?.entity.id,
|
||||
results
|
||||
.get(1)
|
||||
.context("Expected at least two results")?
|
||||
.entity
|
||||
.id,
|
||||
e1.id
|
||||
);
|
||||
|
||||
@@ -1042,7 +1057,11 @@ mod tests {
|
||||
.await
|
||||
.with_context(|| "search should succeed even with orphans".to_string())?;
|
||||
|
||||
assert!(results.is_empty(), "Should return empty result for orphan, got: {:?}", results);
|
||||
assert!(
|
||||
results.is_empty(),
|
||||
"Should return empty result for orphan, got: {:?}",
|
||||
results
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -137,10 +137,11 @@ impl KnowledgeEntityEmbedding {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::{self, Context};
|
||||
#![allow(clippy::expect_used, clippy::must_use_candidate)]
|
||||
use super::*;
|
||||
use crate::storage::db::SurrealDbClient;
|
||||
use crate::storage::types::knowledge_entity::{KnowledgeEntity, KnowledgeEntityType};
|
||||
use anyhow::{self, Context};
|
||||
use chrono::Utc;
|
||||
use surrealdb::Value as SurrealValue;
|
||||
use uuid::Uuid;
|
||||
@@ -270,7 +271,8 @@ mod tests {
|
||||
let stored_embedding = KnowledgeEntityEmbedding::get_by_entity_id(&entity_rid, &db)
|
||||
.await
|
||||
.with_context(|| "Failed to fetch embedding".to_string())?;
|
||||
let stored_embedding = stored_embedding.ok_or_else(|| anyhow::anyhow!("Expected embedding to exist"))?;
|
||||
let stored_embedding =
|
||||
stored_embedding.ok_or_else(|| anyhow::anyhow!("Expected embedding to exist"))?;
|
||||
assert_eq!(stored_embedding.user_id, user_id);
|
||||
assert_eq!(stored_embedding.entity_id, entity_rid);
|
||||
|
||||
@@ -399,7 +401,10 @@ mod tests {
|
||||
.with_context(|| "failed to deserialize fetch rows".to_string())?;
|
||||
|
||||
assert_eq!(rows.len(), 1);
|
||||
let fetched_entity = &rows.first().context("Expected at least one result")?.entity_id;
|
||||
let fetched_entity = &rows
|
||||
.first()
|
||||
.context("Expected at least one result")?
|
||||
.entity_id;
|
||||
assert_eq!(fetched_entity.id, entity_key);
|
||||
assert_eq!(fetched_entity.name, "Test entity");
|
||||
assert_eq!(fetched_entity.user_id, user_id);
|
||||
|
||||
@@ -124,9 +124,10 @@ impl KnowledgeRelationship {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::{self, Context};
|
||||
#![allow(clippy::expect_used, clippy::must_use_candidate)]
|
||||
use super::*;
|
||||
use crate::storage::types::knowledge_entity::{KnowledgeEntity, KnowledgeEntityType};
|
||||
use anyhow::{self, Context};
|
||||
|
||||
async fn setup_test_db() -> SurrealDbClient {
|
||||
let namespace = "test_ns";
|
||||
@@ -283,8 +284,9 @@ mod tests {
|
||||
let rows: Vec<KnowledgeRelationship> = res.take(0).expect("take rows");
|
||||
|
||||
assert_eq!(rows.len(), 1);
|
||||
let row = rows.first().expect("expected 1 row");
|
||||
assert_eq!(
|
||||
rows[0].metadata.source_id,
|
||||
row.metadata.source_id,
|
||||
"source123'; DELETE FROM relates_to; --"
|
||||
);
|
||||
|
||||
@@ -323,7 +325,10 @@ mod tests {
|
||||
.with_context(|| "Query failed".to_string())?;
|
||||
let before_results: Vec<KnowledgeRelationship> =
|
||||
existing_before_delete.take(0).unwrap_or_default();
|
||||
assert!(!before_results.is_empty(), "Relationship should exist before deletion");
|
||||
assert!(
|
||||
!before_results.is_empty(),
|
||||
"Relationship should exist before deletion"
|
||||
);
|
||||
|
||||
KnowledgeRelationship::delete_relationship_by_id(&relationship.id, &user_id, &db)
|
||||
.await
|
||||
@@ -372,7 +377,10 @@ mod tests {
|
||||
.await
|
||||
.with_context(|| "Query failed".to_string())?;
|
||||
let before_results: Vec<KnowledgeRelationship> = before_attempt.take(0).unwrap_or_default();
|
||||
assert!(!before_results.is_empty(), "Relationship should exist before unauthorized delete attempt");
|
||||
assert!(
|
||||
!before_results.is_empty(),
|
||||
"Relationship should exist before unauthorized delete attempt"
|
||||
);
|
||||
|
||||
let result = KnowledgeRelationship::delete_relationship_by_id(
|
||||
&relationship.id,
|
||||
@@ -383,7 +391,9 @@ mod tests {
|
||||
|
||||
match result {
|
||||
Err(AppError::Auth(_)) => {}
|
||||
_ => anyhow::bail!("Expected authorization error when deleting someone else's relationship"),
|
||||
_ => anyhow::bail!(
|
||||
"Expected authorization error when deleting someone else's relationship"
|
||||
),
|
||||
}
|
||||
|
||||
let mut after_attempt = db
|
||||
@@ -394,7 +404,10 @@ mod tests {
|
||||
.with_context(|| "Query failed".to_string())?;
|
||||
let results: Vec<KnowledgeRelationship> = after_attempt.take(0).unwrap_or_default();
|
||||
|
||||
assert!(!results.is_empty(), "Relationship should still exist after unauthorized delete attempt");
|
||||
assert!(
|
||||
!results.is_empty(),
|
||||
"Relationship should still exist after unauthorized delete attempt"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -484,7 +497,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_relationships_by_source_id_resists_query_injection() -> anyhow::Result<()> {
|
||||
async fn test_delete_relationships_by_source_id_resists_query_injection() -> anyhow::Result<()>
|
||||
{
|
||||
let db = setup_test_db().await;
|
||||
|
||||
let entity1_id = create_test_entity("Entity 1", &db).await?;
|
||||
|
||||
@@ -70,9 +70,10 @@ pub fn format_history(history: &[Message]) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::{self, Context};
|
||||
#![allow(clippy::expect_used, clippy::must_use_candidate)]
|
||||
use super::*;
|
||||
use crate::storage::db::SurrealDbClient;
|
||||
use anyhow::{self, Context};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_message_creation() -> anyhow::Result<()> {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#![allow(clippy::unsafe_derive_deserialize)]
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub mod serde_helpers;
|
||||
pub mod analytics;
|
||||
pub mod conversation;
|
||||
pub mod file_info;
|
||||
@@ -11,6 +10,7 @@ pub mod knowledge_entity_embedding;
|
||||
pub mod knowledge_relationship;
|
||||
pub mod message;
|
||||
pub mod scratchpad;
|
||||
pub mod serde_helpers;
|
||||
pub mod system_prompts;
|
||||
pub mod system_settings;
|
||||
pub mod text_chunk;
|
||||
|
||||
@@ -216,6 +216,7 @@ impl Scratchpad {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::must_use_candidate)]
|
||||
use anyhow::{self, Context};
|
||||
|
||||
use super::*;
|
||||
@@ -289,9 +290,15 @@ mod tests {
|
||||
// Store them
|
||||
let scratchpad1_id = scratchpad1.id.clone();
|
||||
let scratchpad2_id = scratchpad2.id.clone();
|
||||
db.store_item(scratchpad1).await.with_context(|| "store scratchpad1".to_string())?;
|
||||
db.store_item(scratchpad2).await.with_context(|| "store scratchpad2".to_string())?;
|
||||
db.store_item(scratchpad3).await.with_context(|| "store scratchpad3".to_string())?;
|
||||
db.store_item(scratchpad1)
|
||||
.await
|
||||
.with_context(|| "store scratchpad1".to_string())?;
|
||||
db.store_item(scratchpad2)
|
||||
.await
|
||||
.with_context(|| "store scratchpad2".to_string())?;
|
||||
db.store_item(scratchpad3)
|
||||
.await
|
||||
.with_context(|| "store scratchpad3".to_string())?;
|
||||
|
||||
// Archive one of the user's scratchpads
|
||||
Scratchpad::archive(&scratchpad2_id, user_id, &db, false)
|
||||
@@ -303,7 +310,10 @@ mod tests {
|
||||
.await
|
||||
.with_context(|| "get_by_user".to_string())?;
|
||||
assert_eq!(user_scratchpads.len(), 1);
|
||||
assert_eq!(user_scratchpads.first().map(|s| &s.id), Some(&scratchpad1_id));
|
||||
assert_eq!(
|
||||
user_scratchpads.first().map(|s| &s.id),
|
||||
Some(&scratchpad1_id)
|
||||
);
|
||||
|
||||
// Verify they belong to the user
|
||||
for scratchpad in &user_scratchpads {
|
||||
@@ -335,7 +345,9 @@ mod tests {
|
||||
let user_id = "test_user";
|
||||
let scratchpad = Scratchpad::new(user_id.to_string(), "Test".to_string());
|
||||
let scratchpad_id = scratchpad.id.clone();
|
||||
db.store_item(scratchpad).await.with_context(|| "store scratchpad".to_string())?;
|
||||
db.store_item(scratchpad)
|
||||
.await
|
||||
.with_context(|| "store scratchpad".to_string())?;
|
||||
|
||||
let archived = Scratchpad::archive(&scratchpad_id, user_id, &db, true)
|
||||
.await
|
||||
@@ -369,7 +381,9 @@ mod tests {
|
||||
let scratchpad = Scratchpad::new(user_id.to_string(), "Test".to_string());
|
||||
let scratchpad_id = scratchpad.id.clone();
|
||||
|
||||
db.store_item(scratchpad).await.with_context(|| "store scratchpad".to_string())?;
|
||||
db.store_item(scratchpad)
|
||||
.await
|
||||
.with_context(|| "store scratchpad".to_string())?;
|
||||
|
||||
let new_content = "Updated content";
|
||||
let updated = Scratchpad::update_content(&scratchpad_id, user_id, new_content, &db)
|
||||
@@ -398,7 +412,9 @@ mod tests {
|
||||
let scratchpad = Scratchpad::new(owner_id.to_string(), "Test".to_string());
|
||||
let scratchpad_id = scratchpad.id.clone();
|
||||
|
||||
db.store_item(scratchpad).await.with_context(|| "store scratchpad".to_string())?;
|
||||
db.store_item(scratchpad)
|
||||
.await
|
||||
.with_context(|| "store scratchpad".to_string())?;
|
||||
|
||||
let result = Scratchpad::update_content(&scratchpad_id, other_user, "Hacked", &db).await;
|
||||
assert!(result.is_err());
|
||||
@@ -425,14 +441,19 @@ mod tests {
|
||||
let scratchpad = Scratchpad::new(user_id.to_string(), "Test".to_string());
|
||||
let scratchpad_id = scratchpad.id.clone();
|
||||
|
||||
db.store_item(scratchpad).await.with_context(|| "store scratchpad".to_string())?;
|
||||
db.store_item(scratchpad)
|
||||
.await
|
||||
.with_context(|| "store scratchpad".to_string())?;
|
||||
|
||||
// Delete should succeed
|
||||
let result = Scratchpad::delete(&scratchpad_id, user_id, &db).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Verify it's gone
|
||||
let retrieved: Option<Scratchpad> = db.get_item(&scratchpad_id).await.with_context(|| "get_item".to_string())?;
|
||||
let retrieved: Option<Scratchpad> = db
|
||||
.get_item(&scratchpad_id)
|
||||
.await
|
||||
.with_context(|| "get_item".to_string())?;
|
||||
assert!(retrieved.is_none());
|
||||
Ok(())
|
||||
}
|
||||
@@ -454,7 +475,9 @@ mod tests {
|
||||
let scratchpad = Scratchpad::new(owner_id.to_string(), "Test".to_string());
|
||||
let scratchpad_id = scratchpad.id.clone();
|
||||
|
||||
db.store_item(scratchpad).await.with_context(|| "store scratchpad".to_string())?;
|
||||
db.store_item(scratchpad)
|
||||
.await
|
||||
.with_context(|| "store scratchpad".to_string())?;
|
||||
|
||||
let result = Scratchpad::delete(&scratchpad_id, other_user, &db).await;
|
||||
assert!(result.is_err());
|
||||
@@ -464,7 +487,10 @@ mod tests {
|
||||
}
|
||||
|
||||
// Verify it still exists
|
||||
let retrieved: Option<Scratchpad> = db.get_item(&scratchpad_id).await.with_context(|| "get_item".to_string())?;
|
||||
let retrieved: Option<Scratchpad> = db
|
||||
.get_item(&scratchpad_id)
|
||||
.await
|
||||
.with_context(|| "get_item".to_string())?;
|
||||
assert!(retrieved.is_some());
|
||||
Ok(())
|
||||
}
|
||||
@@ -484,7 +510,9 @@ mod tests {
|
||||
Scratchpad::new(user_id.to_string(), "Test Timezone Scratchpad".to_string());
|
||||
let scratchpad_id = scratchpad.id.clone();
|
||||
|
||||
db.store_item(scratchpad).await.with_context(|| "store scratchpad".to_string())?;
|
||||
db.store_item(scratchpad)
|
||||
.await
|
||||
.with_context(|| "store scratchpad".to_string())?;
|
||||
|
||||
let retrieved = Scratchpad::get_by_id(&scratchpad_id, user_id, &db)
|
||||
.await
|
||||
@@ -505,7 +533,13 @@ mod tests {
|
||||
.with_context(|| "archive".to_string())?;
|
||||
|
||||
assert!(archived.archived_at.is_some());
|
||||
assert!(archived.archived_at.with_context(|| "expected archived_at".to_string())?.timestamp() > 0);
|
||||
assert!(
|
||||
archived
|
||||
.archived_at
|
||||
.with_context(|| "expected archived_at".to_string())?
|
||||
.timestamp()
|
||||
> 0
|
||||
);
|
||||
assert!(archived.ingested_at.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -71,7 +71,9 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_option_datetime<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
|
||||
pub fn deserialize_option_datetime<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<DateTime<Utc>>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
|
||||
@@ -64,14 +64,13 @@ impl SystemSettings {
|
||||
let mut needs_update = false;
|
||||
|
||||
let backend_label = provider.backend_label().to_string();
|
||||
let provider_dimensions = u32::try_from(provider.dimension())
|
||||
.unwrap_or_else(|_| {
|
||||
tracing::warn!(
|
||||
"Provider dimension {} exceeds u32 max; falling back to 0",
|
||||
provider.dimension()
|
||||
);
|
||||
0u32
|
||||
});
|
||||
let provider_dimensions = u32::try_from(provider.dimension()).unwrap_or_else(|_| {
|
||||
tracing::warn!(
|
||||
"Provider dimension {} exceeds u32 max; falling back to 0",
|
||||
provider.dimension()
|
||||
);
|
||||
0u32
|
||||
});
|
||||
let provider_model = provider.model_code();
|
||||
|
||||
// Sync backend label
|
||||
@@ -114,9 +113,10 @@ impl SystemSettings {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::{self, Context};
|
||||
#![allow(clippy::expect_used, clippy::must_use_candidate)]
|
||||
use crate::storage::indexes::ensure_runtime;
|
||||
use crate::storage::types::{knowledge_entity::KnowledgeEntity, text_chunk::TextChunk};
|
||||
use anyhow::{self, Context};
|
||||
use async_openai::Client;
|
||||
|
||||
use super::*;
|
||||
@@ -138,8 +138,8 @@ mod tests {
|
||||
.take(0)
|
||||
.with_context(|| "Failed to extract table info response".to_string())?;
|
||||
|
||||
let info_json: serde_json::Value =
|
||||
serde_json::to_value(info).with_context(|| "Failed to convert info to json".to_string())?;
|
||||
let info_json: serde_json::Value = serde_json::to_value(info)
|
||||
.with_context(|| "Failed to convert info to json".to_string())?;
|
||||
|
||||
let indexes = info_json
|
||||
.get("Object")
|
||||
@@ -383,7 +383,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_should_change_embedding_length_on_indexes_when_switching_length() -> anyhow::Result<()> {
|
||||
async fn test_should_change_embedding_length_on_indexes_when_switching_length(
|
||||
) -> anyhow::Result<()> {
|
||||
let db = SurrealDbClient::memory("test", &Uuid::new_v4().to_string())
|
||||
.await
|
||||
.with_context(|| "Failed to start DB".to_string())?;
|
||||
@@ -436,7 +437,9 @@ mod tests {
|
||||
.with_context(|| "TextChunk re-embedding should succeed on fresh DB".to_string())?;
|
||||
KnowledgeEntity::update_all_embeddings(&db, &openai_client, &new_model, new_dimension)
|
||||
.await
|
||||
.with_context(|| "KnowledgeEntity re-embedding should succeed on fresh DB".to_string())?;
|
||||
.with_context(|| {
|
||||
"KnowledgeEntity re-embedding should succeed on fresh DB".to_string()
|
||||
})?;
|
||||
|
||||
let text_chunk_dimension = get_hnsw_index_dimension(
|
||||
&db,
|
||||
|
||||
@@ -237,7 +237,9 @@ impl TextChunk {
|
||||
new_model: &str,
|
||||
new_dimensions: u32,
|
||||
) -> Result<(), AppError> {
|
||||
info!("Starting re-embedding process for all text chunks. New dimensions: {new_dimensions}");
|
||||
info!(
|
||||
"Starting re-embedding process for all text chunks. New dimensions: {new_dimensions}"
|
||||
);
|
||||
|
||||
// Fetch all chunks first
|
||||
let all_chunks: Vec<TextChunk> = db.select(Self::table_name()).await?;
|
||||
@@ -451,6 +453,7 @@ impl TextChunk {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::must_use_candidate)]
|
||||
use anyhow::{self, Context};
|
||||
|
||||
use super::*;
|
||||
@@ -501,7 +504,9 @@ mod tests {
|
||||
let db = SurrealDbClient::memory(namespace, database)
|
||||
.await
|
||||
.with_context(|| "Failed to start in-memory surrealdb".to_string())?;
|
||||
db.apply_migrations().await.with_context(|| "migrations".to_string())?;
|
||||
db.apply_migrations()
|
||||
.await
|
||||
.with_context(|| "migrations".to_string())?;
|
||||
|
||||
let source_id = "source123".to_string();
|
||||
let user_id = "user123".to_string();
|
||||
@@ -580,7 +585,9 @@ mod tests {
|
||||
let db = SurrealDbClient::memory(namespace, database)
|
||||
.await
|
||||
.with_context(|| "Failed to start in-memory surrealdb".to_string())?;
|
||||
db.apply_migrations().await.with_context(|| "migrations".to_string())?;
|
||||
db.apply_migrations()
|
||||
.await
|
||||
.with_context(|| "migrations".to_string())?;
|
||||
TextChunkEmbedding::redefine_hnsw_index(&db, 5)
|
||||
.await
|
||||
.with_context(|| "redefine index".to_string())?;
|
||||
@@ -672,7 +679,9 @@ mod tests {
|
||||
let db = SurrealDbClient::memory(namespace, database)
|
||||
.await
|
||||
.with_context(|| "Failed to start in-memory surrealdb".to_string())?;
|
||||
db.apply_migrations().await.with_context(|| "migrations".to_string())?;
|
||||
db.apply_migrations()
|
||||
.await
|
||||
.with_context(|| "migrations".to_string())?;
|
||||
|
||||
let source_id = "store-src".to_string();
|
||||
let user_id = "user_store".to_string();
|
||||
@@ -686,7 +695,8 @@ mod tests {
|
||||
.await
|
||||
.with_context(|| "store with embedding".to_string())?;
|
||||
|
||||
let stored_chunk: Option<TextChunk> = db.get_item(&chunk.id)
|
||||
let stored_chunk: Option<TextChunk> = db
|
||||
.get_item(&chunk.id)
|
||||
.await
|
||||
.with_context(|| "get_item".to_string())?;
|
||||
let stored_chunk = stored_chunk.with_context(|| "expected stored chunk".to_string())?;
|
||||
@@ -711,7 +721,9 @@ mod tests {
|
||||
let db = SurrealDbClient::memory(namespace, database)
|
||||
.await
|
||||
.with_context(|| "Failed to start in-memory surrealdb".to_string())?;
|
||||
db.apply_migrations().await.with_context(|| "migrations".to_string())?;
|
||||
db.apply_migrations()
|
||||
.await
|
||||
.with_context(|| "migrations".to_string())?;
|
||||
|
||||
let embedding_dimension = 3usize;
|
||||
ensure_runtime(&db, embedding_dimension)
|
||||
@@ -728,7 +740,8 @@ mod tests {
|
||||
.await
|
||||
.with_context(|| "store with embedding".to_string())?;
|
||||
|
||||
let stored_chunk: Option<TextChunk> = db.get_item(&chunk.id)
|
||||
let stored_chunk: Option<TextChunk> = db
|
||||
.get_item(&chunk.id)
|
||||
.await
|
||||
.with_context(|| "get_item".to_string())?;
|
||||
let stored_chunk = stored_chunk.with_context(|| "chunk should be stored".to_string())?;
|
||||
@@ -754,7 +767,9 @@ mod tests {
|
||||
let db = SurrealDbClient::memory(namespace, database)
|
||||
.await
|
||||
.with_context(|| "Failed to start in-memory surrealdb".to_string())?;
|
||||
db.apply_migrations().await.with_context(|| "migrations".to_string())?;
|
||||
db.apply_migrations()
|
||||
.await
|
||||
.with_context(|| "migrations".to_string())?;
|
||||
|
||||
TextChunkEmbedding::redefine_hnsw_index(&db, 3)
|
||||
.await
|
||||
@@ -775,7 +790,9 @@ mod tests {
|
||||
let db = SurrealDbClient::memory(namespace, database)
|
||||
.await
|
||||
.with_context(|| "Failed to start in-memory surrealdb".to_string())?;
|
||||
db.apply_migrations().await.with_context(|| "migrations".to_string())?;
|
||||
db.apply_migrations()
|
||||
.await
|
||||
.with_context(|| "migrations".to_string())?;
|
||||
|
||||
TextChunkEmbedding::redefine_hnsw_index(&db, 3)
|
||||
.await
|
||||
@@ -813,7 +830,9 @@ mod tests {
|
||||
let db = SurrealDbClient::memory(namespace, database)
|
||||
.await
|
||||
.with_context(|| "Failed to start in-memory surrealdb".to_string())?;
|
||||
db.apply_migrations().await.with_context(|| "migrations".to_string())?;
|
||||
db.apply_migrations()
|
||||
.await
|
||||
.with_context(|| "migrations".to_string())?;
|
||||
|
||||
TextChunkEmbedding::redefine_hnsw_index(&db, 3)
|
||||
.await
|
||||
@@ -836,14 +855,8 @@ mod tests {
|
||||
.with_context(|| "vector_search".to_string())?;
|
||||
|
||||
assert_eq!(results.len(), 2);
|
||||
assert_eq!(
|
||||
results.first().map(|r| &r.chunk.id),
|
||||
Some(&chunk2.id)
|
||||
);
|
||||
assert_eq!(
|
||||
results.get(1).map(|r| &r.chunk.id),
|
||||
Some(&chunk1.id)
|
||||
);
|
||||
assert_eq!(results.first().map(|r| &r.chunk.id), Some(&chunk2.id));
|
||||
assert_eq!(results.get(1).map(|r| &r.chunk.id), Some(&chunk1.id));
|
||||
let r0 = results.first().context("expected first result")?;
|
||||
let r1 = results.get(1).context("expected second result")?;
|
||||
assert!(r0.score >= r1.score);
|
||||
@@ -857,9 +870,13 @@ mod tests {
|
||||
let db = SurrealDbClient::memory(namespace, database)
|
||||
.await
|
||||
.with_context(|| "Failed to start in-memory surrealdb".to_string())?;
|
||||
db.apply_migrations().await.with_context(|| "migrations".to_string())?;
|
||||
db.apply_migrations()
|
||||
.await
|
||||
.with_context(|| "migrations".to_string())?;
|
||||
ensure_chunk_fts_index(&db).await?;
|
||||
rebuild(&db).await.with_context(|| "rebuild indexes".to_string())?;
|
||||
rebuild(&db)
|
||||
.await
|
||||
.with_context(|| "rebuild indexes".to_string())?;
|
||||
|
||||
let results = TextChunk::fts_search(5, "hello", &db, "user")
|
||||
.await
|
||||
@@ -876,7 +893,9 @@ mod tests {
|
||||
let db = SurrealDbClient::memory(namespace, database)
|
||||
.await
|
||||
.with_context(|| "Failed to start in-memory surrealdb".to_string())?;
|
||||
db.apply_migrations().await.with_context(|| "migrations".to_string())?;
|
||||
db.apply_migrations()
|
||||
.await
|
||||
.with_context(|| "migrations".to_string())?;
|
||||
ensure_chunk_fts_index(&db).await?;
|
||||
|
||||
let user_id = "fts_user";
|
||||
@@ -885,8 +904,12 @@ mod tests {
|
||||
"rustaceans love rust".to_string(),
|
||||
user_id.to_string(),
|
||||
);
|
||||
db.store_item(chunk.clone()).await.with_context(|| "store chunk".to_string())?;
|
||||
rebuild(&db).await.with_context(|| "rebuild indexes".to_string())?;
|
||||
db.store_item(chunk.clone())
|
||||
.await
|
||||
.with_context(|| "store chunk".to_string())?;
|
||||
rebuild(&db)
|
||||
.await
|
||||
.with_context(|| "rebuild indexes".to_string())?;
|
||||
|
||||
let results = TextChunk::fts_search(3, "rust", &db, user_id)
|
||||
.await
|
||||
@@ -906,7 +929,9 @@ mod tests {
|
||||
let db = SurrealDbClient::memory(namespace, database)
|
||||
.await
|
||||
.with_context(|| "Failed to start in-memory surrealdb".to_string())?;
|
||||
db.apply_migrations().await.with_context(|| "migrations".to_string())?;
|
||||
db.apply_migrations()
|
||||
.await
|
||||
.with_context(|| "migrations".to_string())?;
|
||||
ensure_chunk_fts_index(&db).await?;
|
||||
|
||||
let user_id = "fts_user_order";
|
||||
@@ -935,7 +960,9 @@ mod tests {
|
||||
db.store_item(other_user_chunk)
|
||||
.await
|
||||
.with_context(|| "store other user chunk".to_string())?;
|
||||
rebuild(&db).await.with_context(|| "rebuild indexes".to_string())?;
|
||||
rebuild(&db)
|
||||
.await
|
||||
.with_context(|| "rebuild indexes".to_string())?;
|
||||
|
||||
let results = TextChunk::fts_search(3, "apple", &db, user_id)
|
||||
.await
|
||||
|
||||
@@ -126,6 +126,7 @@ impl TextChunkEmbedding {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::must_use_candidate)]
|
||||
use anyhow::{self, Context};
|
||||
|
||||
use super::*;
|
||||
@@ -177,9 +178,11 @@ mod tests {
|
||||
.query("INFO FOR TABLE text_chunk_embedding;")
|
||||
.await
|
||||
.with_context(|| "info query failed".to_string())?;
|
||||
let info: SurrealValue = info_res.take(0).with_context(|| "failed to take info result".to_string())?;
|
||||
let info_json: serde_json::Value =
|
||||
serde_json::to_value(info).with_context(|| "failed to convert info to json".to_string())?;
|
||||
let info: SurrealValue = info_res
|
||||
.take(0)
|
||||
.with_context(|| "failed to take info result".to_string())?;
|
||||
let info_json: serde_json::Value = serde_json::to_value(info)
|
||||
.with_context(|| "failed to convert info to json".to_string())?;
|
||||
let idx_sql = info_json
|
||||
.get("Object")
|
||||
.and_then(|v| v.get("indexes"))
|
||||
|
||||
@@ -185,6 +185,7 @@ impl TextContent {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::must_use_candidate)]
|
||||
use anyhow::{self, Context};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -723,6 +723,7 @@ impl User {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::must_use_candidate)]
|
||||
use anyhow::{self, Context};
|
||||
|
||||
use super::*;
|
||||
@@ -908,12 +909,16 @@ mod tests {
|
||||
first.created_at -= chrono::Duration::minutes(1);
|
||||
first.updated_at = first.created_at;
|
||||
first.state = TaskState::Succeeded;
|
||||
db.store_item(first.clone()).await.with_context(|| "store first".to_string())?;
|
||||
db.store_item(first.clone())
|
||||
.await
|
||||
.with_context(|| "store first".to_string())?;
|
||||
|
||||
// Latest task
|
||||
let mut second = IngestionTask::new(payload.clone(), user_id.to_string());
|
||||
second.state = TaskState::Processing;
|
||||
db.store_item(second.clone()).await.with_context(|| "store second".to_string())?;
|
||||
db.store_item(second.clone())
|
||||
.await
|
||||
.with_context(|| "store second".to_string())?;
|
||||
|
||||
let other_payload = IngestionPayload::Text {
|
||||
text: "Other".to_string(),
|
||||
@@ -922,7 +927,9 @@ mod tests {
|
||||
user_id: other_user_id.to_string(),
|
||||
};
|
||||
let other_task = IngestionTask::new(other_payload, other_user_id.to_string());
|
||||
db.store_item(other_task).await.with_context(|| "store other".to_string())?;
|
||||
db.store_item(other_task)
|
||||
.await
|
||||
.with_context(|| "store other".to_string())?;
|
||||
|
||||
let tasks = User::get_all_ingestion_tasks(user_id, &db)
|
||||
.await
|
||||
@@ -1192,7 +1199,10 @@ mod tests {
|
||||
}
|
||||
|
||||
// Check first conversation title matches the most recently updated
|
||||
let most_recent = conversations.iter().max_by_key(|c| c.created_at).context("expected most recent")?;
|
||||
let most_recent = conversations
|
||||
.iter()
|
||||
.max_by_key(|c| c.created_at)
|
||||
.context("expected most recent")?;
|
||||
let r0 = retrieved.first().context("expected first result")?;
|
||||
assert_eq!(r0.id, most_recent.id);
|
||||
Ok(())
|
||||
|
||||
@@ -9,7 +9,7 @@ pub enum IngestValidationError {
|
||||
pub fn validate_ingest_input(
|
||||
config: &AppConfig,
|
||||
content: Option<&str>,
|
||||
context: &str,
|
||||
ctx: &str,
|
||||
category: &str,
|
||||
file_count: usize,
|
||||
) -> Result<(), IngestValidationError> {
|
||||
@@ -29,7 +29,7 @@ pub fn validate_ingest_input(
|
||||
}
|
||||
}
|
||||
|
||||
if context.len() > config.ingest_max_context_bytes {
|
||||
if ctx.len() > config.ingest_max_context_bytes {
|
||||
return Err(IngestValidationError::PayloadTooLarge(format!(
|
||||
"Context is too large. Maximum allowed is {} bytes",
|
||||
config.ingest_max_context_bytes
|
||||
@@ -48,6 +48,7 @@ pub fn validate_ingest_input(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::must_use_candidate)]
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user