use serde_json::json; use cc_switch_lib::{ get_codex_auth_path, get_codex_config_path, read_json_file, switch_provider_test_hook, write_codex_live_atomic, AppError, AppState, AppType, MultiAppConfig, Provider, }; #[path = "support.rs"] mod support; use support::{ensure_test_home, reset_test_fs, test_mutex}; #[test] fn switch_provider_updates_codex_live_and_state() { let _guard = test_mutex().lock().expect("acquire test mutex"); reset_test_fs(); let _home = ensure_test_home(); let legacy_auth = json!({"OPENAI_API_KEY": "legacy-key"}); let legacy_config = r#"[mcp_servers.legacy] type = "stdio" command = "echo" "#; write_codex_live_atomic(&legacy_auth, Some(legacy_config)) .expect("seed existing codex live config"); let mut config = MultiAppConfig::default(); { let manager = config .get_manager_mut(&AppType::Codex) .expect("codex manager"); manager.current = "old-provider".to_string(); manager.providers.insert( "old-provider".to_string(), Provider::with_id( "old-provider".to_string(), "Legacy".to_string(), json!({ "auth": {"OPENAI_API_KEY": "stale"}, "config": "stale-config" }), None, ), ); manager.providers.insert( "new-provider".to_string(), Provider::with_id( "new-provider".to_string(), "Latest".to_string(), json!({ "auth": {"OPENAI_API_KEY": "fresh-key"}, "config": r#"[mcp_servers.latest] type = "stdio" command = "say" "# }), None, ), ); } config.mcp.codex.servers.insert( "echo-server".into(), json!({ "id": "echo-server", "enabled": true, "server": { "type": "stdio", "command": "echo" } }), ); let app_state = AppState { config: std::sync::Mutex::new(config), }; switch_provider_test_hook(&app_state, AppType::Codex, "new-provider") .expect("switch provider should succeed"); let auth_value: serde_json::Value = read_json_file(&get_codex_auth_path()).expect("read auth.json"); assert_eq!( auth_value .get("OPENAI_API_KEY") .and_then(|v| v.as_str()) .unwrap_or(""), "fresh-key", "live auth.json should reflect new provider" ); let config_text = std::fs::read_to_string(get_codex_config_path()).expect("read config.toml"); assert!( config_text.contains("mcp_servers.echo-server"), "config.toml should contain synced MCP servers" ); let locked = app_state .config .lock() .expect("lock config after switch"); let manager = locked .get_manager(&AppType::Codex) .expect("codex manager after switch"); assert_eq!(manager.current, "new-provider", "current provider updated"); let new_provider = manager .providers .get("new-provider") .expect("new provider exists"); let new_config_text = new_provider .settings_config .get("config") .and_then(|v| v.as_str()) .unwrap_or_default(); assert_eq!( new_config_text, config_text, "provider config snapshot should match live file" ); let legacy = manager .providers .get("old-provider") .expect("legacy provider still exists"); let legacy_auth_value = legacy .settings_config .get("auth") .and_then(|v| v.get("OPENAI_API_KEY")) .and_then(|v| v.as_str()) .unwrap_or(""); assert_eq!( legacy_auth_value, "legacy-key", "previous provider should be backfilled with live auth" ); } #[test] fn switch_provider_missing_provider_returns_error() { let _guard = test_mutex().lock().expect("acquire test mutex"); reset_test_fs(); let mut config = MultiAppConfig::default(); config .get_manager_mut(&AppType::Claude) .expect("claude manager") .current = "does-not-exist".to_string(); let app_state = AppState { config: std::sync::Mutex::new(config), }; let err = switch_provider_test_hook(&app_state, AppType::Claude, "missing-provider") .expect_err("switching to a missing provider should fail"); assert!( err.to_string().contains("供应商不存在"), "error message should mention missing provider" ); } #[test] fn switch_provider_updates_claude_live_and_state() { let _guard = test_mutex().lock().expect("acquire test mutex"); reset_test_fs(); let _home = ensure_test_home(); let settings_path = cc_switch_lib::get_claude_settings_path(); if let Some(parent) = settings_path.parent() { std::fs::create_dir_all(parent).expect("create claude settings dir"); } let legacy_live = json!({ "env": { "ANTHROPIC_API_KEY": "legacy-key" }, "workspace": { "path": "/tmp/workspace" } }); std::fs::write( &settings_path, serde_json::to_string_pretty(&legacy_live).expect("serialize legacy live"), ) .expect("seed claude live config"); let mut config = MultiAppConfig::default(); { let manager = config .get_manager_mut(&AppType::Claude) .expect("claude manager"); manager.current = "old-provider".to_string(); manager.providers.insert( "old-provider".to_string(), Provider::with_id( "old-provider".to_string(), "Legacy Claude".to_string(), json!({ "env": { "ANTHROPIC_API_KEY": "stale-key" } }), None, ), ); manager.providers.insert( "new-provider".to_string(), Provider::with_id( "new-provider".to_string(), "Fresh Claude".to_string(), json!({ "env": { "ANTHROPIC_API_KEY": "fresh-key" }, "workspace": { "path": "/tmp/new-workspace" } }), None, ), ); } let app_state = AppState { config: std::sync::Mutex::new(config), }; switch_provider_test_hook(&app_state, AppType::Claude, "new-provider") .expect("switch provider should succeed"); let live_after: serde_json::Value = read_json_file(&settings_path).expect("read claude live settings"); assert_eq!( live_after .get("env") .and_then(|env| env.get("ANTHROPIC_API_KEY")) .and_then(|key| key.as_str()), Some("fresh-key"), "live settings.json should reflect new provider auth" ); let locked = app_state .config .lock() .expect("lock config after switch"); let manager = locked .get_manager(&AppType::Claude) .expect("claude manager after switch"); assert_eq!(manager.current, "new-provider", "current provider updated"); let legacy_provider = manager .providers .get("old-provider") .expect("legacy provider still exists"); assert_eq!( legacy_provider.settings_config, legacy_live, "previous provider should receive backfilled live config" ); let new_provider = manager .providers .get("new-provider") .expect("new provider exists"); assert_eq!( new_provider .settings_config .get("env") .and_then(|env| env.get("ANTHROPIC_API_KEY")) .and_then(|key| key.as_str()), Some("fresh-key"), "new provider snapshot should retain fresh auth" ); drop(locked); let home_dir = std::env::var("HOME").expect("HOME should be set by ensure_test_home"); let config_path = std::path::Path::new(&home_dir) .join(".cc-switch") .join("config.json"); assert!( config_path.exists(), "switching provider should persist config.json" ); let persisted: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&config_path).expect("read saved config")) .expect("parse saved config"); assert_eq!( persisted .get("claude") .and_then(|claude| claude.get("current")) .and_then(|current| current.as_str()), Some("new-provider"), "saved config.json should record the new current provider" ); } #[test] fn switch_provider_codex_missing_auth_returns_error_and_keeps_state() { let _guard = test_mutex().lock().expect("acquire test mutex"); reset_test_fs(); let _home = ensure_test_home(); let mut config = MultiAppConfig::default(); { let manager = config .get_manager_mut(&AppType::Codex) .expect("codex manager"); manager.providers.insert( "invalid".to_string(), Provider::with_id( "invalid".to_string(), "Broken Codex".to_string(), json!({ "config": "[mcp_servers.test]\ncommand = \"noop\"" }), None, ), ); } let app_state = AppState { config: std::sync::Mutex::new(config), }; let err = switch_provider_test_hook(&app_state, AppType::Codex, "invalid") .expect_err("switching should fail when auth missing"); match err { AppError::Config(msg) => assert!( msg.contains("auth"), "expected auth missing error message, got {msg}" ), other => panic!("expected config error, got {other:?}"), } let locked = app_state .config .lock() .expect("lock config after failure"); let manager = locked .get_manager(&AppType::Codex) .expect("codex manager"); assert!( manager.current.is_empty(), "current provider should remain empty on failure" ); }