use std::env;
use std::fs;
use std::thread;
use std::time::Duration;
use rusqlite::Connection;
use tempfile::tempdir;
use super::Storage;
use crate::bridge_protocol::{
BridgeManagementOperation, BridgeManagementPhase, BridgeManagementStatus, BridgeManagementTask,
};
#[test]
fn thread_index_migration_removes_legacy_note_column() {
let temp_dir = tempdir().expect("创建临时目录失败");
let db_path = temp_dir.path().join("bridge.db");
{
let conn = Connection::open(&db_path).expect("创建旧版数据库失败");
conn.execute_batch(
"CREATE TABLE thread_index (
thread_id TEXT PRIMARY KEY,
runtime_id TEXT NOT NULL DEFAULT 'primary',
name TEXT NULL,
note TEXT NULL,
preview TEXT NOT NULL,
cwd TEXT NOT NULL,
status TEXT NOT NULL,
model_provider TEXT NOT NULL,
source TEXT NOT NULL,
created_at_ms INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL,
is_loaded INTEGER NOT NULL,
is_active INTEGER NOT NULL,
archived INTEGER NOT NULL DEFAULT 0,
raw_json TEXT NOT NULL
);
INSERT INTO thread_index (
thread_id, runtime_id, name, note, preview, cwd, status,
model_provider, source, created_at_ms, updated_at_ms, is_loaded,
is_active, archived, raw_json
)
VALUES (
'thread-legacy',
'runtime-primary',
'Legacy Name',
'legacy note',
'legacy preview',
'/srv/workspace',
'idle',
'openai',
'mobile',
1,
2,
1,
0,
0,
'{\"id\":\"thread-legacy\",\"runtimeId\":\"runtime-primary\",\"name\":\"Legacy Name\",\"note\":\"legacy note\",\"preview\":\"legacy preview\",\"cwd\":\"/srv/workspace\",\"status\":\"idle\",\"statusInfo\":{\"kind\":\"idle\",\"raw\":null},\"tokenUsage\":null,\"modelProvider\":\"openai\",\"source\":\"mobile\",\"createdAt\":1,\"updatedAt\":2,\"isLoaded\":true,\"isActive\":false,\"archived\":false}'
);",
)
.expect("写入旧版 thread_index 数据失败");
}
let storage = Storage::open(db_path.clone()).expect("执行 thread_index 迁移失败");
let migrated = storage
.get_thread_index("thread-legacy")
.expect("读取迁移后线程失败")
.expect("迁移后线程不存在");
assert_eq!(migrated.name.as_deref(), Some("Legacy Name"));
assert_eq!(migrated.preview, "legacy preview");
let conn = Connection::open(&db_path).expect("打开迁移后的数据库失败");
let mut stmt = conn
.prepare("PRAGMA table_info(thread_index)")
.expect("准备 thread_index 结构查询失败");
let columns = stmt
.query_map([], |row| row.get::<_, String>(1))
.expect("查询 thread_index 结构失败")
.collect::<rusqlite::Result<Vec<_>>>()
.expect("收集 thread_index 列失败");
assert!(!columns.iter().any(|column| column == "note"));
}
#[test]
fn ensure_primary_runtime_refreshes_existing_binary() {
let base_dir =
env::temp_dir().join(format!("codex-mobile-storage-test-{}", std::process::id()));
fs::create_dir_all(&base_dir).expect("创建测试目录失败");
let db_path = base_dir.join("bridge.db");
let storage = Storage::open(db_path).expect("打开存储失败");
let initial = storage
.ensure_primary_runtime(None, "codex".to_string())
.expect("创建 primary runtime 失败");
assert_eq!(initial.codex_binary, "codex");
let refreshed = storage
.ensure_primary_runtime(None, "/home/test/.npm-global/bin/codex".to_string())
.expect("刷新 primary runtime 失败");
assert_eq!(refreshed.codex_binary, "/home/test/.npm-global/bin/codex");
}
#[test]
fn migrate_legacy_workspaces_preserves_existing_bookmarks() {
let temp_dir = tempdir().expect("创建临时目录失败");
let db_path = temp_dir.path().join("bridge.db");
{
let conn = Connection::open(&db_path).expect("创建旧版数据库失败");
conn.execute_batch(
"CREATE TABLE workspaces (
root_path TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
created_at_ms INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL
);
CREATE TABLE directory_bookmarks (
path TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
created_at_ms INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL
);
INSERT INTO workspaces (root_path, display_name, created_at_ms, updated_at_ms)
VALUES
('/tmp/new-project', '新项目', 11, 12),
('/tmp/existing-project', '旧工作区名称', 21, 22);
INSERT INTO directory_bookmarks (path, display_name, created_at_ms, updated_at_ms)
VALUES ('/tmp/existing-project', '已存在书签', 31, 32);",
)
.expect("写入旧版测试数据失败");
}
Storage::open(db_path.clone()).expect("执行迁移失败");
let conn = Connection::open(&db_path).expect("打开迁移后的数据库失败");
let mut stmt = conn
.prepare(
"SELECT path, display_name, created_at_ms, updated_at_ms
FROM directory_bookmarks
ORDER BY path ASC",
)
.expect("准备查询目录书签失败");
let bookmarks = stmt
.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, i64>(2)?,
row.get::<_, i64>(3)?,
))
})
.expect("查询目录书签失败")
.collect::<rusqlite::Result<Vec<_>>>()
.expect("收集目录书签失败");
assert_eq!(
bookmarks,
vec![
(
"/tmp/existing-project".to_string(),
"已存在书签".to_string(),
31,
32,
),
("/tmp/new-project".to_string(), "新项目".to_string(), 11, 12),
]
);
let workspace_exists = conn
.query_row(
"SELECT EXISTS(
SELECT 1
FROM sqlite_master
WHERE type = 'table' AND name = 'workspaces'
)",
[],
|row| row.get::<_, i64>(0),
)
.expect("检查 workspaces 表失败");
assert_eq!(workspace_exists, 0);
}
#[test]
fn upsert_directory_bookmark_updates_name_without_resetting_created_at() {
let temp_dir = tempdir().expect("创建临时目录失败");
let db_path = temp_dir.path().join("bridge.db");
let workspace = temp_dir.path().join("workspace");
fs::create_dir_all(&workspace).expect("创建工作目录失败");
let storage = Storage::open(db_path).expect("打开存储失败");
let first = storage
.upsert_directory_bookmark(&workspace, Some("旧名称"))
.expect("首次写入目录书签失败");
thread::sleep(Duration::from_millis(2));
let second = storage
.upsert_directory_bookmark(&workspace, Some("新名称"))
.expect("更新目录书签失败");
assert_eq!(first.created_at_ms, second.created_at_ms);
assert_eq!(second.display_name, "新名称");
assert!(second.updated_at_ms >= first.updated_at_ms);
let bookmarks = storage
.list_directory_bookmarks()
.expect("读取目录书签失败");
assert_eq!(bookmarks.len(), 1);
assert_eq!(bookmarks[0].display_name, "新名称");
}
#[test]
fn record_directory_usage_accumulates_use_count() {
let temp_dir = tempdir().expect("创建临时目录失败");
let db_path = temp_dir.path().join("bridge.db");
let workspace = temp_dir.path().join("workspace");
fs::create_dir_all(&workspace).expect("创建工作目录失败");
let storage = Storage::open(db_path).expect("打开存储失败");
let first = storage
.record_directory_usage(&workspace)
.expect("首次记录目录使用失败");
thread::sleep(Duration::from_millis(2));
let second = storage
.record_directory_usage(&workspace)
.expect("再次记录目录使用失败");
assert_eq!(first.use_count, 1);
assert_eq!(second.use_count, 2);
assert!(second.last_used_at_ms >= first.last_used_at_ms);
let history = storage
.list_directory_history(20)
.expect("读取目录历史失败");
assert_eq!(history.len(), 1);
assert_eq!(history[0].use_count, 2);
}
#[test]
fn bridge_management_tasks_can_round_trip() {
let temp_dir = tempdir().expect("创建临时目录失败");
let db_path = temp_dir.path().join("bridge.db");
let storage = Storage::open(db_path).expect("打开存储失败");
let task = BridgeManagementTask {
task_id: "task-1".to_string(),
operation: BridgeManagementOperation::Update,
status: BridgeManagementStatus::Running,
phase: BridgeManagementPhase::InstallRelease,
summary: "正在安装 bridge 0.2.8".to_string(),
detail: Some("通过 cargo install 拉取 release".to_string()),
failure_code: None,
target_version: Some("0.2.8".to_string()),
current_version: Some("0.2.7".to_string()),
started_at_ms: 1,
updated_at_ms: 2,
snapshot: None,
};
storage
.upsert_bridge_management_task(&task)
.expect("写入 bridge 管理任务失败");
let loaded = storage
.get_bridge_management_task("task-1")
.expect("读取 bridge 管理任务失败")
.expect("bridge 管理任务不存在");
assert_eq!(loaded.task_id, "task-1");
assert_eq!(loaded.target_version.as_deref(), Some("0.2.8"));
assert_eq!(loaded.current_version.as_deref(), Some("0.2.7"));
assert!(
storage
.active_bridge_management_task()
.expect("查询活动任务失败")
.is_some()
);
}