mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-26 23:56:02 +08:00
e6654bd7f9
- Remove `is_proxy_target` field from Provider struct (Rust & TypeScript) - Remove related DAO methods: get_proxy_target_provider, set_proxy_target - Remove deprecated Tauri commands: get_proxy_targets, set_proxy_target - Add `is_available()` method to CircuitBreaker for availability checks without consuming HalfOpen probe permits (used in select_providers) - Keep `allow_request()` for actual request gating with permit tracking - Update stream_check to use failover_queue instead of is_proxy_target - Clean up commented-out reset circuit breaker button in ProviderActions - Remove unused useProxyTargets and useSetProxyTarget hooks
334 lines
9.8 KiB
Rust
334 lines
9.8 KiB
Rust
//! 数据库模块测试
|
|
//!
|
|
//! 包含 Schema 迁移和基本功能的测试。
|
|
|
|
use super::*;
|
|
use crate::app_config::MultiAppConfig;
|
|
use crate::provider::{Provider, ProviderManager};
|
|
use indexmap::IndexMap;
|
|
use rusqlite::Connection;
|
|
use serde_json::json;
|
|
use std::collections::HashMap;
|
|
|
|
const LEGACY_SCHEMA_SQL: &str = r#"
|
|
CREATE TABLE providers (
|
|
id TEXT NOT NULL,
|
|
app_type TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
settings_config TEXT NOT NULL,
|
|
PRIMARY KEY (id, app_type)
|
|
);
|
|
CREATE TABLE provider_endpoints (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
provider_id TEXT NOT NULL,
|
|
app_type TEXT NOT NULL,
|
|
url TEXT NOT NULL
|
|
);
|
|
CREATE TABLE mcp_servers (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
server_config TEXT NOT NULL
|
|
);
|
|
CREATE TABLE prompts (
|
|
id TEXT NOT NULL,
|
|
app_type TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
PRIMARY KEY (id, app_type)
|
|
);
|
|
CREATE TABLE skills (
|
|
key TEXT PRIMARY KEY,
|
|
installed BOOLEAN NOT NULL DEFAULT 0
|
|
);
|
|
CREATE TABLE skill_repos (
|
|
owner TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
PRIMARY KEY (owner, name)
|
|
);
|
|
CREATE TABLE settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT
|
|
);
|
|
"#;
|
|
|
|
#[derive(Debug)]
|
|
struct ColumnInfo {
|
|
name: String,
|
|
r#type: String,
|
|
notnull: i64,
|
|
default: Option<String>,
|
|
}
|
|
|
|
fn get_column_info(conn: &Connection, table: &str, column: &str) -> ColumnInfo {
|
|
let mut stmt = conn
|
|
.prepare(&format!("PRAGMA table_info(\"{table}\");"))
|
|
.expect("prepare pragma");
|
|
let mut rows = stmt.query([]).expect("query pragma");
|
|
while let Some(row) = rows.next().expect("read row") {
|
|
let name: String = row.get(1).expect("name");
|
|
if name.eq_ignore_ascii_case(column) {
|
|
return ColumnInfo {
|
|
name,
|
|
r#type: row.get::<_, String>(2).expect("type"),
|
|
notnull: row.get::<_, i64>(3).expect("notnull"),
|
|
default: row.get::<_, Option<String>>(4).ok().flatten(),
|
|
};
|
|
}
|
|
}
|
|
panic!("column {table}.{column} not found");
|
|
}
|
|
|
|
fn normalize_default(default: &Option<String>) -> Option<String> {
|
|
default
|
|
.as_ref()
|
|
.map(|s| s.trim_matches('\'').trim_matches('"').to_string())
|
|
}
|
|
|
|
#[test]
|
|
fn migration_sets_user_version_when_missing() {
|
|
let conn = Connection::open_in_memory().expect("open memory db");
|
|
|
|
Database::create_tables_on_conn(&conn).expect("create tables");
|
|
assert_eq!(
|
|
Database::get_user_version(&conn).expect("read version before"),
|
|
0
|
|
);
|
|
|
|
Database::apply_schema_migrations_on_conn(&conn).expect("apply migration");
|
|
|
|
assert_eq!(
|
|
Database::get_user_version(&conn).expect("read version after"),
|
|
SCHEMA_VERSION
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn migration_rejects_future_version() {
|
|
let conn = Connection::open_in_memory().expect("open memory db");
|
|
Database::create_tables_on_conn(&conn).expect("create tables");
|
|
Database::set_user_version(&conn, SCHEMA_VERSION + 1).expect("set future version");
|
|
|
|
let err =
|
|
Database::apply_schema_migrations_on_conn(&conn).expect_err("should reject higher version");
|
|
assert!(
|
|
err.to_string().contains("数据库版本过新"),
|
|
"unexpected error: {err}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn migration_adds_missing_columns_for_providers() {
|
|
let conn = Connection::open_in_memory().expect("open memory db");
|
|
|
|
// 创建旧版 providers 表,缺少新增列
|
|
conn.execute_batch(LEGACY_SCHEMA_SQL)
|
|
.expect("seed old schema");
|
|
|
|
Database::apply_schema_migrations_on_conn(&conn).expect("apply migrations");
|
|
|
|
// 验证关键新增列已补齐
|
|
for (table, column) in [
|
|
("providers", "meta"),
|
|
("providers", "is_current"),
|
|
("provider_endpoints", "added_at"),
|
|
("mcp_servers", "enabled_gemini"),
|
|
("prompts", "updated_at"),
|
|
("skills", "installed_at"),
|
|
("skill_repos", "enabled"),
|
|
] {
|
|
assert!(
|
|
Database::has_column(&conn, table, column).expect("check column"),
|
|
"{table}.{column} should exist after migration"
|
|
);
|
|
}
|
|
|
|
// 验证 meta 列约束保持一致
|
|
let meta = get_column_info(&conn, "providers", "meta");
|
|
assert_eq!(meta.notnull, 1, "meta should be NOT NULL");
|
|
assert_eq!(
|
|
normalize_default(&meta.default).as_deref(),
|
|
Some("{}"),
|
|
"meta default should be '{{}}'"
|
|
);
|
|
|
|
assert_eq!(
|
|
Database::get_user_version(&conn).expect("version after migration"),
|
|
SCHEMA_VERSION
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn migration_aligns_column_defaults_and_types() {
|
|
let conn = Connection::open_in_memory().expect("open memory db");
|
|
conn.execute_batch(LEGACY_SCHEMA_SQL)
|
|
.expect("seed old schema");
|
|
|
|
Database::apply_schema_migrations_on_conn(&conn).expect("apply migrations");
|
|
|
|
let is_current = get_column_info(&conn, "providers", "is_current");
|
|
assert_eq!(is_current.r#type, "BOOLEAN");
|
|
assert_eq!(is_current.notnull, 1);
|
|
assert_eq!(normalize_default(&is_current.default).as_deref(), Some("0"));
|
|
|
|
let tags = get_column_info(&conn, "mcp_servers", "tags");
|
|
assert_eq!(tags.r#type, "TEXT");
|
|
assert_eq!(tags.notnull, 1);
|
|
assert_eq!(normalize_default(&tags.default).as_deref(), Some("[]"));
|
|
|
|
let enabled = get_column_info(&conn, "prompts", "enabled");
|
|
assert_eq!(enabled.r#type, "BOOLEAN");
|
|
assert_eq!(enabled.notnull, 1);
|
|
assert_eq!(normalize_default(&enabled.default).as_deref(), Some("1"));
|
|
|
|
let installed_at = get_column_info(&conn, "skills", "installed_at");
|
|
assert_eq!(installed_at.r#type, "INTEGER");
|
|
assert_eq!(installed_at.notnull, 1);
|
|
assert_eq!(
|
|
normalize_default(&installed_at.default).as_deref(),
|
|
Some("0")
|
|
);
|
|
|
|
let branch = get_column_info(&conn, "skill_repos", "branch");
|
|
assert_eq!(branch.r#type, "TEXT");
|
|
assert_eq!(normalize_default(&branch.default).as_deref(), Some("main"));
|
|
|
|
let skill_repo_enabled = get_column_info(&conn, "skill_repos", "enabled");
|
|
assert_eq!(skill_repo_enabled.r#type, "BOOLEAN");
|
|
assert_eq!(skill_repo_enabled.notnull, 1);
|
|
assert_eq!(
|
|
normalize_default(&skill_repo_enabled.default).as_deref(),
|
|
Some("1")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dry_run_does_not_write_to_disk() {
|
|
// Create minimal valid config for migration
|
|
let mut apps = HashMap::new();
|
|
apps.insert("claude".to_string(), ProviderManager::default());
|
|
|
|
let config = MultiAppConfig {
|
|
version: 2,
|
|
apps,
|
|
mcp: Default::default(),
|
|
prompts: Default::default(),
|
|
skills: Default::default(),
|
|
common_config_snippets: Default::default(),
|
|
claude_common_config_snippet: None,
|
|
};
|
|
|
|
// Dry-run should succeed without any file I/O errors
|
|
let result = Database::migrate_from_json_dry_run(&config);
|
|
assert!(
|
|
result.is_ok(),
|
|
"Dry-run should succeed with valid config: {result:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dry_run_validates_schema_compatibility() {
|
|
// Create config with actual provider data
|
|
let mut providers = IndexMap::new();
|
|
providers.insert(
|
|
"test-provider".to_string(),
|
|
Provider {
|
|
id: "test-provider".to_string(),
|
|
name: "Test Provider".to_string(),
|
|
settings_config: json!({
|
|
"anthropicApiKey": "sk-test-123",
|
|
}),
|
|
website_url: None,
|
|
category: None,
|
|
created_at: Some(1234567890),
|
|
sort_index: None,
|
|
notes: None,
|
|
meta: None,
|
|
icon: None,
|
|
icon_color: None,
|
|
},
|
|
);
|
|
|
|
let mut manager = ProviderManager::default();
|
|
manager.providers = providers;
|
|
manager.current = "test-provider".to_string();
|
|
|
|
let mut apps = HashMap::new();
|
|
apps.insert("claude".to_string(), manager);
|
|
|
|
let config = MultiAppConfig {
|
|
version: 2,
|
|
apps,
|
|
mcp: Default::default(),
|
|
prompts: Default::default(),
|
|
skills: Default::default(),
|
|
common_config_snippets: Default::default(),
|
|
claude_common_config_snippet: None,
|
|
};
|
|
|
|
// Dry-run should validate the full migration path
|
|
let result = Database::migrate_from_json_dry_run(&config);
|
|
assert!(
|
|
result.is_ok(),
|
|
"Dry-run should succeed with provider data: {result:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn model_pricing_is_seeded_on_init() {
|
|
let db = Database::memory().expect("create memory db");
|
|
|
|
let conn = db.conn.lock().expect("lock conn");
|
|
|
|
let count: i64 = conn
|
|
.query_row("SELECT COUNT(*) FROM model_pricing", [], |row| row.get(0))
|
|
.expect("count pricing");
|
|
|
|
assert!(
|
|
count > 0,
|
|
"模型定价数据应该在初始化时自动填充,实际数量: {}",
|
|
count
|
|
);
|
|
|
|
// 验证包含 Claude 模型
|
|
let claude_count: i64 = conn
|
|
.query_row(
|
|
"SELECT COUNT(*) FROM model_pricing WHERE model_id LIKE 'claude-%'",
|
|
[],
|
|
|row| row.get(0),
|
|
)
|
|
.expect("check claude");
|
|
assert!(
|
|
claude_count > 0,
|
|
"应该包含 Claude 模型定价,实际数量: {}",
|
|
claude_count
|
|
);
|
|
|
|
// 验证包含 GPT 模型
|
|
let gpt_count: i64 = conn
|
|
.query_row(
|
|
"SELECT COUNT(*) FROM model_pricing WHERE model_id LIKE 'gpt-%'",
|
|
[],
|
|
|row| row.get(0),
|
|
)
|
|
.expect("check gpt");
|
|
assert!(
|
|
gpt_count > 0,
|
|
"应该包含 GPT 模型定价,实际数量: {}",
|
|
gpt_count
|
|
);
|
|
|
|
// 验证包含 Gemini 模型
|
|
let gemini_count: i64 = conn
|
|
.query_row(
|
|
"SELECT COUNT(*) FROM model_pricing WHERE model_id LIKE 'gemini-%'",
|
|
[],
|
|
|row| row.get(0),
|
|
)
|
|
.expect("check gemini");
|
|
assert!(
|
|
gemini_count > 0,
|
|
"应该包含 Gemini 模型定价,实际数量: {}",
|
|
gemini_count
|
|
);
|
|
}
|