fix(mcp): migrate import functions to unified v3.7.0 structure
- Rewrite import_from_claude/codex/gemini to write directly to mcp.servers - Implement skip-on-error strategy for fault tolerance (single invalid item no longer aborts entire batch) - Smart merge logic: existing servers only enable corresponding app, preserve other configs - Remove deprecated markers from service layer - Export McpApps type for test usage - Update mcp_commands tests to use unified structure Fixes runtime import issue where data was written to legacy structure (mcp.claude/codex.servers) but unified panel reads from new structure (mcp.servers), causing "imported but invisible" bug.
This commit is contained in:
@@ -18,7 +18,7 @@ mod settings;
|
||||
mod store;
|
||||
mod usage_script;
|
||||
|
||||
pub use app_config::{AppType, McpServer, MultiAppConfig};
|
||||
pub use app_config::{AppType, McpApps, McpServer, MultiAppConfig};
|
||||
pub use codex_config::{get_codex_auth_path, get_codex_config_path, write_codex_live_atomic};
|
||||
pub use commands::*;
|
||||
pub use config::{get_claude_mcp_path, get_claude_settings_path, read_json_file};
|
||||
|
||||
@@ -324,92 +324,101 @@ pub fn sync_enabled_to_claude(config: &MultiAppConfig) -> Result<(), AppError> {
|
||||
crate::claude_mcp::set_mcp_servers_map(&enabled)
|
||||
}
|
||||
|
||||
/// 从 ~/.claude.json 导入 mcpServers 到 config.json(设为 enabled=true)。
|
||||
/// 已存在的项仅强制 enabled=true,不覆盖其他字段。
|
||||
/// 从 ~/.claude.json 导入 mcpServers 到统一结构(v3.7.0+)
|
||||
/// 已存在的服务器将启用 Claude 应用,不覆盖其他字段和应用状态
|
||||
pub fn import_from_claude(config: &mut MultiAppConfig) -> Result<usize, AppError> {
|
||||
use crate::app_config::{McpApps, McpServer};
|
||||
|
||||
let text_opt = crate::claude_mcp::read_mcp_json()?;
|
||||
let Some(text) = text_opt else { return Ok(0) };
|
||||
let mut changed = normalize_servers_for(config, &AppType::Claude);
|
||||
|
||||
let v: Value = serde_json::from_str(&text)
|
||||
.map_err(|e| AppError::McpValidation(format!("解析 ~/.claude.json 失败: {e}")))?;
|
||||
let Some(map) = v.get("mcpServers").and_then(|x| x.as_object()) else {
|
||||
return Ok(changed);
|
||||
return Ok(0);
|
||||
};
|
||||
|
||||
// 确保新结构存在
|
||||
if config.mcp.servers.is_none() {
|
||||
config.mcp.servers = Some(HashMap::new());
|
||||
}
|
||||
let servers = config.mcp.servers.as_mut().unwrap();
|
||||
|
||||
let mut changed = 0;
|
||||
let mut errors = Vec::new();
|
||||
|
||||
for (id, spec) in map.iter() {
|
||||
// 校验目标 spec
|
||||
validate_server_spec(spec)?;
|
||||
|
||||
let entry = config
|
||||
.mcp_for_mut(&AppType::Claude)
|
||||
.servers
|
||||
.entry(id.clone());
|
||||
use std::collections::hash_map::Entry;
|
||||
match entry {
|
||||
Entry::Vacant(vac) => {
|
||||
let mut obj = serde_json::Map::new();
|
||||
obj.insert(String::from("id"), json!(id));
|
||||
obj.insert(String::from("name"), json!(id));
|
||||
obj.insert(String::from("server"), spec.clone());
|
||||
obj.insert(String::from("enabled"), json!(true));
|
||||
vac.insert(Value::Object(obj));
|
||||
changed += 1;
|
||||
}
|
||||
Entry::Occupied(mut occ) => {
|
||||
let value = occ.get_mut();
|
||||
let Some(existing) = value.as_object_mut() else {
|
||||
log::warn!("MCP 条目 '{id}' 不是 JSON 对象,覆盖为导入数据");
|
||||
let mut obj = serde_json::Map::new();
|
||||
obj.insert(String::from("id"), json!(id));
|
||||
obj.insert(String::from("name"), json!(id));
|
||||
obj.insert(String::from("server"), spec.clone());
|
||||
obj.insert(String::from("enabled"), json!(true));
|
||||
occ.insert(Value::Object(obj));
|
||||
changed += 1;
|
||||
// 校验:单项失败不中止,收集错误继续处理
|
||||
if let Err(e) = validate_server_spec(spec) {
|
||||
log::warn!("跳过无效 MCP 服务器 '{id}': {e}");
|
||||
errors.push(format!("{id}: {e}"));
|
||||
continue;
|
||||
};
|
||||
}
|
||||
|
||||
let mut modified = false;
|
||||
let prev_enabled = existing
|
||||
.get("enabled")
|
||||
.and_then(|b| b.as_bool())
|
||||
.unwrap_or(false);
|
||||
if !prev_enabled {
|
||||
existing.insert(String::from("enabled"), json!(true));
|
||||
modified = true;
|
||||
}
|
||||
if existing.get("server").is_none() {
|
||||
log::warn!("MCP 条目 '{id}' 缺少 server 字段,覆盖为导入数据");
|
||||
existing.insert(String::from("server"), spec.clone());
|
||||
modified = true;
|
||||
}
|
||||
if existing.get("id").is_none() {
|
||||
log::warn!("MCP 条目 '{id}' 缺少 id 字段,自动填充");
|
||||
existing.insert(String::from("id"), json!(id));
|
||||
modified = true;
|
||||
}
|
||||
if modified {
|
||||
if let Some(existing) = servers.get_mut(id) {
|
||||
// 已存在:仅启用 Claude 应用
|
||||
if !existing.apps.claude {
|
||||
existing.apps.claude = true;
|
||||
changed += 1;
|
||||
log::info!("MCP 服务器 '{id}' 已启用 Claude 应用");
|
||||
}
|
||||
} else {
|
||||
// 新建服务器:默认仅启用 Claude
|
||||
servers.insert(
|
||||
id.clone(),
|
||||
McpServer {
|
||||
id: id.clone(),
|
||||
name: id.clone(),
|
||||
server: spec.clone(),
|
||||
apps: McpApps {
|
||||
claude: true,
|
||||
codex: false,
|
||||
gemini: false,
|
||||
},
|
||||
description: None,
|
||||
homepage: None,
|
||||
docs: None,
|
||||
tags: Vec::new(),
|
||||
},
|
||||
);
|
||||
changed += 1;
|
||||
log::info!("导入新 MCP 服务器 '{id}'");
|
||||
}
|
||||
}
|
||||
|
||||
if !errors.is_empty() {
|
||||
log::warn!(
|
||||
"导入完成,但有 {} 项失败: {:?}",
|
||||
errors.len(),
|
||||
errors
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
/// 从 ~/.codex/config.toml 导入 MCP 到 config.json(Codex 作用域),并将导入项设为 enabled=true。
|
||||
/// 支持两种 schema:[mcp.servers.<id>] 与 [mcp_servers.<id>]。
|
||||
/// 已存在的项仅强制 enabled=true,不覆盖其他字段。
|
||||
/// 从 ~/.codex/config.toml 导入 MCP 到统一结构(v3.7.0+)
|
||||
/// 支持两种 schema:[mcp.servers.<id>] 与 [mcp_servers.<id>]
|
||||
/// 已存在的服务器将启用 Codex 应用,不覆盖其他字段和应用状态
|
||||
pub fn import_from_codex(config: &mut MultiAppConfig) -> Result<usize, AppError> {
|
||||
use crate::app_config::{McpApps, McpServer};
|
||||
|
||||
let text = crate::codex_config::read_and_validate_codex_config_text()?;
|
||||
if text.trim().is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
let mut changed_total = normalize_servers_for(config, &AppType::Codex);
|
||||
|
||||
let root: toml::Table = toml::from_str(&text)
|
||||
.map_err(|e| AppError::McpValidation(format!("解析 ~/.codex/config.toml 失败: {e}")))?;
|
||||
|
||||
// 确保新结构存在
|
||||
if config.mcp.servers.is_none() {
|
||||
config.mcp.servers = Some(HashMap::new());
|
||||
}
|
||||
let servers = config.mcp.servers.as_mut().unwrap();
|
||||
|
||||
let mut changed_total = 0usize;
|
||||
|
||||
// helper:处理一组 servers 表
|
||||
let mut import_servers_tbl = |servers_tbl: &toml::value::Table| {
|
||||
let mut changed = 0usize;
|
||||
@@ -476,70 +485,48 @@ pub fn import_from_codex(config: &mut MultiAppConfig) -> Result<usize, AppError>
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
_ => {
|
||||
log::warn!("跳过未知类型 '{typ}' 的 Codex MCP 项 '{id}'");
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
|
||||
let spec_v = serde_json::Value::Object(spec);
|
||||
|
||||
// 校验
|
||||
// 校验:单项失败继续处理
|
||||
if let Err(e) = validate_server_spec(&spec_v) {
|
||||
log::warn!("跳过无效 Codex MCP 项 '{id}': {e}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 合并:仅强制 enabled=true
|
||||
use std::collections::hash_map::Entry;
|
||||
let entry = config
|
||||
.mcp_for_mut(&AppType::Codex)
|
||||
.servers
|
||||
.entry(id.clone());
|
||||
match entry {
|
||||
Entry::Vacant(vac) => {
|
||||
let mut obj = serde_json::Map::new();
|
||||
obj.insert(String::from("id"), json!(id));
|
||||
obj.insert(String::from("name"), json!(id));
|
||||
obj.insert(String::from("server"), spec_v.clone());
|
||||
obj.insert(String::from("enabled"), json!(true));
|
||||
vac.insert(serde_json::Value::Object(obj));
|
||||
if let Some(existing) = servers.get_mut(id) {
|
||||
// 已存在:仅启用 Codex 应用
|
||||
if !existing.apps.codex {
|
||||
existing.apps.codex = true;
|
||||
changed += 1;
|
||||
log::info!("MCP 服务器 '{id}' 已启用 Codex 应用");
|
||||
}
|
||||
Entry::Occupied(mut occ) => {
|
||||
let value = occ.get_mut();
|
||||
let Some(existing) = value.as_object_mut() else {
|
||||
log::warn!("MCP 条目 '{id}' 不是 JSON 对象,覆盖为导入数据");
|
||||
let mut obj = serde_json::Map::new();
|
||||
obj.insert(String::from("id"), json!(id));
|
||||
obj.insert(String::from("name"), json!(id));
|
||||
obj.insert(String::from("server"), spec_v.clone());
|
||||
obj.insert(String::from("enabled"), json!(true));
|
||||
occ.insert(serde_json::Value::Object(obj));
|
||||
} else {
|
||||
// 新建服务器:默认仅启用 Codex
|
||||
servers.insert(
|
||||
id.clone(),
|
||||
McpServer {
|
||||
id: id.clone(),
|
||||
name: id.clone(),
|
||||
server: spec_v,
|
||||
apps: McpApps {
|
||||
claude: false,
|
||||
codex: true,
|
||||
gemini: false,
|
||||
},
|
||||
description: None,
|
||||
homepage: None,
|
||||
docs: None,
|
||||
tags: Vec::new(),
|
||||
},
|
||||
);
|
||||
changed += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut modified = false;
|
||||
let prev = existing
|
||||
.get("enabled")
|
||||
.and_then(|b| b.as_bool())
|
||||
.unwrap_or(false);
|
||||
if !prev {
|
||||
existing.insert(String::from("enabled"), json!(true));
|
||||
modified = true;
|
||||
}
|
||||
if existing.get("server").is_none() {
|
||||
log::warn!("MCP 条目 '{id}' 缺少 server 字段,覆盖为导入数据");
|
||||
existing.insert(String::from("server"), spec_v.clone());
|
||||
modified = true;
|
||||
}
|
||||
if existing.get("id").is_none() {
|
||||
log::warn!("MCP 条目 '{id}' 缺少 id 字段,自动填充");
|
||||
existing.insert(String::from("id"), json!(id));
|
||||
modified = true;
|
||||
}
|
||||
if modified {
|
||||
changed += 1;
|
||||
}
|
||||
}
|
||||
log::info!("导入新 MCP 服务器 '{id}'");
|
||||
}
|
||||
}
|
||||
changed
|
||||
@@ -724,76 +711,76 @@ pub fn sync_enabled_to_gemini(config: &MultiAppConfig) -> Result<(), AppError> {
|
||||
crate::gemini_mcp::set_mcp_servers_map(&enabled)
|
||||
}
|
||||
|
||||
/// 从 ~/.gemini/settings.json 导入 mcpServers 到 config.json(设为 enabled=true)。
|
||||
/// 已存在的项仅强制 enabled=true,不覆盖其他字段。
|
||||
/// 从 ~/.gemini/settings.json 导入 mcpServers 到统一结构(v3.7.0+)
|
||||
/// 已存在的服务器将启用 Gemini 应用,不覆盖其他字段和应用状态
|
||||
pub fn import_from_gemini(config: &mut MultiAppConfig) -> Result<usize, AppError> {
|
||||
use crate::app_config::{McpApps, McpServer};
|
||||
|
||||
let text_opt = crate::gemini_mcp::read_mcp_json()?;
|
||||
let Some(text) = text_opt else { return Ok(0) };
|
||||
let mut changed = normalize_servers_for(config, &AppType::Gemini);
|
||||
|
||||
let v: Value = serde_json::from_str(&text)
|
||||
.map_err(|e| AppError::McpValidation(format!("解析 ~/.gemini/settings.json 失败: {e}")))?;
|
||||
let Some(map) = v.get("mcpServers").and_then(|x| x.as_object()) else {
|
||||
return Ok(changed);
|
||||
return Ok(0);
|
||||
};
|
||||
|
||||
// 确保新结构存在
|
||||
if config.mcp.servers.is_none() {
|
||||
config.mcp.servers = Some(HashMap::new());
|
||||
}
|
||||
let servers = config.mcp.servers.as_mut().unwrap();
|
||||
|
||||
let mut changed = 0;
|
||||
let mut errors = Vec::new();
|
||||
|
||||
for (id, spec) in map.iter() {
|
||||
// 校验目标 spec
|
||||
validate_server_spec(spec)?;
|
||||
|
||||
let entry = config
|
||||
.mcp_for_mut(&AppType::Gemini)
|
||||
.servers
|
||||
.entry(id.clone());
|
||||
use std::collections::hash_map::Entry;
|
||||
match entry {
|
||||
Entry::Vacant(vac) => {
|
||||
let mut obj = serde_json::Map::new();
|
||||
obj.insert(String::from("id"), json!(id));
|
||||
obj.insert(String::from("name"), json!(id));
|
||||
obj.insert(String::from("server"), spec.clone());
|
||||
obj.insert(String::from("enabled"), json!(true));
|
||||
vac.insert(Value::Object(obj));
|
||||
changed += 1;
|
||||
}
|
||||
Entry::Occupied(mut occ) => {
|
||||
let value = occ.get_mut();
|
||||
let Some(existing) = value.as_object_mut() else {
|
||||
log::warn!("MCP 条目 '{id}' 不是 JSON 对象,覆盖为导入数据");
|
||||
let mut obj = serde_json::Map::new();
|
||||
obj.insert(String::from("id"), json!(id));
|
||||
obj.insert(String::from("name"), json!(id));
|
||||
obj.insert(String::from("server"), spec.clone());
|
||||
obj.insert(String::from("enabled"), json!(true));
|
||||
occ.insert(Value::Object(obj));
|
||||
changed += 1;
|
||||
// 校验:单项失败不中止,收集错误继续处理
|
||||
if let Err(e) = validate_server_spec(spec) {
|
||||
log::warn!("跳过无效 MCP 服务器 '{id}': {e}");
|
||||
errors.push(format!("{id}: {e}"));
|
||||
continue;
|
||||
};
|
||||
}
|
||||
|
||||
let mut modified = false;
|
||||
let prev_enabled = existing
|
||||
.get("enabled")
|
||||
.and_then(|b| b.as_bool())
|
||||
.unwrap_or(false);
|
||||
if !prev_enabled {
|
||||
existing.insert(String::from("enabled"), json!(true));
|
||||
modified = true;
|
||||
}
|
||||
if existing.get("server").is_none() {
|
||||
log::warn!("MCP 条目 '{id}' 缺少 server 字段,覆盖为导入数据");
|
||||
existing.insert(String::from("server"), spec.clone());
|
||||
modified = true;
|
||||
}
|
||||
if existing.get("id").is_none() {
|
||||
log::warn!("MCP 条目 '{id}' 缺少 id 字段,自动填充");
|
||||
existing.insert(String::from("id"), json!(id));
|
||||
modified = true;
|
||||
}
|
||||
if modified {
|
||||
if let Some(existing) = servers.get_mut(id) {
|
||||
// 已存在:仅启用 Gemini 应用
|
||||
if !existing.apps.gemini {
|
||||
existing.apps.gemini = true;
|
||||
changed += 1;
|
||||
log::info!("MCP 服务器 '{id}' 已启用 Gemini 应用");
|
||||
}
|
||||
} else {
|
||||
// 新建服务器:默认仅启用 Gemini
|
||||
servers.insert(
|
||||
id.clone(),
|
||||
McpServer {
|
||||
id: id.clone(),
|
||||
name: id.clone(),
|
||||
server: spec.clone(),
|
||||
apps: McpApps {
|
||||
claude: false,
|
||||
codex: false,
|
||||
gemini: true,
|
||||
},
|
||||
description: None,
|
||||
homepage: None,
|
||||
docs: None,
|
||||
tags: Vec::new(),
|
||||
},
|
||||
);
|
||||
changed += 1;
|
||||
log::info!("导入新 MCP 服务器 '{id}'");
|
||||
}
|
||||
}
|
||||
|
||||
if !errors.is_empty() {
|
||||
log::warn!(
|
||||
"导入完成,但有 {} 项失败: {:?}",
|
||||
errors.len(),
|
||||
errors
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
|
||||
@@ -231,11 +231,7 @@ impl McpService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// [已废弃] 从 Claude 导入 MCP(兼容旧 API)
|
||||
#[deprecated(
|
||||
since = "3.7.0",
|
||||
note = "Import will be handled differently in unified structure"
|
||||
)]
|
||||
/// 从 Claude 导入 MCP(v3.7.0 已更新为统一结构)
|
||||
pub fn import_from_claude(state: &AppState) -> Result<usize, AppError> {
|
||||
let mut cfg = state.config.write()?;
|
||||
let count = mcp::import_from_claude(&mut cfg)?;
|
||||
@@ -244,11 +240,7 @@ impl McpService {
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// [已废弃] 从 Codex 导入 MCP(兼容旧 API)
|
||||
#[deprecated(
|
||||
since = "3.7.0",
|
||||
note = "Import will be handled differently in unified structure"
|
||||
)]
|
||||
/// 从 Codex 导入 MCP(v3.7.0 已更新为统一结构)
|
||||
pub fn import_from_codex(state: &AppState) -> Result<usize, AppError> {
|
||||
let mut cfg = state.config.write()?;
|
||||
let count = mcp::import_from_codex(&mut cfg)?;
|
||||
@@ -257,11 +249,7 @@ impl McpService {
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// [已废弃] 从 Gemini 导入 MCP(兼容旧 API)
|
||||
#[deprecated(
|
||||
since = "3.7.0",
|
||||
note = "Import will be handled differently in unified structure"
|
||||
)]
|
||||
/// 从 Gemini 导入 MCP(v3.7.0 已更新为统一结构)
|
||||
pub fn import_from_gemini(state: &AppState) -> Result<usize, AppError> {
|
||||
let mut cfg = state.config.write()?;
|
||||
let count = mcp::import_from_gemini(&mut cfg)?;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::{fs, sync::RwLock};
|
||||
use std::{collections::HashMap, fs, sync::RwLock};
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use cc_switch_lib::{
|
||||
get_claude_mcp_path, get_claude_settings_path, import_default_config_test_hook, AppError,
|
||||
AppState, AppType, McpService, MultiAppConfig,
|
||||
AppState, AppType, McpApps, McpServer, McpService, MultiAppConfig,
|
||||
};
|
||||
|
||||
#[path = "support.rs"]
|
||||
@@ -126,16 +126,12 @@ fn import_mcp_from_claude_creates_config_and_enables_servers() {
|
||||
);
|
||||
|
||||
let guard = state.config.read().expect("lock config");
|
||||
let claude_servers = &guard.mcp.claude.servers;
|
||||
let entry = claude_servers
|
||||
.get("echo")
|
||||
.expect("server imported into config.json");
|
||||
// v3.7.0: 检查统一结构
|
||||
let servers = guard.mcp.servers.as_ref().expect("unified servers should exist");
|
||||
let entry = servers.get("echo").expect("server imported into unified structure");
|
||||
assert!(
|
||||
entry
|
||||
.get("enabled")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false),
|
||||
"imported server should be marked enabled"
|
||||
entry.apps.claude,
|
||||
"imported server should have Claude app enabled"
|
||||
);
|
||||
drop(guard);
|
||||
|
||||
@@ -181,43 +177,61 @@ fn import_mcp_from_claude_invalid_json_preserves_state() {
|
||||
fn set_mcp_enabled_for_codex_writes_live_config() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
ensure_test_home();
|
||||
let home = ensure_test_home();
|
||||
|
||||
// 创建 Codex 配置目录和文件
|
||||
let codex_dir = home.join(".codex");
|
||||
fs::create_dir_all(&codex_dir).expect("create codex dir");
|
||||
fs::write(codex_dir.join("auth.json"), r#"{"OPENAI_API_KEY":"test-key"}"#)
|
||||
.expect("create auth.json");
|
||||
fs::write(codex_dir.join("config.toml"), "")
|
||||
.expect("create empty config.toml");
|
||||
|
||||
let mut config = MultiAppConfig::default();
|
||||
config.ensure_app(&AppType::Codex);
|
||||
config.mcp.codex.servers.insert(
|
||||
|
||||
// v3.7.0: 使用统一结构
|
||||
config.mcp.servers = Some(HashMap::new());
|
||||
config.mcp.servers.as_mut().unwrap().insert(
|
||||
"codex-server".into(),
|
||||
json!({
|
||||
"id": "codex-server",
|
||||
"name": "Codex Server",
|
||||
"server": {
|
||||
McpServer {
|
||||
id: "codex-server".to_string(),
|
||||
name: "Codex Server".to_string(),
|
||||
server: json!({
|
||||
"type": "stdio",
|
||||
"command": "echo"
|
||||
},
|
||||
"enabled": false
|
||||
}),
|
||||
apps: McpApps {
|
||||
claude: false,
|
||||
codex: false, // 初始未启用
|
||||
gemini: false,
|
||||
},
|
||||
description: None,
|
||||
homepage: None,
|
||||
docs: None,
|
||||
tags: Vec::new(),
|
||||
},
|
||||
);
|
||||
|
||||
let state = AppState {
|
||||
config: RwLock::new(config),
|
||||
};
|
||||
|
||||
McpService::set_enabled(&state, AppType::Codex, "codex-server", true)
|
||||
.expect("set enabled should succeed");
|
||||
// v3.7.0: 使用 toggle_app 替代 set_enabled
|
||||
McpService::toggle_app(&state, "codex-server", AppType::Codex, true)
|
||||
.expect("toggle_app should succeed");
|
||||
|
||||
let guard = state.config.read().expect("lock config");
|
||||
let entry = guard
|
||||
.mcp
|
||||
.codex
|
||||
.servers
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get("codex-server")
|
||||
.expect("codex server exists");
|
||||
assert!(
|
||||
entry
|
||||
.get("enabled")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false),
|
||||
"server should be marked enabled after command"
|
||||
entry.apps.codex,
|
||||
"server should have Codex app enabled after toggle"
|
||||
);
|
||||
drop(guard);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user