feat: organize code for proper import
Move platform checker for db access to helper Add test for to threads controller
This commit is contained in:
parent
9720ad368e
commit
08d527366e
@ -3,9 +3,11 @@ use std::io::Write;
|
||||
use tauri::Runtime;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
use super::db;
|
||||
use super::helpers::{
|
||||
get_lock_for_thread, read_messages_from_file, update_thread_metadata, write_messages_to_file,
|
||||
get_lock_for_thread, read_messages_from_file, should_use_sqlite, update_thread_metadata,
|
||||
write_messages_to_file,
|
||||
};
|
||||
use super::{
|
||||
constants::THREADS_FILE,
|
||||
@ -21,8 +23,9 @@ use super::{
|
||||
pub async fn list_threads<R: Runtime>(
|
||||
app_handle: tauri::AppHandle<R>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
if db::should_use_sqlite() {
|
||||
if should_use_sqlite() {
|
||||
// Use SQLite on mobile platforms
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
return db::db_list_threads(app_handle).await;
|
||||
}
|
||||
|
||||
@ -63,7 +66,8 @@ pub async fn create_thread<R: Runtime>(
|
||||
app_handle: tauri::AppHandle<R>,
|
||||
mut thread: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
if db::should_use_sqlite() {
|
||||
if should_use_sqlite() {
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
return db::db_create_thread(app_handle, thread).await;
|
||||
}
|
||||
|
||||
@ -88,7 +92,8 @@ pub async fn modify_thread<R: Runtime>(
|
||||
app_handle: tauri::AppHandle<R>,
|
||||
thread: serde_json::Value,
|
||||
) -> Result<(), String> {
|
||||
if db::should_use_sqlite() {
|
||||
if should_use_sqlite() {
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
return db::db_modify_thread(app_handle, thread).await;
|
||||
}
|
||||
|
||||
@ -113,7 +118,8 @@ pub async fn delete_thread<R: Runtime>(
|
||||
app_handle: tauri::AppHandle<R>,
|
||||
thread_id: String,
|
||||
) -> Result<(), String> {
|
||||
if db::should_use_sqlite() {
|
||||
if should_use_sqlite() {
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
return db::db_delete_thread(app_handle, &thread_id).await;
|
||||
}
|
||||
|
||||
@ -132,7 +138,8 @@ pub async fn list_messages<R: Runtime>(
|
||||
app_handle: tauri::AppHandle<R>,
|
||||
thread_id: String,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
if db::should_use_sqlite() {
|
||||
if should_use_sqlite() {
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
return db::db_list_messages(app_handle, &thread_id).await;
|
||||
}
|
||||
|
||||
@ -147,7 +154,8 @@ pub async fn create_message<R: Runtime>(
|
||||
app_handle: tauri::AppHandle<R>,
|
||||
mut message: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
if db::should_use_sqlite() {
|
||||
if should_use_sqlite() {
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
return db::db_create_message(app_handle, message).await;
|
||||
}
|
||||
|
||||
@ -198,7 +206,8 @@ pub async fn modify_message<R: Runtime>(
|
||||
app_handle: tauri::AppHandle<R>,
|
||||
message: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
if db::should_use_sqlite() {
|
||||
if should_use_sqlite() {
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
return db::db_modify_message(app_handle, message).await;
|
||||
}
|
||||
|
||||
@ -241,7 +250,8 @@ pub async fn delete_message<R: Runtime>(
|
||||
thread_id: String,
|
||||
message_id: String,
|
||||
) -> Result<(), String> {
|
||||
if db::should_use_sqlite() {
|
||||
if should_use_sqlite() {
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
return db::db_delete_message(app_handle, &thread_id, &message_id).await;
|
||||
}
|
||||
|
||||
@ -269,7 +279,8 @@ pub async fn get_thread_assistant<R: Runtime>(
|
||||
app_handle: tauri::AppHandle<R>,
|
||||
thread_id: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
if db::should_use_sqlite() {
|
||||
if should_use_sqlite() {
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
return db::db_get_thread_assistant(app_handle, &thread_id).await;
|
||||
}
|
||||
|
||||
@ -299,7 +310,8 @@ pub async fn create_thread_assistant<R: Runtime>(
|
||||
thread_id: String,
|
||||
assistant: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
if db::should_use_sqlite() {
|
||||
if should_use_sqlite() {
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
return db::db_create_thread_assistant(app_handle, &thread_id, assistant).await;
|
||||
}
|
||||
|
||||
@ -329,7 +341,8 @@ pub async fn modify_thread_assistant<R: Runtime>(
|
||||
thread_id: String,
|
||||
assistant: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
if db::should_use_sqlite() {
|
||||
if should_use_sqlite() {
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
return db::db_modify_thread_assistant(app_handle, &thread_id, assistant).await;
|
||||
}
|
||||
|
||||
|
||||
@ -23,17 +23,8 @@ const DB_NAME: &str = "jan.db";
|
||||
/// Global database pool for mobile platforms
|
||||
static DB_POOL: OnceLock<Mutex<Option<SqlitePool>>> = OnceLock::new();
|
||||
|
||||
/// Check if the platform should use SQLite (mobile platforms)
|
||||
pub fn should_use_sqlite() -> bool {
|
||||
cfg!(any(target_os = "android", target_os = "ios"))
|
||||
}
|
||||
|
||||
/// Initialize database with connection pool and run migrations
|
||||
pub async fn init_database<R: Runtime>(app: &AppHandle<R>) -> Result<(), String> {
|
||||
if !should_use_sqlite() {
|
||||
return Ok(()); // Skip DB initialization on desktop
|
||||
}
|
||||
|
||||
// Get app data directory
|
||||
let app_data_dir = app
|
||||
.path()
|
||||
|
||||
@ -13,6 +13,11 @@ use super::utils::{get_messages_path, get_thread_metadata_path};
|
||||
// Global per-thread locks for message file writes
|
||||
pub static MESSAGE_LOCKS: OnceLock<Mutex<HashMap<String, Arc<Mutex<()>>>>> = OnceLock::new();
|
||||
|
||||
/// Check if the platform should use SQLite (mobile platforms)
|
||||
pub fn should_use_sqlite() -> bool {
|
||||
cfg!(any(target_os = "android", target_os = "ios"))
|
||||
}
|
||||
|
||||
/// Get a lock for a specific thread to ensure thread-safe message file operations
|
||||
pub async fn get_lock_for_thread(thread_id: &str) -> Arc<Mutex<()>> {
|
||||
let locks = MESSAGE_LOCKS.get_or_init(|| Mutex::new(HashMap::new()));
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
|
||||
pub mod commands;
|
||||
mod constants;
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
pub mod db;
|
||||
pub mod helpers;
|
||||
pub mod utils;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
|
||||
use super::commands::*;
|
||||
use super::helpers::should_use_sqlite;
|
||||
use serde_json::json;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
@ -23,6 +24,32 @@ fn mock_app_with_temp_data_dir() -> (tauri::App<MockRuntime>, PathBuf) {
|
||||
(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();
|
||||
@ -137,3 +164,307 @@ async fn test_create_and_get_thread_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 = futures::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();
|
||||
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);
|
||||
}
|
||||
|
||||
@ -4,7 +4,11 @@ import { isPlatformTauri } from '@/lib/platform/utils'
|
||||
|
||||
// Mock platform detection
|
||||
vi.mock('@/lib/platform/utils', () => ({
|
||||
isPlatformTauri: vi.fn().mockReturnValue(false)
|
||||
isPlatformTauri: vi.fn().mockReturnValue(false),
|
||||
isPlatformIOS: vi.fn().mockReturnValue(false),
|
||||
isPlatformAndroid: vi.fn().mockReturnValue(false),
|
||||
isIOS: vi.fn().mockReturnValue(false),
|
||||
isAndroid: vi.fn().mockReturnValue(false)
|
||||
}))
|
||||
|
||||
// Mock @jan/extensions-web to return empty extensions for testing
|
||||
@ -213,4 +217,4 @@ describe('ServiceHub Integration Tests', () => {
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user