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:
Vanalite 2025-10-02 20:53:46 +07:00
parent 9720ad368e
commit 08d527366e
6 changed files with 368 additions and 23 deletions

View File

@ -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;
}

View File

@ -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()

View File

@ -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()));

View File

@ -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;

View File

@ -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);
}

View File

@ -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', () => {
})
})
})
})