From 7811383b593f5a47a0582efdd1bee0d89a3478b8 Mon Sep 17 00:00:00 2001 From: step <60814302+steponeerror@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:52:57 +0800 Subject: [PATCH] fix(codex): use relative filename for model_catalog_json (#3614) * fix(codex): use relative filename for model_catalog_json Instead of writing an absolute path (which breaks on WSL/symlink setups), write only the filename "cc-switch-model-catalog.json" to config.toml. Codex CLI resolves relative paths from the config directory, and both files always reside in the same directory (~/.codex/). This eliminates the need for UNC-to-Linux path translation and makes the config portable across Windows, WSL, and symlinked directories. Also simplifies ownership checks in resolve_cc_switch_catalog_path() and set_codex_model_catalog_json_field() by removing dead string-equality comparisons that never matched on WSL. Closes farion1231/cc-switch#3573 Related: farion1231/cc-switch#3569 * style(codex): fix rustfmt violations in model_catalog tests Wrap a >100-col UNC-path line and remove a trailing blank line that broke 'cargo fmt --check' in Backend CI. Style-only, no logic change. --------- Co-authored-by: steponeerror Co-authored-by: Jason --- src-tauri/src/codex_config.rs | 125 +++++++++++++++++++++++++++++++--- 1 file changed, 114 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/codex_config.rs b/src-tauri/src/codex_config.rs index eb58160fb..5f1a7ddb6 100644 --- a/src-tauri/src/codex_config.rs +++ b/src-tauri/src/codex_config.rs @@ -682,20 +682,18 @@ fn set_codex_model_catalog_json_field( let mut doc = config_text .parse::() .map_err(|e| AppError::Message(format!("Invalid Codex config.toml: {e}")))?; - let generated_path = get_codex_model_catalog_path(); match catalog_path { - Some(path) => { - doc["model_catalog_json"] = toml_edit::value(path.to_string_lossy().as_ref()); + Some(_) => { + doc["model_catalog_json"] = toml_edit::value(CC_SWITCH_CODEX_MODEL_CATALOG_FILENAME); } None => { let should_remove = doc .get("model_catalog_json") .and_then(|item| item.as_str()) .map(|path| { - path == generated_path.to_string_lossy().as_ref() - || Path::new(path).file_name().and_then(|name| name.to_str()) - == Some(CC_SWITCH_CODEX_MODEL_CATALOG_FILENAME) + Path::new(path).file_name().and_then(|name| name.to_str()) + == Some(CC_SWITCH_CODEX_MODEL_CATALOG_FILENAME) }) .unwrap_or(false); if should_remove { @@ -780,9 +778,8 @@ fn resolve_cc_switch_catalog_path(config_text: &str, generated_path: &Path) -> O .filter(|s| !s.is_empty())?; let referenced_path = Path::new(catalog_path_str); - let is_cc_switch_owned = catalog_path_str == generated_path.to_string_lossy().as_ref() - || referenced_path.file_name().and_then(|name| name.to_str()) - == Some(CC_SWITCH_CODEX_MODEL_CATALOG_FILENAME); + let is_cc_switch_owned = referenced_path.file_name().and_then(|name| name.to_str()) + == Some(CC_SWITCH_CODEX_MODEL_CATALOG_FILENAME); if !is_cc_switch_owned { return None; } @@ -1791,7 +1788,7 @@ base_url = "https://production.api/v1" } #[test] - fn model_catalog_json_field_operates_on_top_level() { + fn model_catalog_json_field_writes_relative_filename() { let input = r#"model_provider = "any" [model_providers.any] @@ -1805,7 +1802,7 @@ name = "any" parsed .get("model_catalog_json") .and_then(|value| value.as_str()), - Some("/tmp/cc-switch-model-catalog.json") + Some(CC_SWITCH_CODEX_MODEL_CATALOG_FILENAME) ); assert!( parsed @@ -2026,4 +2023,110 @@ name = "any" ); } } + + #[test] + #[cfg(target_os = "windows")] + fn set_catalog_json_field_writes_filename_ignoring_unc_path() { + let input = r#"model_provider = "custom" +model = "glm-5" +"#; + // Simulate a WSL UNC path as cc-switch would see it on Windows; + // the function now writes just the relative filename. + let unc_path = + Path::new(r"\\wsl.localhost\Ubuntu\home\user\.codex\cc-switch-model-catalog.json"); + + let result = set_codex_model_catalog_json_field(input, Some(unc_path)).unwrap(); + let parsed: toml::Value = toml::from_str(&result).unwrap(); + + let written_path = parsed + .get("model_catalog_json") + .and_then(|v| v.as_str()) + .expect("model_catalog_json should be set"); + assert_eq!( + written_path, CC_SWITCH_CODEX_MODEL_CATALOG_FILENAME, + "should write only the relative filename, not the UNC path" + ); + } + + #[test] + fn set_catalog_json_field_writes_filename_for_any_path() { + let input = r#"model_provider = "custom" +model = "glm-5" +"#; + let regular_path = Path::new("/home/user/.codex/cc-switch-model-catalog.json"); + + let result = set_codex_model_catalog_json_field(input, Some(regular_path)).unwrap(); + let parsed: toml::Value = toml::from_str(&result).unwrap(); + + assert_eq!( + parsed.get("model_catalog_json").and_then(|v| v.as_str()), + Some(CC_SWITCH_CODEX_MODEL_CATALOG_FILENAME), + "should write only the relative filename, not the full path" + ); + } + + #[test] + fn set_catalog_json_none_removes_cc_switch_owned_by_filename() { + // After the WSL fix, TOML may contain a Linux-style path. + // The None arm must still remove it (file_name match catches any format). + let input = r#"model_catalog_json = "/home/user/.codex/cc-switch-model-catalog.json" +"#; + let result = set_codex_model_catalog_json_field(input, None).unwrap(); + let parsed: toml::Value = toml::from_str(&result).unwrap(); + assert!( + parsed.get("model_catalog_json").is_none(), + "None arm should remove cc-switch-owned field regardless of path format" + ); + } + + #[test] + fn set_catalog_json_none_preserves_user_owned_catalog() { + let input = r#"model_catalog_json = "/Users/me/.codex/my-custom-catalog.json" +"#; + let result = set_codex_model_catalog_json_field(input, None).unwrap(); + let parsed: toml::Value = toml::from_str(&result).unwrap(); + assert_eq!( + parsed.get("model_catalog_json").and_then(|v| v.as_str()), + Some("/Users/me/.codex/my-custom-catalog.json"), + "None arm should NOT remove user-owned catalog" + ); + } + + #[test] + fn resolve_catalog_finds_relative_filename() { + let config_text = r#"model_provider = "custom" +model_catalog_json = "cc-switch-model-catalog.json" +"#; + let generated_path = PathBuf::from("/home/user/.codex/cc-switch-model-catalog.json"); + let result = resolve_cc_switch_catalog_path(config_text, &generated_path); + assert_eq!( + result, + Some(generated_path), + "relative filename should resolve to generated_path for file I/O" + ); + } + + #[test] + fn resolve_catalog_ignores_user_owned_relative() { + let config_text = r#"model_catalog_json = "my-custom-catalog.json" +"#; + let generated_path = PathBuf::from("/home/user/.codex/cc-switch-model-catalog.json"); + let result = resolve_cc_switch_catalog_path(config_text, &generated_path); + assert_eq!( + result, None, + "user-owned catalog should not be claimed by cc-switch" + ); + } + + #[test] + fn set_catalog_json_none_removes_relative_path() { + let input = r#"model_catalog_json = "cc-switch-model-catalog.json" +"#; + let result = set_codex_model_catalog_json_field(input, None).unwrap(); + let parsed: toml::Value = toml::from_str(&result).unwrap(); + assert!( + parsed.get("model_catalog_json").is_none(), + "None arm should remove relative cc-switch-owned field" + ); + } }