From a661ae0d101495a8de61e4ee9d7ca29b54edbf88 Mon Sep 17 00:00:00 2001 From: "shijie.rao" Date: Thu, 2 Oct 2025 09:53:01 -0700 Subject: [PATCH 1/5] [CLI] CLI-425: add file name to fuzzy search response --- codex-rs/app-server-protocol/src/protocol.rs | 1 + codex-rs/app-server/src/fuzzy_file_search.rs | 10 +++++- .../tests/suite/fuzzy_file_search.rs | 31 ++++++++++++++++--- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol.rs b/codex-rs/app-server-protocol/src/protocol.rs index 4bc10b2e65..845a2431f4 100644 --- a/codex-rs/app-server-protocol/src/protocol.rs +++ b/codex-rs/app-server-protocol/src/protocol.rs @@ -725,6 +725,7 @@ pub struct FuzzyFileSearchParams { pub struct FuzzyFileSearchResult { pub root: String, pub path: String, + pub file_name: String, pub score: u32, #[serde(skip_serializing_if = "Option::is_none")] pub indices: Option>, diff --git a/codex-rs/app-server/src/fuzzy_file_search.rs b/codex-rs/app-server/src/fuzzy_file_search.rs index 146cc12a39..6c83a0f4ec 100644 --- a/codex-rs/app-server/src/fuzzy_file_search.rs +++ b/codex-rs/app-server/src/fuzzy_file_search.rs @@ -1,5 +1,6 @@ use std::num::NonZero; use std::num::NonZeroUsize; +use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; @@ -56,9 +57,16 @@ pub(crate) async fn run_fuzzy_file_search( match res { Ok(Ok((root, res))) => { for m in res.matches { + let path = m.path; + //TODO(shijie): Move file name generation to file_search lib. + let file_name = Path::new(&path) + .file_name() + .map(|name| name.to_string_lossy().into_owned()) + .unwrap_or_else(|| path.clone()); let result = FuzzyFileSearchResult { root: root.clone(), - path: m.path, + path, + file_name, score: m.score, indices: m.indices, }; diff --git a/codex-rs/app-server/tests/suite/fuzzy_file_search.rs b/codex-rs/app-server/tests/suite/fuzzy_file_search.rs index 8e33b130e1..20b7590e07 100644 --- a/codex-rs/app-server/tests/suite/fuzzy_file_search.rs +++ b/codex-rs/app-server/tests/suite/fuzzy_file_search.rs @@ -14,11 +14,13 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() { let codex_home = TempDir::new().expect("create temp codex home"); let root = TempDir::new().expect("create temp search root"); - // Create files designed to have deterministic ordering for query "abc". + // Create files designed to have deterministic ordering for query "abe". std::fs::write(root.path().join("abc"), "x").expect("write file abc"); - std::fs::write(root.path().join("abcde"), "x").expect("write file abcx"); - std::fs::write(root.path().join("abexy"), "x").expect("write file abcx"); + std::fs::write(root.path().join("abcde"), "x").expect("write file abcde"); + std::fs::write(root.path().join("abexy"), "x").expect("write file abexy"); std::fs::write(root.path().join("zzz.txt"), "x").expect("write file zzz"); + std::fs::create_dir_all(root.path().join("sub")).expect("create sub dir"); + std::fs::write(root.path().join("sub").join("abce"), "x").expect("write file sub/abce"); // Start MCP server and initialize. let mut mcp = McpProcess::new(codex_home.path()).await.expect("spawn mcp"); @@ -48,8 +50,27 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() { value, json!({ "files": [ - { "root": root_path.clone(), "path": "abexy", "score": 88, "indices": [0, 1, 2] }, - { "root": root_path.clone(), "path": "abcde", "score": 74, "indices": [0, 1, 4] }, + { + "root": root_path.clone(), + "path": "abexy", + "file_name": "abexy", + "score": 88, + "indices": [0, 1, 2], + }, + { + "root": root_path.clone(), + "path": "abcde", + "file_name": "abcde", + "score": 74, + "indices": [0, 1, 4], + }, + { + "root": root_path.clone(), + "path": "sub/abce", + "file_name": "abce", + "score": 72, + "indices": [4, 5, 7], + }, ] }) ); From 1d53bacc3718636b4b3b69c0e14b45231aa60448 Mon Sep 17 00:00:00 2001 From: "shijie.rao" Date: Thu, 2 Oct 2025 12:23:45 -0700 Subject: [PATCH 2/5] Address review comments: convert test to use anyhow --- .../tests/suite/fuzzy_file_search.rs | 69 +++++++++++-------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/codex-rs/app-server/tests/suite/fuzzy_file_search.rs b/codex-rs/app-server/tests/suite/fuzzy_file_search.rs index 20b7590e07..7247294a5e 100644 --- a/codex-rs/app-server/tests/suite/fuzzy_file_search.rs +++ b/codex-rs/app-server/tests/suite/fuzzy_file_search.rs @@ -1,3 +1,5 @@ +use anyhow::Context; +use anyhow::Result; use app_test_support::McpProcess; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; @@ -9,32 +11,34 @@ use tokio::time::timeout; const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_fuzzy_file_search_sorts_and_includes_indices() { +async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> { // Prepare a temporary Codex home and a separate root with test files. - let codex_home = TempDir::new().expect("create temp codex home"); - let root = TempDir::new().expect("create temp search root"); + let codex_home = TempDir::new().context("create temp codex home")?; + let root = TempDir::new().context("create temp search root")?; // Create files designed to have deterministic ordering for query "abe". - std::fs::write(root.path().join("abc"), "x").expect("write file abc"); - std::fs::write(root.path().join("abcde"), "x").expect("write file abcde"); - std::fs::write(root.path().join("abexy"), "x").expect("write file abexy"); - std::fs::write(root.path().join("zzz.txt"), "x").expect("write file zzz"); - std::fs::create_dir_all(root.path().join("sub")).expect("create sub dir"); - std::fs::write(root.path().join("sub").join("abce"), "x").expect("write file sub/abce"); + std::fs::write(root.path().join("abc"), "x").context("write file abc")?; + std::fs::write(root.path().join("abcde"), "x").context("write file abcde")?; + std::fs::write(root.path().join("abexy"), "x").context("write file abexy")?; + std::fs::write(root.path().join("zzz.txt"), "x").context("write file zzz")?; + std::fs::create_dir_all(root.path().join("sub")).context("create sub dir")?; + std::fs::write(root.path().join("sub").join("abce"), "x").context("write file sub/abce")?; // Start MCP server and initialize. - let mut mcp = McpProcess::new(codex_home.path()).await.expect("spawn mcp"); + let mut mcp = McpProcess::new(codex_home.path()) + .await + .context("spawn mcp")?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()) .await - .expect("init timeout") - .expect("init failed"); + .context("init timeout")? + .context("init failed")?; let root_path = root.path().to_string_lossy().to_string(); // Send fuzzyFileSearch request. let request_id = mcp .send_fuzzy_file_search_request("abe", vec![root_path.clone()], None) .await - .expect("send fuzzyFileSearch"); + .context("send fuzzyFileSearch")?; // Read response and verify shape and ordering. let resp: JSONRPCResponse = timeout( @@ -42,8 +46,8 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() { mcp.read_stream_until_response_message(RequestId::Integer(request_id)), ) .await - .expect("fuzzyFileSearch timeout") - .expect("fuzzyFileSearch resp"); + .context("fuzzyFileSearch timeout")? + .context("fuzzyFileSearch resp")?; let value = resp.result; assert_eq!( @@ -74,26 +78,30 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() { ] }) ); + + Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_fuzzy_file_search_accepts_cancellation_token() { - let codex_home = TempDir::new().expect("create temp codex home"); - let root = TempDir::new().expect("create temp search root"); +async fn test_fuzzy_file_search_accepts_cancellation_token() -> Result<()> { + let codex_home = TempDir::new().context("create temp codex home")?; + let root = TempDir::new().context("create temp search root")?; - std::fs::write(root.path().join("alpha.txt"), "contents").expect("write alpha"); + std::fs::write(root.path().join("alpha.txt"), "contents").context("write alpha")?; - let mut mcp = McpProcess::new(codex_home.path()).await.expect("spawn mcp"); + let mut mcp = McpProcess::new(codex_home.path()) + .await + .context("spawn mcp")?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()) .await - .expect("init timeout") - .expect("init failed"); + .context("init timeout")? + .context("init failed")?; let root_path = root.path().to_string_lossy().to_string(); let request_id = mcp .send_fuzzy_file_search_request("alp", vec![root_path.clone()], None) .await - .expect("send fuzzyFileSearch"); + .context("send fuzzyFileSearch")?; let request_id_2 = mcp .send_fuzzy_file_search_request( @@ -102,24 +110,27 @@ async fn test_fuzzy_file_search_accepts_cancellation_token() { Some(request_id.to_string()), ) .await - .expect("send fuzzyFileSearch"); + .context("send fuzzyFileSearch")?; let resp: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, mcp.read_stream_until_response_message(RequestId::Integer(request_id_2)), ) .await - .expect("fuzzyFileSearch timeout") - .expect("fuzzyFileSearch resp"); + .context("fuzzyFileSearch timeout")? + .context("fuzzyFileSearch resp")?; let files = resp .result .get("files") - .and_then(|value| value.as_array()) - .cloned() - .expect("files array"); + .context("files key missing")? + .as_array() + .context("files not array")? + .clone(); assert_eq!(files.len(), 1); assert_eq!(files[0]["root"], root_path); assert_eq!(files[0]["path"], "alpha.txt"); + + Ok(()) } From 04ae948ba62c8f3cfe1107e6ea639934431ed986 Mon Sep 17 00:00:00 2001 From: "shijie.rao" Date: Thu, 2 Oct 2025 12:42:26 -0700 Subject: [PATCH 3/5] Address review comment: account for windows env diff --- .../app-server/tests/suite/fuzzy_file_search.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/codex-rs/app-server/tests/suite/fuzzy_file_search.rs b/codex-rs/app-server/tests/suite/fuzzy_file_search.rs index 7247294a5e..7364a03328 100644 --- a/codex-rs/app-server/tests/suite/fuzzy_file_search.rs +++ b/codex-rs/app-server/tests/suite/fuzzy_file_search.rs @@ -21,8 +21,15 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> { std::fs::write(root.path().join("abcde"), "x").context("write file abcde")?; std::fs::write(root.path().join("abexy"), "x").context("write file abexy")?; std::fs::write(root.path().join("zzz.txt"), "x").context("write file zzz")?; - std::fs::create_dir_all(root.path().join("sub")).context("create sub dir")?; - std::fs::write(root.path().join("sub").join("abce"), "x").context("write file sub/abce")?; + let sub_dir = root.path().join("sub"); + std::fs::create_dir_all(&sub_dir).context("create sub dir")?; + let sub_abce_path = sub_dir.join("abce"); + std::fs::write(&sub_abce_path, "x").context("write file sub/abce")?; + let sub_abce_rel = sub_abce_path + .strip_prefix(root.path()) + .context("strip root prefix from sub/abce")? + .to_string_lossy() + .to_string(); // Start MCP server and initialize. let mut mcp = McpProcess::new(codex_home.path()) @@ -70,7 +77,7 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> { }, { "root": root_path.clone(), - "path": "sub/abce", + "path": sub_abce_rel, "file_name": "abce", "score": 72, "indices": [4, 5, 7], From 429c8106354a23750f4d5456864802490fcd6d22 Mon Sep 17 00:00:00 2001 From: "shijie.rao" Date: Thu, 2 Oct 2025 13:20:25 -0700 Subject: [PATCH 4/5] Ignore the score value for nested dir because of score diff between env --- .../tests/suite/fuzzy_file_search.rs | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/codex-rs/app-server/tests/suite/fuzzy_file_search.rs b/codex-rs/app-server/tests/suite/fuzzy_file_search.rs index 7364a03328..e256daf3aa 100644 --- a/codex-rs/app-server/tests/suite/fuzzy_file_search.rs +++ b/codex-rs/app-server/tests/suite/fuzzy_file_search.rs @@ -57,35 +57,41 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> { .context("fuzzyFileSearch resp")?; let value = resp.result; + let files = value + .get("files") + .context("files missing")? + .as_array() + .context("files not array")?; + assert_eq!(files.len(), 3); + + assert_eq!( + files[0], + json!({ + "root": root_path.clone(), + "path": "abexy", + "file_name": "abexy", + "score": 88, + "indices": [0, 1, 2] + }) + ); assert_eq!( - value, + files[1], json!({ - "files": [ - { - "root": root_path.clone(), - "path": "abexy", - "file_name": "abexy", - "score": 88, - "indices": [0, 1, 2], - }, - { - "root": root_path.clone(), - "path": "abcde", - "file_name": "abcde", - "score": 74, - "indices": [0, 1, 4], - }, - { - "root": root_path.clone(), - "path": sub_abce_rel, - "file_name": "abce", - "score": 72, - "indices": [4, 5, 7], - }, - ] + "root": root_path.clone(), + "path": "abcde", + "file_name": "abcde", + "score": 74, + "indices": [0, 1, 4] }) ); + let third = &files[2]; + assert_eq!(third["root"], json!(root_path)); + assert_eq!(third["path"], json!(sub_abce_rel)); + assert_eq!(third["file_name"], json!("abce")); + assert_eq!(third["indices"], json!([4, 5, 7])); + assert!(third["score"].is_number()); + Ok(()) } From 867c09179bc84ec426900187ae68787837f1de72 Mon Sep 17 00:00:00 2001 From: "shijie.rao" Date: Thu, 2 Oct 2025 16:32:12 -0700 Subject: [PATCH 5/5] Address review comments --- .../tests/suite/fuzzy_file_search.rs | 55 +++++++++---------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/codex-rs/app-server/tests/suite/fuzzy_file_search.rs b/codex-rs/app-server/tests/suite/fuzzy_file_search.rs index e256daf3aa..a2bc974ad0 100644 --- a/codex-rs/app-server/tests/suite/fuzzy_file_search.rs +++ b/codex-rs/app-server/tests/suite/fuzzy_file_search.rs @@ -57,40 +57,37 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> { .context("fuzzyFileSearch resp")?; let value = resp.result; - let files = value - .get("files") - .context("files missing")? - .as_array() - .context("files not array")?; - assert_eq!(files.len(), 3); + // The path separator on Windows affects the score. + let expected_score = if cfg!(windows) { 69 } else { 72 }; assert_eq!( - files[0], + value, json!({ - "root": root_path.clone(), - "path": "abexy", - "file_name": "abexy", - "score": 88, - "indices": [0, 1, 2] + "files": [ + { + "root": root_path.clone(), + "path": "abexy", + "file_name": "abexy", + "score": 88, + "indices": [0, 1, 2], + }, + { + "root": root_path.clone(), + "path": "abcde", + "file_name": "abcde", + "score": 74, + "indices": [0, 1, 4], + }, + { + "root": root_path.clone(), + "path": sub_abce_rel, + "file_name": "abce", + "score": expected_score, + "indices": [4, 5, 7], + }, + ] }) ); - assert_eq!( - files[1], - json!({ - "root": root_path.clone(), - "path": "abcde", - "file_name": "abcde", - "score": 74, - "indices": [0, 1, 4] - }) - ); - - let third = &files[2]; - assert_eq!(third["root"], json!(root_path)); - assert_eq!(third["path"], json!(sub_abce_rel)); - assert_eq!(third["file_name"], json!("abce")); - assert_eq!(third["indices"], json!([4, 5, 7])); - assert!(third["score"].is_number()); Ok(()) }