diff --git a/codex-rs/app-server-protocol/src/protocol.rs b/codex-rs/app-server-protocol/src/protocol.rs index 4bc10b2e..845a2431 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 146cc12a..6c83a0f4 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 8e33b130..a2bc974a 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,30 +11,41 @@ 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 "abc". - 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("zzz.txt"), "x").expect("write file zzz"); + // Create files designed to have deterministic ordering for query "abe". + 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")?; + 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()).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( @@ -40,39 +53,65 @@ 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; + // The path separator on Windows affects the score. + let expected_score = if cfg!(windows) { 69 } else { 72 }; + assert_eq!( 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_rel, + "file_name": "abce", + "score": expected_score, + "indices": [4, 5, 7], + }, ] }) ); + + 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( @@ -81,24 +120,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(()) }