mirror of
https://github.com/perstarkse/minne.git
synced 2026-04-18 06:59:43 +02:00
feat: chat conversation list titles, sorting, etc
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use surrealdb::opt::PatchOp;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{error::AppError, storage::db::SurrealDbClient, stored_object};
|
||||
@@ -46,6 +47,31 @@ impl Conversation {
|
||||
|
||||
Ok((conversation, messages))
|
||||
}
|
||||
pub async fn patch_title(
|
||||
id: &str,
|
||||
user_id: &str,
|
||||
new_title: &str,
|
||||
db: &SurrealDbClient,
|
||||
) -> Result<(), AppError> {
|
||||
// First verify ownership by getting conversation user_id
|
||||
let conversation: Option<Conversation> = db.get_item(id).await?;
|
||||
let conversation =
|
||||
conversation.ok_or_else(|| AppError::NotFound("Conversation not found".to_string()))?;
|
||||
|
||||
if conversation.user_id != user_id {
|
||||
return Err(AppError::Auth(
|
||||
"Unauthorized to update this conversation".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let _updated: Option<Self> = db
|
||||
.update((Self::table_name(), id))
|
||||
.patch(PatchOp::replace("/title", new_title.to_string()))
|
||||
.patch(PatchOp::replace("/updated_at", Utc::now()))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -141,6 +167,85 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_patch_title_success() {
|
||||
let namespace = "test_ns";
|
||||
let database = &Uuid::new_v4().to_string();
|
||||
let db = SurrealDbClient::memory(namespace, database)
|
||||
.await
|
||||
.expect("Failed to start in-memory surrealdb");
|
||||
|
||||
let user_id = "user_1";
|
||||
let original_title = "Original Title";
|
||||
let conversation = Conversation::new(user_id.to_string(), original_title.to_string());
|
||||
let conversation_id = conversation.id.clone();
|
||||
|
||||
db.store_item(conversation)
|
||||
.await
|
||||
.expect("Failed to store conversation");
|
||||
|
||||
let new_title = "Updated Title";
|
||||
|
||||
// Patch title successfully
|
||||
let result = Conversation::patch_title(&conversation_id, user_id, new_title, &db).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Retrieve from DB to verify
|
||||
let updated_conversation = db
|
||||
.get_item::<Conversation>(&conversation_id)
|
||||
.await
|
||||
.expect("Failed to get conversation")
|
||||
.expect("Conversation missing");
|
||||
assert_eq!(updated_conversation.title, new_title);
|
||||
assert_eq!(updated_conversation.user_id, user_id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_patch_title_not_found() {
|
||||
let namespace = "test_ns";
|
||||
let database = &Uuid::new_v4().to_string();
|
||||
let db = SurrealDbClient::memory(namespace, database)
|
||||
.await
|
||||
.expect("Failed to start in-memory surrealdb");
|
||||
|
||||
// Try to patch non-existing conversation
|
||||
let result = Conversation::patch_title("nonexistent", "user_x", "New Title", &db).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(AppError::NotFound(_)) => {}
|
||||
_ => panic!("Expected NotFound error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_patch_title_unauthorized() {
|
||||
let namespace = "test_ns";
|
||||
let database = &Uuid::new_v4().to_string();
|
||||
let db = SurrealDbClient::memory(namespace, database)
|
||||
.await
|
||||
.expect("Failed to start in-memory surrealdb");
|
||||
|
||||
let owner_id = "owner";
|
||||
let other_user_id = "intruder";
|
||||
let conversation = Conversation::new(owner_id.to_string(), "Private".to_string());
|
||||
let conversation_id = conversation.id.clone();
|
||||
|
||||
db.store_item(conversation)
|
||||
.await
|
||||
.expect("Failed to store conversation");
|
||||
|
||||
// Attempt patch with unauthorized user
|
||||
let result =
|
||||
Conversation::patch_title(&conversation_id, other_user_id, "Hacked Title", &db).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(AppError::Auth(_)) => {}
|
||||
_ => panic!("Expected Auth error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_complete_conversation_with_messages() {
|
||||
// Setup in-memory database for testing
|
||||
|
||||
@@ -580,7 +580,7 @@ mod tests {
|
||||
// Should fail with FileNotFound error
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(FileError::FileNotFound(_)) => {
|
||||
Err(AppError::File(_)) => {
|
||||
// Expected error
|
||||
}
|
||||
_ => panic!("Expected FileNotFound error"),
|
||||
|
||||
@@ -96,6 +96,8 @@ impl IngestionPayload {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::Utc;
|
||||
|
||||
use super::*;
|
||||
|
||||
// Create a mock FileInfo for testing
|
||||
|
||||
@@ -421,7 +421,10 @@ impl User {
|
||||
) -> Result<Vec<Conversation>, AppError> {
|
||||
let conversations: Vec<Conversation> = db
|
||||
.client
|
||||
.query("SELECT * FROM type::table($table_name) WHERE user_id = $user_id")
|
||||
.query(
|
||||
"SELECT * FROM type::table($table_name) WHERE user_id = $user_id
|
||||
ORDER BY updated_at DESC",
|
||||
)
|
||||
.bind(("table_name", Conversation::table_name()))
|
||||
.bind(("user_id", user_id.to_string()))
|
||||
.await?
|
||||
@@ -775,4 +778,41 @@ mod tests {
|
||||
let updated_user = updated_user.unwrap();
|
||||
assert_eq!(updated_user.timezone, new_timezone);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_conversations_order() {
|
||||
let db = setup_test_db().await;
|
||||
let user_id = "user_order_test";
|
||||
|
||||
// Create conversations with varying updated_at timestamps
|
||||
let mut conversations = Vec::new();
|
||||
for i in 0..5 {
|
||||
let mut conv = Conversation::new(user_id.to_string(), format!("Conv {}", i));
|
||||
// Fake updated_at i minutes apart
|
||||
conv.updated_at = chrono::Utc::now() - chrono::Duration::minutes(i);
|
||||
db.store_item(conv.clone())
|
||||
.await
|
||||
.expect("Failed to store conversation");
|
||||
conversations.push(conv);
|
||||
}
|
||||
|
||||
// Retrieve via get_user_conversations - should be ordered by updated_at DESC
|
||||
let retrieved = User::get_user_conversations(user_id, &db)
|
||||
.await
|
||||
.expect("Failed to get conversations");
|
||||
|
||||
assert_eq!(retrieved.len(), conversations.len());
|
||||
|
||||
for window in retrieved.windows(2) {
|
||||
// Assert each earlier conversation has updated_at >= later conversation
|
||||
assert!(
|
||||
window[0].updated_at >= window[1].updated_at,
|
||||
"Conversations not ordered descending by updated_at"
|
||||
);
|
||||
}
|
||||
|
||||
// (Optional) Check first conversation title matches the most recently updated
|
||||
let most_recent = conversations.iter().max_by_key(|c| c.updated_at).unwrap();
|
||||
assert_eq!(retrieved[0].id, most_recent.id);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user