refactor(mcp): preserve TOML formatting when syncing to Codex
Switch from `toml` to `toml_edit` crate for incremental TOML editing, preserving user-written comments, whitespace, and key ordering in Codex config.toml. Changes: - Add toml_edit 0.22 dependency for preserving-style TOML editing - Refactor sync_enabled_to_codex() to use toml_edit::DocumentMut API - Implement smart style detection: inherit existing mcp.servers or mcp_servers style, with fallback to sensible defaults - Add deduplication logic to prevent both styles coexisting - Add tests for format preservation and style inheritance - Fix unused_mut and nonminimal_bool compiler warnings - Apply cargo fmt to all modified files Benefits: - User comments and formatting are no longer lost during sync - Respects user's preferred TOML structure (nested vs toplevel) - Non-MCP fields in config.toml remain untouched - Minimal surprise principle: only modifies necessary sections Testing: - All 47 tests passing (unit + integration) - Clippy clean (0 warnings, 0 errors) - Release build successful - New tests verify comment preservation and style detection
This commit is contained in:
36
src-tauri/Cargo.lock
generated
36
src-tauri/Cargo.lock
generated
@@ -588,6 +588,7 @@ dependencies = [
|
|||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml 0.8.2",
|
"toml 0.8.2",
|
||||||
|
"toml_edit 0.22.27",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1573,7 +1574,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
|
checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
"proc-macro-crate 2.0.2",
|
"proc-macro-crate 2.0.0",
|
||||||
"proc-macro-error",
|
"proc-macro-error",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -3110,11 +3111,10 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "2.0.2"
|
version = "2.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
|
checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"toml_datetime 0.6.3",
|
|
||||||
"toml_edit 0.20.2",
|
"toml_edit 0.20.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4876,7 +4876,7 @@ checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned 0.6.9",
|
"serde_spanned 0.6.9",
|
||||||
"toml_datetime 0.6.3",
|
"toml_datetime 0.6.11",
|
||||||
"toml_edit 0.20.2",
|
"toml_edit 0.20.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4897,9 +4897,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "0.6.3"
|
version = "0.6.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
|
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -4920,7 +4920,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.11.4",
|
"indexmap 2.11.4",
|
||||||
"toml_datetime 0.6.3",
|
"toml_datetime 0.6.11",
|
||||||
"winnow 0.5.40",
|
"winnow 0.5.40",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4933,10 +4933,22 @@ dependencies = [
|
|||||||
"indexmap 2.11.4",
|
"indexmap 2.11.4",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned 0.6.9",
|
"serde_spanned 0.6.9",
|
||||||
"toml_datetime 0.6.3",
|
"toml_datetime 0.6.11",
|
||||||
"winnow 0.5.40",
|
"winnow 0.5.40",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.22.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap 2.11.4",
|
||||||
|
"toml_datetime 0.6.11",
|
||||||
|
"toml_write",
|
||||||
|
"winnow 0.7.13",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.23.6"
|
version = "0.23.6"
|
||||||
@@ -4958,6 +4970,12 @@ dependencies = [
|
|||||||
"winnow 0.7.13",
|
"winnow 0.7.13",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_write"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_writer"
|
name = "toml_writer"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ tauri-plugin-dialog = "2"
|
|||||||
tauri-plugin-store = "2"
|
tauri-plugin-store = "2"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
|
toml_edit = "0.22"
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
|
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
|
||||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
|
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ fn switch_provider_internal(
|
|||||||
Some(app_type_str.clone()),
|
Some(app_type_str.clone()),
|
||||||
provider_id,
|
provider_id,
|
||||||
)
|
)
|
||||||
.map_err(AppError::Message)?;
|
.map_err(AppError::Message)?;
|
||||||
|
|
||||||
// 切换成功后重新创建托盘菜单
|
// 切换成功后重新创建托盘菜单
|
||||||
if let Ok(new_menu) = create_tray_menu(app, app_state.inner()) {
|
if let Ok(new_menu) = create_tray_menu(app, app_state.inner()) {
|
||||||
|
|||||||
@@ -573,167 +573,148 @@ pub fn import_from_codex(config: &mut MultiAppConfig) -> Result<usize, AppError>
|
|||||||
/// - 仅更新 `mcp.servers` 或 `mcp_servers` 子表,保留 `mcp` 其它键
|
/// - 仅更新 `mcp.servers` 或 `mcp_servers` 子表,保留 `mcp` 其它键
|
||||||
/// - 仅写入启用项;无启用项时清理对应子表
|
/// - 仅写入启用项;无启用项时清理对应子表
|
||||||
pub fn sync_enabled_to_codex(config: &MultiAppConfig) -> Result<(), AppError> {
|
pub fn sync_enabled_to_codex(config: &MultiAppConfig) -> Result<(), AppError> {
|
||||||
use toml::{value::Value as TomlValue, Table as TomlTable};
|
use toml_edit::{DocumentMut, Item, Table};
|
||||||
|
|
||||||
// 1) 收集启用项(Codex 维度)
|
// 1) 收集启用项(Codex 维度)
|
||||||
let enabled = collect_enabled_servers(&config.mcp.codex);
|
let enabled = collect_enabled_servers(&config.mcp.codex);
|
||||||
|
|
||||||
// 2) 读取现有 config.toml 并解析为 Table(允许空文件)
|
// 2) 读取现有 config.toml 文本;保持无效 TOML 的错误返回(不覆盖文件)
|
||||||
let base_text = crate::codex_config::read_and_validate_codex_config_text()?;
|
let base_text = crate::codex_config::read_and_validate_codex_config_text()?;
|
||||||
let mut root: TomlTable = if base_text.trim().is_empty() {
|
|
||||||
TomlTable::new()
|
// 3) 使用 toml_edit 解析(允许空文件)
|
||||||
|
let mut doc: DocumentMut = if base_text.trim().is_empty() {
|
||||||
|
DocumentMut::default()
|
||||||
} else {
|
} else {
|
||||||
toml::from_str::<TomlTable>(&base_text)
|
base_text
|
||||||
|
.parse::<DocumentMut>()
|
||||||
.map_err(|e| AppError::McpValidation(format!("解析 config.toml 失败: {}", e)))?
|
.map_err(|e| AppError::McpValidation(format!("解析 config.toml 失败: {}", e)))?
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3) 写入 servers 表(支持 mcp.servers 与 mcp_servers;优先沿用已有风格,默认 mcp_servers)
|
enum Target {
|
||||||
let prefer_mcp_servers = root.get("mcp_servers").is_some() || root.get("mcp").is_none();
|
McpServers, // 顶层 mcp_servers
|
||||||
if enabled.is_empty() {
|
McpDotServers, // mcp.servers
|
||||||
// 无启用项:移除两种节点
|
}
|
||||||
// 清除 mcp.servers,但保留其他 mcp 字段
|
|
||||||
let mut should_drop_mcp = false;
|
|
||||||
if let Some(mcp_val) = root.get_mut("mcp") {
|
|
||||||
match mcp_val {
|
|
||||||
TomlValue::Table(tbl) => {
|
|
||||||
tbl.remove("servers");
|
|
||||||
should_drop_mcp = tbl.is_empty();
|
|
||||||
}
|
|
||||||
_ => should_drop_mcp = true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if should_drop_mcp {
|
|
||||||
root.remove("mcp");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清除顶层 mcp_servers
|
// 4) 选择目标风格:优先沿用既有子表;其次在 mcp 表下新建;最后退回顶层 mcp_servers
|
||||||
root.remove("mcp_servers");
|
let has_mcp_dot_servers = doc
|
||||||
|
.get("mcp")
|
||||||
|
.and_then(|m| m.get("servers"))
|
||||||
|
.and_then(|s| s.as_table_like())
|
||||||
|
.is_some();
|
||||||
|
let has_mcp_servers = doc
|
||||||
|
.get("mcp_servers")
|
||||||
|
.and_then(|s| s.as_table_like())
|
||||||
|
.is_some();
|
||||||
|
let mcp_is_table = doc.get("mcp").and_then(|m| m.as_table_like()).is_some();
|
||||||
|
|
||||||
|
let target = if has_mcp_dot_servers {
|
||||||
|
Target::McpDotServers
|
||||||
|
} else if has_mcp_servers {
|
||||||
|
Target::McpServers
|
||||||
|
} else if mcp_is_table {
|
||||||
|
Target::McpDotServers
|
||||||
} else {
|
} else {
|
||||||
let mut servers_tbl = TomlTable::new();
|
Target::McpServers
|
||||||
|
};
|
||||||
|
|
||||||
for (id, spec) in enabled.iter() {
|
// 构造目标 servers 表(稳定的键顺序)
|
||||||
let mut s = TomlTable::new();
|
let build_servers_table = || -> Table {
|
||||||
|
let mut servers = Table::new();
|
||||||
// 类型(缺省视为 stdio)
|
let mut ids: Vec<_> = enabled.keys().cloned().collect();
|
||||||
|
ids.sort();
|
||||||
|
for id in ids {
|
||||||
|
let spec = enabled.get(&id).expect("spec must exist");
|
||||||
|
let mut t = Table::new();
|
||||||
let typ = spec.get("type").and_then(|v| v.as_str()).unwrap_or("stdio");
|
let typ = spec.get("type").and_then(|v| v.as_str()).unwrap_or("stdio");
|
||||||
s.insert("type".into(), TomlValue::String(typ.to_string()));
|
t["type"] = toml_edit::value(typ);
|
||||||
|
|
||||||
match typ {
|
match typ {
|
||||||
"stdio" => {
|
"stdio" => {
|
||||||
let cmd = spec
|
let cmd = spec.get("command").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
.get("command")
|
t["command"] = toml_edit::value(cmd);
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_string();
|
|
||||||
s.insert("command".into(), TomlValue::String(cmd));
|
|
||||||
|
|
||||||
if let Some(args) = spec.get("args").and_then(|v| v.as_array()) {
|
if let Some(args) = spec.get("args").and_then(|v| v.as_array()) {
|
||||||
let arr = args
|
let mut arr_v = toml_edit::Array::default();
|
||||||
.iter()
|
for a in args.iter().filter_map(|x| x.as_str()) {
|
||||||
.filter_map(|x| x.as_str())
|
arr_v.push(a);
|
||||||
.map(|x| TomlValue::String(x.to_string()))
|
}
|
||||||
.collect::<Vec<_>>();
|
if !arr_v.is_empty() {
|
||||||
if !arr.is_empty() {
|
t["args"] = toml_edit::Item::Value(toml_edit::Value::Array(arr_v));
|
||||||
s.insert("args".into(), TomlValue::Array(arr));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(cwd) = spec.get("cwd").and_then(|v| v.as_str()) {
|
if let Some(cwd) = spec.get("cwd").and_then(|v| v.as_str()) {
|
||||||
if !cwd.trim().is_empty() {
|
if !cwd.trim().is_empty() {
|
||||||
s.insert("cwd".into(), TomlValue::String(cwd.to_string()));
|
t["cwd"] = toml_edit::value(cwd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(env) = spec.get("env").and_then(|v| v.as_object()) {
|
if let Some(env) = spec.get("env").and_then(|v| v.as_object()) {
|
||||||
let mut env_tbl = TomlTable::new();
|
let mut env_tbl = Table::new();
|
||||||
for (k, v) in env.iter() {
|
for (k, v) in env.iter() {
|
||||||
if let Some(sv) = v.as_str() {
|
if let Some(s) = v.as_str() {
|
||||||
env_tbl.insert(k.clone(), TomlValue::String(sv.to_string()));
|
env_tbl[&k[..]] = toml_edit::value(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !env_tbl.is_empty() {
|
if !env_tbl.is_empty() {
|
||||||
s.insert("env".into(), TomlValue::Table(env_tbl));
|
t["env"] = Item::Table(env_tbl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"http" => {
|
"http" => {
|
||||||
let url = spec
|
let url = spec.get("url").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
.get("url")
|
t["url"] = toml_edit::value(url);
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_string();
|
|
||||||
s.insert("url".into(), TomlValue::String(url));
|
|
||||||
|
|
||||||
if let Some(headers) = spec.get("headers").and_then(|v| v.as_object()) {
|
if let Some(headers) = spec.get("headers").and_then(|v| v.as_object()) {
|
||||||
let mut h_tbl = TomlTable::new();
|
let mut h_tbl = Table::new();
|
||||||
for (k, v) in headers.iter() {
|
for (k, v) in headers.iter() {
|
||||||
if let Some(sv) = v.as_str() {
|
if let Some(s) = v.as_str() {
|
||||||
h_tbl.insert(k.clone(), TomlValue::String(sv.to_string()));
|
h_tbl[&k[..]] = toml_edit::value(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !h_tbl.is_empty() {
|
if !h_tbl.is_empty() {
|
||||||
s.insert("headers".into(), TomlValue::Table(h_tbl));
|
t["headers"] = Item::Table(h_tbl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
servers[&id[..]] = Item::Table(t);
|
||||||
servers_tbl.insert(id.clone(), TomlValue::Table(s));
|
|
||||||
}
|
}
|
||||||
|
servers
|
||||||
|
};
|
||||||
|
|
||||||
let servers_value = TomlValue::Table(servers_tbl.clone());
|
// 5) 应用更新:仅就地更新目标子表;避免改动其它键/注释/空白
|
||||||
|
if enabled.is_empty() {
|
||||||
if prefer_mcp_servers {
|
// 无启用项:移除两种 servers 表(如果存在),但保留 mcp 其它字段
|
||||||
root.insert("mcp_servers".into(), servers_value);
|
if let Some(mcp_item) = doc.get_mut("mcp") {
|
||||||
|
if let Some(tbl) = mcp_item.as_table_like_mut() {
|
||||||
// 若存在 mcp,则仅移除 servers 字段,保留其他键
|
tbl.remove("servers");
|
||||||
let mut should_drop_mcp = false;
|
}
|
||||||
if let Some(mcp_val) = root.get_mut("mcp") {
|
}
|
||||||
match mcp_val {
|
doc.as_table_mut().remove("mcp_servers");
|
||||||
TomlValue::Table(tbl) => {
|
} else {
|
||||||
|
let servers_tbl = build_servers_table();
|
||||||
|
match target {
|
||||||
|
Target::McpDotServers => {
|
||||||
|
// 确保 mcp 为表
|
||||||
|
if doc.get("mcp").and_then(|m| m.as_table_like()).is_none() {
|
||||||
|
doc["mcp"] = Item::Table(Table::new());
|
||||||
|
}
|
||||||
|
doc["mcp"]["servers"] = Item::Table(servers_tbl);
|
||||||
|
// 去重:若存在顶层 mcp_servers,则移除以避免重复定义
|
||||||
|
doc.as_table_mut().remove("mcp_servers");
|
||||||
|
}
|
||||||
|
Target::McpServers => {
|
||||||
|
doc["mcp_servers"] = Item::Table(servers_tbl);
|
||||||
|
// 去重:若存在 mcp.servers,则移除该子表,保留 mcp 其它键
|
||||||
|
if let Some(mcp_item) = doc.get_mut("mcp") {
|
||||||
|
if let Some(tbl) = mcp_item.as_table_like_mut() {
|
||||||
tbl.remove("servers");
|
tbl.remove("servers");
|
||||||
should_drop_mcp = tbl.is_empty();
|
|
||||||
}
|
|
||||||
_ => should_drop_mcp = true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if should_drop_mcp {
|
|
||||||
root.remove("mcp");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let mut inserted = false;
|
|
||||||
|
|
||||||
if let Some(mcp_val) = root.get_mut("mcp") {
|
|
||||||
match mcp_val {
|
|
||||||
TomlValue::Table(tbl) => {
|
|
||||||
tbl.insert("servers".into(), TomlValue::Table(servers_tbl.clone()));
|
|
||||||
inserted = true;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let mut mcp_tbl = TomlTable::new();
|
|
||||||
mcp_tbl.insert("servers".into(), TomlValue::Table(servers_tbl.clone()));
|
|
||||||
*mcp_val = TomlValue::Table(mcp_tbl);
|
|
||||||
inserted = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !inserted {
|
|
||||||
let mut mcp_tbl = TomlTable::new();
|
|
||||||
mcp_tbl.insert("servers".into(), TomlValue::Table(servers_tbl));
|
|
||||||
root.insert("mcp".into(), TomlValue::Table(mcp_tbl));
|
|
||||||
}
|
|
||||||
|
|
||||||
root.remove("mcp_servers");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) 序列化并写回 config.toml(仅改 TOML,不触碰 auth.json)
|
// 6) 写回(仅改 TOML,不触碰 auth.json);toml_edit 会尽量保留未改区域的注释/空白/顺序
|
||||||
let new_text = toml::to_string(&TomlValue::Table(root))
|
let new_text = doc.to_string();
|
||||||
.map_err(|e| AppError::McpValidation(format!("序列化 config.toml 失败: {}", e)))?;
|
|
||||||
let path = crate::codex_config::get_codex_config_path();
|
let path = crate::codex_config::get_codex_config_path();
|
||||||
crate::config::write_text_file(&path, &new_text)?;
|
crate::config::write_text_file(&path, &new_text)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,6 +179,100 @@ fn sync_enabled_to_codex_writes_enabled_servers() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sync_enabled_to_codex_preserves_non_mcp_content_and_style() {
|
||||||
|
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||||
|
reset_test_fs();
|
||||||
|
|
||||||
|
// 预置含有顶层注释与非 MCP 键的 config.toml
|
||||||
|
let path = cc_switch_lib::get_codex_config_path();
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent).expect("create codex dir");
|
||||||
|
}
|
||||||
|
let seed = r#"# top-comment
|
||||||
|
title = "keep-me"
|
||||||
|
|
||||||
|
[profile]
|
||||||
|
mode = "dev"
|
||||||
|
"#;
|
||||||
|
fs::write(&path, seed).expect("seed config.toml");
|
||||||
|
|
||||||
|
// 启用一个 MCP 项,触发增量写入
|
||||||
|
let mut config = MultiAppConfig::default();
|
||||||
|
config.mcp.codex.servers.insert(
|
||||||
|
"echo".into(),
|
||||||
|
json!({
|
||||||
|
"id": "echo",
|
||||||
|
"enabled": true,
|
||||||
|
"server": { "type": "stdio", "command": "echo" }
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
cc_switch_lib::sync_enabled_to_codex(&config).expect("sync codex");
|
||||||
|
|
||||||
|
let text = fs::read_to_string(&path).expect("read config.toml");
|
||||||
|
// 顶层注释与非 MCP 键应保留
|
||||||
|
assert!(
|
||||||
|
text.contains("# top-comment"),
|
||||||
|
"top comment should be preserved"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
text.contains("title = \"keep-me\""),
|
||||||
|
"top key should be preserved"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
text.contains("[profile]"),
|
||||||
|
"non-MCP table should be preserved"
|
||||||
|
);
|
||||||
|
// 新增的 mcp_servers/或 mcp.servers 应存在并包含 echo
|
||||||
|
assert!(
|
||||||
|
text.contains("mcp_servers") || text.contains("[mcp.servers]"),
|
||||||
|
"one server table style should be present"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
text.contains("echo") && text.contains("command = \"echo\""),
|
||||||
|
"echo server should be serialized"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sync_enabled_to_codex_keeps_existing_style_mcp_dot_servers() {
|
||||||
|
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||||
|
reset_test_fs();
|
||||||
|
let path = cc_switch_lib::get_codex_config_path();
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent).expect("create codex dir");
|
||||||
|
}
|
||||||
|
// 预置 mcp.servers 风格
|
||||||
|
let seed = r#"[mcp]
|
||||||
|
other = "keep"
|
||||||
|
[mcp.servers]
|
||||||
|
"#;
|
||||||
|
fs::write(&path, seed).expect("seed config.toml");
|
||||||
|
|
||||||
|
let mut config = MultiAppConfig::default();
|
||||||
|
config.mcp.codex.servers.insert(
|
||||||
|
"echo".into(),
|
||||||
|
json!({
|
||||||
|
"id": "echo",
|
||||||
|
"enabled": true,
|
||||||
|
"server": { "type": "stdio", "command": "echo" }
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
cc_switch_lib::sync_enabled_to_codex(&config).expect("sync codex");
|
||||||
|
let text = fs::read_to_string(&path).expect("read config.toml");
|
||||||
|
// 仍应采用 mcp.servers 风格
|
||||||
|
assert!(
|
||||||
|
text.contains("[mcp.servers]"),
|
||||||
|
"should keep mcp.servers style"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!text.contains("mcp_servers"),
|
||||||
|
"should not switch to mcp_servers"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sync_enabled_to_codex_removes_servers_when_none_enabled() {
|
fn sync_enabled_to_codex_removes_servers_when_none_enabled() {
|
||||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||||
|
|||||||
Reference in New Issue
Block a user