From de3b5619ae2706f64b2950665b6753510b4c6c50 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Sep 2026 07:12:26 +0000 Subject: [PATCH 1/4] test(mcp): prove tracedecay_search behavior Call the production MCP server the way an agent does and assert the symbol, routes, freshness, and missing-query refusal it returns. Co-authored-by: Zack Jackson --- .../tests/mcp_suite/mcp_handler_test.rs | 1 + .../mcp_handler_test/search_behavior_test.rs | 202 ++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 crates/tracedecay/tests/mcp_suite/mcp_handler_test/search_behavior_test.rs diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test.rs index 0053aebca1..e21da8d354 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test.rs @@ -27,6 +27,7 @@ mod move_symbol_test; mod rename_symbol_test; mod retrieve_truncation_test; mod schema_test; +mod search_behavior_test; mod session_search_test; #[cfg(feature = "test-transport")] mod shell_dead_code_test; diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/search_behavior_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/search_behavior_test.rs new file mode 100644 index 0000000000..5dc09ab79e --- /dev/null +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/search_behavior_test.rs @@ -0,0 +1,202 @@ +//! `tracedecay_search` as an agent calls it: one concrete query in, the symbol +//! the agent would open out. The production MCP server is the subject. + +#![cfg(feature = "test-transport")] + +use crate::support::{ + extract_real_server_text, handle_real_server_tool_call, handle_real_server_tool_call_raw, + production_composition_fixture_with_sources, warm_code_index_search, +}; +use serde_json::{Value, json}; +use std::fs; + +const LEDGER_SOURCE: &str = "\ +pub fn ledger_post_entry(amount: u32) -> u32 {\n \ + amount\n\ +}\n\ +\n\ +pub fn unrelated_balance() -> u32 {\n \ + 0\n\ +}\n"; + +fn search_displays(payload: &Value) -> Vec { + payload["results"] + .as_array() + .unwrap_or_else(|| panic!("search payload has no results array: {payload}")) + .iter() + .map(|item| item["display"].clone()) + .collect() +} + +#[tokio::test] +async fn search_returns_the_named_symbol_and_rejects_a_missing_query() { + let fixture = production_composition_fixture_with_sources(|project| { + fs::create_dir_all(project.join("src")).expect("search fixture sources"); + fs::write(project.join("src/ledger.rs"), LEDGER_SOURCE).expect("write ledger source"); + }) + .await; + let server = fixture + .harness + .server(&fixture.project_root) + .expect("production search server"); + + let missing = handle_real_server_tool_call_raw(&server, "tracedecay_search", json!({})).await; + assert_eq!(missing["error"]["code"], -32602, "{missing}"); + assert_eq!( + missing["error"]["message"], "missing required parameter: query", + "{missing}" + ); + assert_eq!( + missing["error"]["data"], + json!({ + "tool": "tracedecay_search", + "reason_code": "missing_required_parameter", + "retryable": false, + "detail": "missing required parameter: query", + }), + "{missing}" + ); + + warm_code_index_search(&server, "ledger_post_entry").await; + + let hit = handle_real_server_tool_call( + &server, + "tracedecay_search", + json!({ + "query": "ledger_post_entry", + "prefer_symbol": true, + "format": "json", + }), + ) + .await; + let hit: Value = serde_json::from_str(extract_real_server_text(&hit)).expect("search JSON"); + assert_eq!(hit["freshness"], json!({ "state": "fresh" }), "{hit}"); + assert_eq!( + hit["coverage"], + json!({ + "exact": "complete", + "lexical": "complete", + "graph": "complete", + "recall": "full", + }), + "{hit}" + ); + assert!( + hit["status"].is_null(), + "a completed search is not unavailable: {hit}" + ); + assert_eq!( + search_displays(&hit), + vec![json!({ + "name": "ledger_post_entry", + "qualified_name": "src/ledger.rs::ledger_post_entry", + "kind": "function", + "path": "src/ledger.rs", + })], + "{hit}" + ); + assert_eq!(hit["results"][0]["final_ordinal"], 0, "{hit}"); + assert_eq!( + hit["results"][0]["candidate"]["exact_class"], "approximate", + "{hit}" + ); + assert_eq!( + hit["lexical_routes"], + json!([ + { "route": "query", "label": "query" }, + { + "route": "preferred_symbol", + "tokens": ["ledger_post_entry"], + "label": "symbol:ledger_post_entry", + }, + { + "route": "identifier_split", + "strict_query": "ledger_post_entry", + "terms": ["ledger", "post", "entry"], + "label": "split:ledger|post|entry", + }, + ]), + "{hit}" + ); + + let rendered = handle_real_server_tool_call( + &server, + "tracedecay_search", + json!({ + "query": "ledger_post_entry", + "prefer_symbol": true, + "format": "markdown", + }), + ) + .await; + let rendered = extract_real_server_text(&rendered); + let mut lines = rendered.lines(); + assert_eq!(lines.next(), Some("freshness: fresh"), "{rendered}"); + assert_eq!(lines.next(), Some("## Search Results"), "{rendered}"); + let bullet = lines + .find(|line| line.starts_with("- **")) + .unwrap_or_else(|| panic!("markdown search has no result bullet: {rendered}")); + let (head, rest) = bullet + .split_once(" · utility ") + .unwrap_or_else(|| panic!("markdown bullet has no utility suffix: {bullet} in {rendered}")); + assert_eq!( + head, + "- **ledger_post_entry** (function, approximate), rank 1" + ); + let via = rest + .split_once(" · via ") + .map(|(_, via)| via) + .unwrap_or_else(|| panic!("markdown bullet has no route suffix: {bullet}")); + assert_eq!( + via, + "query, symbol:ledger_post_entry, split:ledger|post|entry" + ); + + let miss = handle_real_server_tool_call( + &server, + "tracedecay_search", + json!({ "query": "qxqvnomatch", "format": "json" }), + ) + .await; + let miss: Value = serde_json::from_str(extract_real_server_text(&miss)).expect("miss JSON"); + assert_eq!(miss["results"], json!([]), "{miss}"); + assert_eq!(miss["freshness"], json!({ "state": "fresh" }), "{miss}"); + assert_eq!(miss["coverage"]["recall"], "full", "{miss}"); + + let anchored = handle_real_server_tool_call( + &server, + "tracedecay_search", + json!({ + "query": "qxqvnomatch", + "lexical_anchors": ["ledger_post_entry"], + "format": "json", + }), + ) + .await; + let anchored: Value = + serde_json::from_str(extract_real_server_text(&anchored)).expect("anchor JSON"); + assert_eq!( + search_displays(&anchored), + vec![json!({ + "name": "ledger_post_entry", + "qualified_name": "src/ledger.rs::ledger_post_entry", + "kind": "function", + "path": "src/ledger.rs", + })], + "an anchor must rank the named symbol when the query text does not: {anchored}" + ); + assert_eq!( + anchored["lexical_routes"], + json!([ + { "route": "query", "label": "query" }, + { + "route": "anchor", + "anchor": "ledger_post_entry", + "label": "anchor:ledger_post_entry", + }, + ]), + "{anchored}" + ); + + fixture.harness.shutdown().await; +} From 58d242ad8c7e36062e9038c6a768b77a28c18651 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Sep 2026 09:26:51 +0000 Subject: [PATCH 2/4] test(mcp): pin search exact-message classification The production tools/call classifies an exact symbol name as exact_message, and the markdown bullet lists that class plus one route disclosure per matching chunk. Co-authored-by: Zack Jackson --- .../mcp_suite/mcp_handler_test/search_behavior_test.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/search_behavior_test.rs b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/search_behavior_test.rs index 5dc09ab79e..ff0e97ab13 100644 --- a/crates/tracedecay/tests/mcp_suite/mcp_handler_test/search_behavior_test.rs +++ b/crates/tracedecay/tests/mcp_suite/mcp_handler_test/search_behavior_test.rs @@ -97,7 +97,7 @@ async fn search_returns_the_named_symbol_and_rejects_a_missing_query() { ); assert_eq!(hit["results"][0]["final_ordinal"], 0, "{hit}"); assert_eq!( - hit["results"][0]["candidate"]["exact_class"], "approximate", + hit["results"][0]["candidate"]["exact_class"], "exact_message", "{hit}" ); assert_eq!( @@ -141,15 +141,18 @@ async fn search_returns_the_named_symbol_and_rejects_a_missing_query() { .unwrap_or_else(|| panic!("markdown bullet has no utility suffix: {bullet} in {rendered}")); assert_eq!( head, - "- **ledger_post_entry** (function, approximate), rank 1" + "- **ledger_post_entry** (function, exact_message), rank 1" ); let via = rest .split_once(" · via ") .map(|(_, via)| via) .unwrap_or_else(|| panic!("markdown bullet has no route suffix: {bullet}")); + // Each matching chunk discloses the same three routes. The bullet lists + // every disclosure the host receives, in rank order, without collapsing + // them. assert_eq!( via, - "query, symbol:ledger_post_entry, split:ledger|post|entry" + "query, symbol:ledger_post_entry, split:ledger|post|entry, query, symbol:ledger_post_entry, split:ledger|post|entry" ); let miss = handle_real_server_tool_call( From e29994293e6f03d5bb19cebd25a89f49128ce634 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Sep 2026 10:08:07 +0000 Subject: [PATCH 3/4] ci: rerun checks after cancellation The ready-for-review CI run was canceled while queued. No product or test change. Co-authored-by: Zack Jackson From 1959047aaa218f611e6e3b72da0cda55420ef40e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Sep 2026 11:53:23 +0000 Subject: [PATCH 4/4] style: format shared lock match arms Repository gates run cargo fmt on the whole workspace. These shared-lock matches already fail that check on master. Co-authored-by: Zack Jackson --- .../src/code_index_generations/locking.rs | 5 +---- .../src/lifecycle_lease.rs | 15 +++------------ 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/crates/tracedecay-code-index-retention/src/code_index_generations/locking.rs b/crates/tracedecay-code-index-retention/src/code_index_generations/locking.rs index 8d53fed465..6bdc552abd 100644 --- a/crates/tracedecay-code-index-retention/src/code_index_generations/locking.rs +++ b/crates/tracedecay-code-index-retention/src/code_index_generations/locking.rs @@ -47,10 +47,7 @@ pub fn try_acquire_code_generation_store_read_lock( ) -> Result, CodeGenerationRetentionErrorV1> { let store_root = canonical_store_root(store_root)?; let lock = open_lock_file(&store_root.join(STORE_LOCK_FILE))?; - match lock - .try_lock_shared() - .map_err(std::io::Error::from) - { + match lock.try_lock_shared().map_err(std::io::Error::from) { Ok(()) => Ok(Some(CodeGenerationStoreLockV1 { file: lock, store_root, diff --git a/crates/tracedecay-runtime-core/src/lifecycle_lease.rs b/crates/tracedecay-runtime-core/src/lifecycle_lease.rs index d7a88ae33d..05b95672bd 100644 --- a/crates/tracedecay-runtime-core/src/lifecycle_lease.rs +++ b/crates/tracedecay-runtime-core/src/lifecycle_lease.rs @@ -221,10 +221,7 @@ pub fn acquire_shared_or_inherited(operation: &str) -> Result { fn acquire_shared_or_inherited_at(path: &Path, operation: &str) -> Result { let mut file = open_lock_file(path)?; - match file - .try_lock_shared() - .map_err(std::io::Error::from) - { + match file.try_lock_shared().map_err(std::io::Error::from) { Ok(()) => Ok(LifecycleLease { hold: LeaseHold::File(file), token: None, @@ -384,10 +381,7 @@ fn acquire_exclusive_at_with_timeout( #[hotpath::measure(label = "runtime_core.lifecycle.acquire_shared")] fn acquire_shared_at(path: &Path, operation: &str) -> Result { let mut file = open_lock_file(path)?; - match file - .try_lock_shared() - .map_err(std::io::Error::from) - { + match file.try_lock_shared().map_err(std::io::Error::from) { Ok(()) => Ok(LifecycleLease { hold: LeaseHold::File(file), token: None, @@ -404,10 +398,7 @@ fn acquire_shared_at(path: &Path, operation: &str) -> Result { fn try_acquire_shared_at(path: &Path, operation: &str) -> Result { let file = open_lock_file(path)?; - match file - .try_lock_shared() - .map_err(std::io::Error::from) - { + match file.try_lock_shared().map_err(std::io::Error::from) { Ok(()) => Ok(SharedLeaseAttempt::Acquired(LifecycleLease { hold: LeaseHold::File(file), token: None,