Fix six open issues: stale imports, MCP validation, import-graph gaps, dup importers, I/O errors, scope docs
- pipeline: re-resolve imports for every file on update (not just changed), so add/remove/rename of a target fixes unchanged importers; propagate hash and read errors instead of treating unreadable files as empty - discovery: report unreadable walk entries to stderr instead of dropping them - imports: strip `pub` visibility so `pub use` re-exports resolve; share extension/module-entry probe lists (adds cc,cxx,hpp,hh,hxx,mts,cts,jsx,mjs,cjs) - store: DISTINCT + ORDER in importers_of to dedup repeated edges - mcp: validate required tool args (explicit isError), emit JSON-RPC -32700 parse errors and -32600 invalid-request instead of silently skipping - README: document the 13-tool surface vs toktoken's 27 and known gaps - tests: regression coverage for update re-resolution, pub-use re-exports, importer dedup, and MCP error reporting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
1 parent
6875998ab6
commit
4338e28ce0
7 files changed
+271
-49
No files matched your search
+131
-1
@@ -144,7 +144,10 @@ fn inspect_blast_radius_finds_affected_files() {
|
||||
.collect();
|
||||
// lib.rs imports util.rs directly; main.rs reaches it via the crate name.
|
||||
assert!(affected.contains(&"src/lib.rs"), "lib.rs imports util.rs");
|
||||
assert!(affected.contains(&"src/main.rs"), "main.rs → lib.rs → util.rs");
|
||||
assert!(
|
||||
affected.contains(&"src/main.rs"),
|
||||
"main.rs → lib.rs → util.rs"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -496,3 +499,130 @@ fn index_update_reindexes_changed_files() {
|
||||
"index update picks up the new symbol"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn index_update_reimports_unchanged_file_and_dedups() {
|
||||
let tmp = cache_dir().join("update-reimport");
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
let src = tmp.join("src");
|
||||
std::fs::create_dir_all(&src).unwrap();
|
||||
// Two `use` lines targeting the same (not-yet-existing) module.
|
||||
std::fs::write(
|
||||
src.join("main.rs"),
|
||||
"use util::hello;\nuse util::world;\n\nfn main() {}\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
run_json(&["index", "create", tmp.to_str().unwrap()]);
|
||||
|
||||
std::fs::write(
|
||||
src.join("util.rs"),
|
||||
"pub fn hello() {}\npub fn world() {}\n",
|
||||
)
|
||||
.unwrap();
|
||||
run_json(&["index", "update", tmp.to_str().unwrap()]);
|
||||
|
||||
let after = run_json(&[
|
||||
"--project",
|
||||
tmp.to_str().unwrap(),
|
||||
"find",
|
||||
"importers",
|
||||
"src/util.rs",
|
||||
]);
|
||||
let names: Vec<&str> = after
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|v| v.as_str().unwrap())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
names,
|
||||
vec!["src/main.rs"],
|
||||
"unchanged main.rs re-resolves to the new util.rs, deduplicated"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rust_pub_use_re_exports_resolve() {
|
||||
let tmp = cache_dir().join("pub-use");
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
let src = tmp.join("src");
|
||||
std::fs::create_dir_all(&src).unwrap();
|
||||
std::fs::write(src.join("inner.rs"), "pub fn x() {}\n").unwrap();
|
||||
std::fs::write(src.join("re.rs"), "pub use inner;\n").unwrap();
|
||||
|
||||
run_json(&["index", "create", tmp.to_str().unwrap()]);
|
||||
|
||||
let importers = run_json(&[
|
||||
"--project",
|
||||
tmp.to_str().unwrap(),
|
||||
"find",
|
||||
"importers",
|
||||
"src/inner.rs",
|
||||
]);
|
||||
let names: Vec<&str> = importers
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|v| v.as_str().unwrap())
|
||||
.collect();
|
||||
assert!(
|
||||
names.contains(&"src/re.rs"),
|
||||
"pub use in re.rs re-exports inner.rs"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_reports_invalid_params_and_parse_errors() {
|
||||
let src = fixture("rustdemo");
|
||||
let tmp = cache_dir().join("mcp-errors");
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
copy_dir(&src, &tmp);
|
||||
run_json(&["index", "create", tmp.to_str().unwrap()]);
|
||||
|
||||
let mut child = Command::new(bin())
|
||||
.args(["--project", tmp.to_str().unwrap(), "serve"])
|
||||
.env("XDG_CACHE_HOME", cache_dir())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.expect("spawn serve");
|
||||
|
||||
{
|
||||
let mut stdin = child.stdin.take().unwrap();
|
||||
// Missing required `query` argument -> tool-level isError.
|
||||
writeln!(
|
||||
stdin,
|
||||
r#"{{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{{"name":"search_symbols","arguments":{{}}}}}}"#
|
||||
)
|
||||
.unwrap();
|
||||
// Malformed JSON -> JSON-RPC parse error (id null, code -32700).
|
||||
writeln!(stdin, "this is not json").unwrap();
|
||||
}
|
||||
|
||||
let out = child.wait_with_output().unwrap();
|
||||
let responses: Vec<Value> = String::from_utf8_lossy(&out.stdout)
|
||||
.lines()
|
||||
.filter_map(|l| serde_json::from_str(l).ok())
|
||||
.collect();
|
||||
|
||||
let call = responses
|
||||
.iter()
|
||||
.find(|r| r["id"] == 2)
|
||||
.expect("tools/call response");
|
||||
assert_eq!(call["result"]["isError"], true, "missing arg is an error");
|
||||
assert!(
|
||||
call["result"]["content"][0]["text"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("query"),
|
||||
"error names the missing argument"
|
||||
);
|
||||
|
||||
let parse = responses
|
||||
.iter()
|
||||
.find(|r| r["error"]["code"] == -32700)
|
||||
.expect("parse error response");
|
||||
assert_eq!(parse["error"]["message"], "parse error");
|
||||
}
|
||||
Reference in new issue
Block a user