479 lines
15 KiB
Rust
479 lines
15 KiB
Rust
|
|
use super::commands::*;
|
|
use super::helpers::should_use_sqlite;
|
|
use futures_util::future;
|
|
use serde_json::json;
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
use tauri::test::{mock_app, MockRuntime};
|
|
|
|
// Helper to create a mock app handle with a temp data dir
|
|
fn mock_app_with_temp_data_dir() -> (tauri::App<MockRuntime>, PathBuf) {
|
|
let app = mock_app();
|
|
// Create a unique test directory to avoid race conditions between parallel tests
|
|
let unique_id = std::thread::current().id();
|
|
let timestamp = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos();
|
|
let data_dir = std::env::current_dir()
|
|
.unwrap_or_else(|_| PathBuf::from("."))
|
|
.join(format!("test-data-{unique_id:?}-{timestamp}"));
|
|
println!("Mock app data dir: {}", data_dir.display());
|
|
// Ensure the unique test directory exists
|
|
let _ = fs::create_dir_all(&data_dir);
|
|
(app, data_dir)
|
|
}
|
|
|
|
// Helper to create a basic thread
|
|
fn create_test_thread(title: &str) -> serde_json::Value {
|
|
json!({
|
|
"object": "thread",
|
|
"title": title,
|
|
"assistants": [],
|
|
"created": 123,
|
|
"updated": 123,
|
|
"metadata": null
|
|
})
|
|
}
|
|
|
|
// Helper to create a basic message
|
|
fn create_test_message(thread_id: &str, content_text: &str) -> serde_json::Value {
|
|
json!({
|
|
"object": "message",
|
|
"thread_id": thread_id,
|
|
"role": "user",
|
|
"content": [{"type": "text", "text": content_text}],
|
|
"status": "sent",
|
|
"created_at": 123,
|
|
"completed_at": 123,
|
|
"metadata": null
|
|
})
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_create_and_list_threads() {
|
|
let (app, data_dir) = mock_app_with_temp_data_dir();
|
|
// Create a thread
|
|
let thread = json!({
|
|
"object": "thread",
|
|
"title": "Test Thread",
|
|
"assistants": [],
|
|
"created": 1234567890,
|
|
"updated": 1234567890,
|
|
"metadata": null
|
|
});
|
|
let created = create_thread(app.handle().clone(), thread.clone())
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(created["title"], "Test Thread");
|
|
|
|
// List threads
|
|
let threads = list_threads(app.handle().clone()).await.unwrap();
|
|
assert!(!threads.is_empty());
|
|
|
|
// Clean up
|
|
let _ = fs::remove_dir_all(data_dir);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_create_and_list_messages() {
|
|
let (app, data_dir) = mock_app_with_temp_data_dir();
|
|
// Create a thread first
|
|
let thread = json!({
|
|
"object": "thread",
|
|
"title": "Msg Thread",
|
|
"assistants": [],
|
|
"created": 123,
|
|
"updated": 123,
|
|
"metadata": null
|
|
});
|
|
let created = create_thread(app.handle().clone(), thread.clone())
|
|
.await
|
|
.unwrap();
|
|
let thread_id = created["id"].as_str().unwrap().to_string();
|
|
|
|
// Create a message
|
|
let message = json!({
|
|
"object": "message",
|
|
"thread_id": thread_id,
|
|
"assistant_id": null,
|
|
"attachments": null,
|
|
"role": "user",
|
|
"content": [],
|
|
"status": "sent",
|
|
"created_at": 123,
|
|
"completed_at": 123,
|
|
"metadata": null,
|
|
"type_": null,
|
|
"error_code": null,
|
|
"tool_call_id": null
|
|
});
|
|
let created_msg = create_message(app.handle().clone(), message).await.unwrap();
|
|
assert_eq!(created_msg["role"], "user");
|
|
|
|
// List messages
|
|
let messages = list_messages(app.handle().clone(), thread_id.clone())
|
|
.await
|
|
.unwrap();
|
|
assert!(!messages.is_empty(), "Expected at least one message, but got none. Thread ID: {thread_id}");
|
|
assert_eq!(messages[0]["role"], "user");
|
|
|
|
// Clean up
|
|
let _ = fs::remove_dir_all(data_dir);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_create_and_get_thread_assistant() {
|
|
let (app, data_dir) = mock_app_with_temp_data_dir();
|
|
// Create a thread
|
|
let thread = json!({
|
|
"object": "thread",
|
|
"title": "Assistant Thread",
|
|
"assistants": [],
|
|
"created": 1,
|
|
"updated": 1,
|
|
"metadata": null
|
|
});
|
|
let created = create_thread(app.handle().clone(), thread.clone())
|
|
.await
|
|
.unwrap();
|
|
let thread_id = created["id"].as_str().unwrap().to_string();
|
|
|
|
// Add assistant
|
|
let assistant = json!({
|
|
"id": "assistant-1",
|
|
"assistant_name": "Test Assistant",
|
|
"model": {
|
|
"id": "model-1",
|
|
"name": "Test Model",
|
|
"settings": json!({})
|
|
},
|
|
"instructions": null,
|
|
"tools": null
|
|
});
|
|
let _ = create_thread_assistant(app.handle().clone(), thread_id.clone(), assistant.clone())
|
|
.await
|
|
.unwrap();
|
|
|
|
// Get assistant
|
|
let got = get_thread_assistant(app.handle().clone(), thread_id.clone())
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(got["assistant_name"], "Test Assistant");
|
|
|
|
// Clean up
|
|
let _ = fs::remove_dir_all(data_dir);
|
|
}
|
|
|
|
#[test]
|
|
fn test_should_use_sqlite_platform_detection() {
|
|
// Test that should_use_sqlite returns correct value based on platform
|
|
// On desktop platforms (macOS, Linux, Windows), it should return false
|
|
// On mobile platforms (Android, iOS), it should return true
|
|
|
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
|
{
|
|
assert!(should_use_sqlite(), "should_use_sqlite should return true on mobile platforms");
|
|
}
|
|
|
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
{
|
|
assert!(!should_use_sqlite(), "should_use_sqlite should return false on desktop platforms");
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_desktop_storage_backend() {
|
|
// This test verifies that on desktop platforms, the file-based storage is used
|
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
{
|
|
let (app, _data_dir) = mock_app_with_temp_data_dir();
|
|
|
|
// Create a thread
|
|
let thread = json!({
|
|
"object": "thread",
|
|
"title": "Desktop Test Thread",
|
|
"assistants": [],
|
|
"created": 1234567890,
|
|
"updated": 1234567890,
|
|
"metadata": null
|
|
});
|
|
|
|
let created = create_thread(app.handle().clone(), thread.clone())
|
|
.await
|
|
.unwrap();
|
|
let thread_id = created["id"].as_str().unwrap().to_string();
|
|
|
|
// Verify we can retrieve the thread (which proves file storage works)
|
|
let threads = list_threads(app.handle().clone()).await.unwrap();
|
|
let found = threads.iter().any(|t| t["id"] == thread_id);
|
|
assert!(found, "Thread should be retrievable from file-based storage");
|
|
|
|
// Create a message
|
|
let message = json!({
|
|
"object": "message",
|
|
"thread_id": thread_id,
|
|
"role": "user",
|
|
"content": [],
|
|
"status": "sent",
|
|
"created_at": 123,
|
|
"completed_at": 123,
|
|
"metadata": null
|
|
});
|
|
|
|
let _created_msg = create_message(app.handle().clone(), message).await.unwrap();
|
|
|
|
// Verify we can retrieve the message (which proves file storage works)
|
|
let messages = list_messages(app.handle().clone(), thread_id.clone())
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(messages.len(), 1, "Message should be retrievable from file-based storage");
|
|
|
|
// Clean up - get the actual data directory used by the app
|
|
use super::utils::get_data_dir;
|
|
let actual_data_dir = get_data_dir(app.handle().clone());
|
|
let _ = fs::remove_dir_all(actual_data_dir);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_modify_and_delete_thread() {
|
|
let (app, data_dir) = mock_app_with_temp_data_dir();
|
|
|
|
// Create a thread
|
|
let thread = json!({
|
|
"object": "thread",
|
|
"title": "Original Title",
|
|
"assistants": [],
|
|
"created": 1234567890,
|
|
"updated": 1234567890,
|
|
"metadata": null
|
|
});
|
|
|
|
let created = create_thread(app.handle().clone(), thread.clone())
|
|
.await
|
|
.unwrap();
|
|
let thread_id = created["id"].as_str().unwrap().to_string();
|
|
|
|
// Modify the thread
|
|
let mut modified_thread = created.clone();
|
|
modified_thread["title"] = json!("Modified Title");
|
|
|
|
modify_thread(app.handle().clone(), modified_thread.clone())
|
|
.await
|
|
.unwrap();
|
|
|
|
// Verify modification by listing threads
|
|
let threads = list_threads(app.handle().clone()).await.unwrap();
|
|
let found_thread = threads.iter().find(|t| t["id"] == thread_id);
|
|
assert!(found_thread.is_some(), "Modified thread should exist");
|
|
assert_eq!(found_thread.unwrap()["title"], "Modified Title");
|
|
|
|
// Delete the thread
|
|
delete_thread(app.handle().clone(), thread_id.clone())
|
|
.await
|
|
.unwrap();
|
|
|
|
// Verify deletion
|
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
{
|
|
let thread_dir = data_dir.join(&thread_id);
|
|
assert!(!thread_dir.exists(), "Thread directory should be deleted");
|
|
}
|
|
|
|
// Clean up
|
|
let _ = fs::remove_dir_all(data_dir);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_modify_and_delete_message() {
|
|
let (app, data_dir) = mock_app_with_temp_data_dir();
|
|
|
|
// Create a thread
|
|
let thread = json!({
|
|
"object": "thread",
|
|
"title": "Message Test Thread",
|
|
"assistants": [],
|
|
"created": 123,
|
|
"updated": 123,
|
|
"metadata": null
|
|
});
|
|
|
|
let created = create_thread(app.handle().clone(), thread.clone())
|
|
.await
|
|
.unwrap();
|
|
let thread_id = created["id"].as_str().unwrap().to_string();
|
|
|
|
// Create a message
|
|
let message = json!({
|
|
"object": "message",
|
|
"thread_id": thread_id,
|
|
"role": "user",
|
|
"content": [{"type": "text", "text": "Original content"}],
|
|
"status": "sent",
|
|
"created_at": 123,
|
|
"completed_at": 123,
|
|
"metadata": null
|
|
});
|
|
|
|
let created_msg = create_message(app.handle().clone(), message).await.unwrap();
|
|
let message_id = created_msg["id"].as_str().unwrap().to_string();
|
|
|
|
// Modify the message
|
|
let mut modified_msg = created_msg.clone();
|
|
modified_msg["content"] = json!([{"type": "text", "text": "Modified content"}]);
|
|
|
|
modify_message(app.handle().clone(), modified_msg.clone())
|
|
.await
|
|
.unwrap();
|
|
|
|
// Verify modification
|
|
let messages = list_messages(app.handle().clone(), thread_id.clone())
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(messages.len(), 1);
|
|
assert_eq!(messages[0]["content"][0]["text"], "Modified content");
|
|
|
|
// Delete the message
|
|
delete_message(app.handle().clone(), thread_id.clone(), message_id.clone())
|
|
.await
|
|
.unwrap();
|
|
|
|
// Verify deletion
|
|
let messages = list_messages(app.handle().clone(), thread_id.clone())
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(messages.len(), 0, "Message should be deleted");
|
|
|
|
// Clean up
|
|
let _ = fs::remove_dir_all(data_dir);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_modify_thread_assistant() {
|
|
let (app, data_dir) = mock_app_with_temp_data_dir();
|
|
let app_handle = app.handle().clone();
|
|
|
|
let created = create_thread(app_handle.clone(), create_test_thread("Assistant Mod Thread"))
|
|
.await
|
|
.unwrap();
|
|
let thread_id = created["id"].as_str().unwrap();
|
|
|
|
let assistant = json!({
|
|
"id": "assistant-1",
|
|
"assistant_name": "Original Assistant",
|
|
"model": {"id": "model-1", "name": "Test Model"}
|
|
});
|
|
|
|
create_thread_assistant(app_handle.clone(), thread_id.to_string(), assistant.clone())
|
|
.await
|
|
.unwrap();
|
|
|
|
let mut modified_assistant = assistant;
|
|
modified_assistant["assistant_name"] = json!("Modified Assistant");
|
|
|
|
modify_thread_assistant(app_handle.clone(), thread_id.to_string(), modified_assistant)
|
|
.await
|
|
.unwrap();
|
|
|
|
let retrieved = get_thread_assistant(app_handle, thread_id.to_string())
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(retrieved["assistant_name"], "Modified Assistant");
|
|
|
|
let _ = fs::remove_dir_all(data_dir);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_thread_not_found_errors() {
|
|
let (app, data_dir) = mock_app_with_temp_data_dir();
|
|
let app_handle = app.handle().clone();
|
|
let fake_thread_id = "non-existent-thread-id".to_string();
|
|
let assistant = json!({"id": "assistant-1", "assistant_name": "Test Assistant"});
|
|
|
|
assert!(get_thread_assistant(app_handle.clone(), fake_thread_id.clone()).await.is_err());
|
|
assert!(create_thread_assistant(app_handle.clone(), fake_thread_id.clone(), assistant.clone()).await.is_err());
|
|
assert!(modify_thread_assistant(app_handle, fake_thread_id, assistant).await.is_err());
|
|
|
|
let _ = fs::remove_dir_all(data_dir);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_message_without_id_gets_generated() {
|
|
let (app, data_dir) = mock_app_with_temp_data_dir();
|
|
let app_handle = app.handle().clone();
|
|
|
|
let created = create_thread(app_handle.clone(), create_test_thread("Message ID Test"))
|
|
.await
|
|
.unwrap();
|
|
let thread_id = created["id"].as_str().unwrap();
|
|
|
|
let message = json!({"object": "message", "thread_id": thread_id, "role": "user", "content": [], "status": "sent"});
|
|
let created_msg = create_message(app_handle, message).await.unwrap();
|
|
|
|
assert!(created_msg["id"].as_str().is_some_and(|id| !id.is_empty()));
|
|
|
|
let _ = fs::remove_dir_all(data_dir);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_concurrent_message_operations() {
|
|
let (app, data_dir) = mock_app_with_temp_data_dir();
|
|
let app_handle = app.handle().clone();
|
|
|
|
let created = create_thread(app_handle.clone(), create_test_thread("Concurrent Test"))
|
|
.await
|
|
.unwrap();
|
|
let thread_id = created["id"].as_str().unwrap().to_string();
|
|
|
|
let handles: Vec<_> = (0..5)
|
|
.map(|i| {
|
|
let app_h = app_handle.clone();
|
|
let tid = thread_id.clone();
|
|
tokio::spawn(async move {
|
|
create_message(app_h, create_test_message(&tid, &format!("Message {}", i))).await
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
let results = future::join_all(handles).await;
|
|
assert!(results.iter().all(|r| r.is_ok() && r.as_ref().unwrap().is_ok()));
|
|
|
|
let messages = list_messages(app_handle, thread_id).await.unwrap();
|
|
assert_eq!(messages.len(), 5);
|
|
|
|
let _ = fs::remove_dir_all(data_dir);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_empty_thread_list() {
|
|
let (app, data_dir) = mock_app_with_temp_data_dir();
|
|
// Clean up any leftover test data
|
|
let test_data_threads = std::env::current_dir()
|
|
.unwrap_or_else(|_| PathBuf::from("."))
|
|
.join("test-data")
|
|
.join("threads");
|
|
let _ = fs::remove_dir_all(&test_data_threads);
|
|
|
|
let threads = list_threads(app.handle().clone()).await.unwrap();
|
|
assert_eq!(threads.len(), 0);
|
|
let _ = fs::remove_dir_all(data_dir);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_empty_message_list() {
|
|
let (app, data_dir) = mock_app_with_temp_data_dir();
|
|
let app_handle = app.handle().clone();
|
|
|
|
let created = create_thread(app_handle.clone(), create_test_thread("Empty Messages Test"))
|
|
.await
|
|
.unwrap();
|
|
let thread_id = created["id"].as_str().unwrap();
|
|
|
|
let messages = list_messages(app_handle, thread_id.to_string()).await.unwrap();
|
|
assert_eq!(messages.len(), 0);
|
|
|
|
let _ = fs::remove_dir_all(data_dir);
|
|
}
|