mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
feat(skill): implement recursive scanning for skill repositories (#309)
Add recursive directory scanning to discover SKILL.md files in nested directories. When a SKILL.md is found, treat sibling directories as functional folders rather than separate skills.
This commit is contained in:
@@ -3,13 +3,15 @@ use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use cc_switch_lib::{
|
||||
get_claude_settings_path, read_json_file, AppError, AppType, ConfigService,
|
||||
MultiAppConfig, Provider, ProviderMeta,
|
||||
get_claude_settings_path, read_json_file, AppError, AppType, ConfigService, MultiAppConfig,
|
||||
Provider, ProviderMeta,
|
||||
};
|
||||
|
||||
#[path = "support.rs"]
|
||||
mod support;
|
||||
use support::{create_test_state, create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex};
|
||||
use support::{
|
||||
create_test_state, create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn sync_claude_provider_writes_live_settings() {
|
||||
|
||||
@@ -42,9 +42,13 @@ fn import_default_config_claude_persists_provider() {
|
||||
.expect("import default config succeeds");
|
||||
|
||||
// 验证内存状态
|
||||
let providers = state.db.get_all_providers(AppType::Claude.as_str())
|
||||
let providers = state
|
||||
.db
|
||||
.get_all_providers(AppType::Claude.as_str())
|
||||
.expect("get all providers");
|
||||
let current_id = state.db.get_current_provider(AppType::Claude.as_str())
|
||||
let current_id = state
|
||||
.db
|
||||
.get_current_provider(AppType::Claude.as_str())
|
||||
.expect("get current provider");
|
||||
assert_eq!(current_id.as_deref(), Some("default"));
|
||||
let default_provider = providers.get("default").expect("default provider");
|
||||
@@ -87,7 +91,9 @@ fn import_default_config_without_live_file_returns_error() {
|
||||
|
||||
// 使用数据库架构,不再检查 config.json
|
||||
// 失败的导入不应该向数据库写入任何供应商
|
||||
let providers = state.db.get_all_providers(AppType::Claude.as_str())
|
||||
let providers = state
|
||||
.db
|
||||
.get_all_providers(AppType::Claude.as_str())
|
||||
.expect("get all providers");
|
||||
assert!(
|
||||
providers.is_empty(),
|
||||
@@ -125,8 +131,7 @@ fn import_mcp_from_claude_creates_config_and_enables_servers() {
|
||||
"import should report inserted or normalized entries"
|
||||
);
|
||||
|
||||
let servers = state.db.get_all_mcp_servers()
|
||||
.expect("get all mcp servers");
|
||||
let servers = state.db.get_all_mcp_servers().expect("get all mcp servers");
|
||||
let entry = servers
|
||||
.get("echo")
|
||||
.expect("server imported into unified structure");
|
||||
@@ -168,8 +173,7 @@ fn import_mcp_from_claude_invalid_json_preserves_state() {
|
||||
}
|
||||
|
||||
// 使用数据库架构,检查 MCP 服务器未被写入
|
||||
let servers = state.db.get_all_mcp_servers()
|
||||
.expect("get all mcp servers");
|
||||
let servers = state.db.get_all_mcp_servers().expect("get all mcp servers");
|
||||
assert!(
|
||||
servers.is_empty(),
|
||||
"failed import should not persist any MCP servers to database"
|
||||
@@ -224,11 +228,8 @@ fn set_mcp_enabled_for_codex_writes_live_config() {
|
||||
McpService::toggle_app(&state, "codex-server", AppType::Codex, true)
|
||||
.expect("toggle_app should succeed");
|
||||
|
||||
let servers = state.db.get_all_mcp_servers()
|
||||
.expect("get all mcp servers");
|
||||
let entry = servers
|
||||
.get("codex-server")
|
||||
.expect("codex server exists");
|
||||
let servers = state.db.get_all_mcp_servers().expect("get all mcp servers");
|
||||
let entry = servers.get("codex-server").expect("codex server exists");
|
||||
assert!(
|
||||
entry.apps.codex,
|
||||
"server should have Codex app enabled after toggle"
|
||||
|
||||
@@ -7,8 +7,8 @@ use cc_switch_lib::{
|
||||
|
||||
#[path = "support.rs"]
|
||||
mod support;
|
||||
use support::{create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex};
|
||||
use std::collections::HashMap;
|
||||
use support::{create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex};
|
||||
|
||||
#[test]
|
||||
fn switch_provider_updates_codex_live_and_state() {
|
||||
@@ -104,16 +104,22 @@ command = "say"
|
||||
"config.toml should contain synced MCP servers"
|
||||
);
|
||||
|
||||
let current_id = app_state.db.get_current_provider(AppType::Codex.as_str())
|
||||
let current_id = app_state
|
||||
.db
|
||||
.get_current_provider(AppType::Codex.as_str())
|
||||
.expect("get current provider");
|
||||
assert_eq!(current_id.as_deref(), Some("new-provider"), "current provider updated");
|
||||
assert_eq!(
|
||||
current_id.as_deref(),
|
||||
Some("new-provider"),
|
||||
"current provider updated"
|
||||
);
|
||||
|
||||
let providers = app_state.db.get_all_providers(AppType::Codex.as_str())
|
||||
let providers = app_state
|
||||
.db
|
||||
.get_all_providers(AppType::Codex.as_str())
|
||||
.expect("get all providers");
|
||||
|
||||
let new_provider = providers
|
||||
.get("new-provider")
|
||||
.expect("new provider exists");
|
||||
let new_provider = providers.get("new-provider").expect("new provider exists");
|
||||
let new_config_text = new_provider
|
||||
.settings_config
|
||||
.get("config")
|
||||
@@ -165,7 +171,9 @@ fn switch_provider_missing_provider_returns_error() {
|
||||
|
||||
let err_str = err.to_string();
|
||||
assert!(
|
||||
err_str.contains("供应商不存在") || err_str.contains("Provider not found") || err_str.contains("missing-provider"),
|
||||
err_str.contains("供应商不存在")
|
||||
|| err_str.contains("Provider not found")
|
||||
|| err_str.contains("missing-provider"),
|
||||
"error message should mention missing provider, got: {err_str}"
|
||||
);
|
||||
}
|
||||
@@ -241,11 +249,19 @@ fn switch_provider_updates_claude_live_and_state() {
|
||||
"live settings.json should reflect new provider auth"
|
||||
);
|
||||
|
||||
let current_id = app_state.db.get_current_provider(AppType::Claude.as_str())
|
||||
let current_id = app_state
|
||||
.db
|
||||
.get_current_provider(AppType::Claude.as_str())
|
||||
.expect("get current provider");
|
||||
assert_eq!(current_id.as_deref(), Some("new-provider"), "current provider updated");
|
||||
assert_eq!(
|
||||
current_id.as_deref(),
|
||||
Some("new-provider"),
|
||||
"current provider updated"
|
||||
);
|
||||
|
||||
let providers = app_state.db.get_all_providers(AppType::Claude.as_str())
|
||||
let providers = app_state
|
||||
.db
|
||||
.get_all_providers(AppType::Claude.as_str())
|
||||
.expect("get all providers");
|
||||
|
||||
let legacy_provider = providers
|
||||
@@ -258,9 +274,7 @@ fn switch_provider_updates_claude_live_and_state() {
|
||||
"previous provider should be backfilled with live config"
|
||||
);
|
||||
|
||||
let new_provider = providers
|
||||
.get("new-provider")
|
||||
.expect("new provider exists");
|
||||
let new_provider = providers.get("new-provider").expect("new provider exists");
|
||||
assert_eq!(
|
||||
new_provider
|
||||
.settings_config
|
||||
@@ -283,7 +297,9 @@ fn switch_provider_updates_claude_live_and_state() {
|
||||
);
|
||||
|
||||
// 验证当前供应商已更新
|
||||
let current_id = app_state.db.get_current_provider(AppType::Claude.as_str())
|
||||
let current_id = app_state
|
||||
.db
|
||||
.get_current_provider(AppType::Claude.as_str())
|
||||
.expect("get current provider");
|
||||
assert_eq!(
|
||||
current_id.as_deref(),
|
||||
@@ -328,7 +344,9 @@ fn switch_provider_codex_missing_auth_returns_error_and_keeps_state() {
|
||||
other => panic!("expected config error, got {other:?}"),
|
||||
}
|
||||
|
||||
let current_id = app_state.db.get_current_provider(AppType::Codex.as_str())
|
||||
let current_id = app_state
|
||||
.db
|
||||
.get_current_provider(AppType::Codex.as_str())
|
||||
.expect("get current provider");
|
||||
// 切换失败后,由于数据库操作是先设置再验证,current 可能已被设为 "invalid"
|
||||
// 但由于 live 配置写入失败,状态应该回滚
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use serde_json::json;
|
||||
|
||||
use cc_switch_lib::{
|
||||
get_claude_settings_path, read_json_file, write_codex_live_atomic, AppError, AppType,
|
||||
McpApps, McpServer, MultiAppConfig, Provider, ProviderMeta, ProviderService,
|
||||
get_claude_settings_path, read_json_file, write_codex_live_atomic, AppError, AppType, McpApps,
|
||||
McpServer, MultiAppConfig, Provider, ProviderMeta, ProviderService,
|
||||
};
|
||||
|
||||
#[path = "support.rs"]
|
||||
mod support;
|
||||
use support::{create_test_state, create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex};
|
||||
use support::{
|
||||
create_test_state, create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex,
|
||||
};
|
||||
|
||||
fn sanitize_provider_name(name: &str) -> String {
|
||||
name.chars()
|
||||
@@ -69,7 +71,10 @@ command = "say"
|
||||
}
|
||||
|
||||
// 使用新的统一 MCP 结构(v3.7.0+)
|
||||
let servers = initial_config.mcp.servers.get_or_insert_with(Default::default);
|
||||
let servers = initial_config
|
||||
.mcp
|
||||
.servers
|
||||
.get_or_insert_with(Default::default);
|
||||
servers.insert(
|
||||
"echo-server".into(),
|
||||
McpServer {
|
||||
@@ -111,16 +116,22 @@ command = "say"
|
||||
"config.toml should contain synced MCP servers"
|
||||
);
|
||||
|
||||
let current_id = state.db.get_current_provider(AppType::Codex.as_str())
|
||||
let current_id = state
|
||||
.db
|
||||
.get_current_provider(AppType::Codex.as_str())
|
||||
.expect("read current provider after switch");
|
||||
assert_eq!(current_id.as_deref(), Some("new-provider"), "current provider updated");
|
||||
assert_eq!(
|
||||
current_id.as_deref(),
|
||||
Some("new-provider"),
|
||||
"current provider updated"
|
||||
);
|
||||
|
||||
let providers = state.db.get_all_providers(AppType::Codex.as_str())
|
||||
let providers = state
|
||||
.db
|
||||
.get_all_providers(AppType::Codex.as_str())
|
||||
.expect("read providers after switch");
|
||||
|
||||
let new_provider = providers
|
||||
.get("new-provider")
|
||||
.expect("new provider exists");
|
||||
let new_provider = providers.get("new-provider").expect("new provider exists");
|
||||
let new_config_text = new_provider
|
||||
.settings_config
|
||||
.get("config")
|
||||
@@ -385,11 +396,19 @@ fn provider_service_switch_claude_updates_live_and_state() {
|
||||
"live settings.json should reflect new provider auth"
|
||||
);
|
||||
|
||||
let providers = state.db.get_all_providers(AppType::Claude.as_str())
|
||||
let providers = state
|
||||
.db
|
||||
.get_all_providers(AppType::Claude.as_str())
|
||||
.expect("get all providers");
|
||||
let current_id = state.db.get_current_provider(AppType::Claude.as_str())
|
||||
let current_id = state
|
||||
.db
|
||||
.get_current_provider(AppType::Claude.as_str())
|
||||
.expect("get current provider");
|
||||
assert_eq!(current_id.as_deref(), Some("new-provider"), "current provider updated");
|
||||
assert_eq!(
|
||||
current_id.as_deref(),
|
||||
Some("new-provider"),
|
||||
"current provider updated"
|
||||
);
|
||||
|
||||
let legacy_provider = providers
|
||||
.get("old-provider")
|
||||
@@ -509,7 +528,9 @@ fn provider_service_delete_codex_removes_provider_and_files() {
|
||||
ProviderService::delete(&app_state, AppType::Codex, "to-delete")
|
||||
.expect("delete provider should succeed");
|
||||
|
||||
let providers = app_state.db.get_all_providers(AppType::Codex.as_str())
|
||||
let providers = app_state
|
||||
.db
|
||||
.get_all_providers(AppType::Codex.as_str())
|
||||
.expect("get all providers");
|
||||
assert!(
|
||||
!providers.contains_key("to-delete"),
|
||||
@@ -567,7 +588,9 @@ fn provider_service_delete_claude_removes_provider_files() {
|
||||
|
||||
ProviderService::delete(&app_state, AppType::Claude, "delete").expect("delete claude provider");
|
||||
|
||||
let providers = app_state.db.get_all_providers(AppType::Claude.as_str())
|
||||
let providers = app_state
|
||||
.db
|
||||
.get_all_providers(AppType::Claude.as_str())
|
||||
.expect("get all providers");
|
||||
assert!(
|
||||
!providers.contains_key("delete"),
|
||||
@@ -608,15 +631,18 @@ fn provider_service_delete_current_provider_returns_error() {
|
||||
.expect_err("deleting current provider should fail");
|
||||
match err {
|
||||
AppError::Localized { zh, .. } => assert!(
|
||||
zh.contains("不能删除当前正在使用的供应商") || zh.contains("无法删除当前正在使用的供应商"),
|
||||
zh.contains("不能删除当前正在使用的供应商")
|
||||
|| zh.contains("无法删除当前正在使用的供应商"),
|
||||
"unexpected message: {zh}"
|
||||
),
|
||||
AppError::Config(msg) => assert!(
|
||||
msg.contains("不能删除当前正在使用的供应商") || msg.contains("无法删除当前正在使用的供应商"),
|
||||
msg.contains("不能删除当前正在使用的供应商")
|
||||
|| msg.contains("无法删除当前正在使用的供应商"),
|
||||
"unexpected message: {msg}"
|
||||
),
|
||||
AppError::Message(msg) => assert!(
|
||||
msg.contains("不能删除当前正在使用的供应商") || msg.contains("无法删除当前正在使用的供应商"),
|
||||
msg.contains("不能删除当前正在使用的供应商")
|
||||
|| msg.contains("无法删除当前正在使用的供应商"),
|
||||
"unexpected message: {msg}"
|
||||
),
|
||||
other => panic!("expected Config/Message error, got {other:?}"),
|
||||
|
||||
Reference in New Issue
Block a user