From e843b254173db62d4e89bc6cfea4b6e74810de54 Mon Sep 17 00:00:00 2001 From: flupkede Date: Sat, 17 Oct 2026 23:11:50 +0200 Subject: [PATCH] =?UTF-8?q?test:=20final-sweep=20remarks=20=E2=80=94=20mod?= =?UTF-8?q?el-gate=20refusal=20pin,=20include-test=20isolation,=20comment?= =?UTF-8?q?=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final full review (eef179a..8f0b79c) verdict PASS WITH REMARKS; the three actionable findings: - the no-download model gate's search-path refusal had no test the suite reaches (the pool-layer pins are #[ignore]-gated): a hermetic serve test now pins that a semantic search with an empty model cache answers the explicit refusal naming 'codesearch setup' instead of fetching. - the six include-gate tests walked tempdirs without the #[serial] + isolated-home guard their siblings got (6ba0778) — a developer's global .codesearchignore could prune the fixtures and redden the suite. All six now isolated. - parse_import_lines' comment still taught the 0-based premise the reconciliation removed; reworded to the shipped 1-based convention. The two [Design] rows (literal_search size, semantic_search_multi interning dance) are deferred — recorded here, no action. --- src/file/mod.rs | 10 +++++++ src/mcp/mod.rs | 4 +-- src/serve/tests.rs | 74 ++++++++++++++++++++++++++++++++++++++++++++++ src/watch/mod.rs | 5 ++-- 4 files changed, 89 insertions(+), 4 deletions(-) diff --git a/src/file/mod.rs b/src/file/mod.rs index 96f2aacd..3eb0a2d9 100644 --- a/src/file/mod.rs +++ b/src/file/mod.rs @@ -498,7 +498,9 @@ mod tests { } #[test] + #[serial] fn include_list_whitelists_matching_files_only() { + let _isolation = isolated_global_ignore_env(); let dir = TempDir::new().unwrap(); fs::write(dir.path().join("keep.rs"), "fn a() {}").unwrap(); fs::write(dir.path().join("drop.py"), "print(1)").unwrap(); @@ -522,7 +524,9 @@ mod tests { } #[test] + #[serial] fn include_list_negation_unincludes_again() { + let _isolation = isolated_global_ignore_env(); let dir = TempDir::new().unwrap(); fs::write(dir.path().join("keep.rs"), "fn a() {}").unwrap(); fs::write(dir.path().join("gen.rs"), "generated").unwrap(); @@ -540,7 +544,9 @@ mod tests { } #[test] + #[serial] fn no_include_file_indexes_everything() { + let _isolation = isolated_global_ignore_env(); let dir = TempDir::new().unwrap(); fs::write(dir.path().join("a.rs"), "fn a() {}").unwrap(); fs::write(dir.path().join("b.py"), "print(1)").unwrap(); @@ -551,7 +557,9 @@ mod tests { } #[test] + #[serial] fn include_list_ancestor_negation_unincludes_again() { + let _isolation = isolated_global_ignore_env(); let dir = TempDir::new().unwrap(); fs::create_dir_all(dir.path().join("src")).unwrap(); fs::write(dir.path().join("src").join("keep.rs"), "fn a() {}").unwrap(); @@ -579,7 +587,9 @@ mod tests { } #[test] + #[serial] fn include_list_present_but_empty_indexes_everything() { + let _isolation = isolated_global_ignore_env(); let dir = TempDir::new().unwrap(); fs::write(dir.path().join("a.rs"), "fn a() {}").unwrap(); fs::write(dir.path().join("b.py"), "print(1)").unwrap(); diff --git a/src/mcp/mod.rs b/src/mcp/mod.rs index f0d7f207..4ed8691e 100644 --- a/src/mcp/mod.rs +++ b/src/mcp/mod.rs @@ -975,8 +975,8 @@ fn parse_import_lines(content: &str, start_line: usize) -> Vec { if let Some((kind, imported)) = parsed { items.push(ImportItem { imported, - // `start_line` is the chunk's 0-based first line and `offset` - // is 0-based within it; the response emits 1-based lines. + // `start_line` is the chunk's 1-based first line and `offset` + // is 0-based within it; the sum is the match's editor line. line: start_line + offset, kind, }); diff --git a/src/serve/tests.rs b/src/serve/tests.rs index 434d7394..6b4323dc 100644 --- a/src/serve/tests.rs +++ b/src/serve/tests.rs @@ -5668,6 +5668,80 @@ async fn get_chunk_reports_the_real_chunker_editor_line_unchanged() { } } +/// The no-download model gate: with an empty model cache, a semantic search +/// must answer the explicit refusal (naming `codesearch setup`) instead of +/// turning the query into an unbounded network fetch — the whole-night serve +/// stall this gate exists to prevent. +#[tokio::test] +#[serial] +async fn semantic_search_refuses_when_the_model_is_not_cached() { + use rmcp::handler::server::wrapper::Parameters; + + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().join("solo"); + std::fs::create_dir_all(&root).unwrap(); + let db_path = root.join(DB_DIR_NAME); + std::fs::create_dir_all(&db_path).unwrap(); + std::fs::write( + db_path.join("metadata.json"), + r#"{"schema_version":1,"dimensions":2,"model_short_name":"minilm-l6-q"}"#, + ) + .unwrap(); + let stores = crate::index::SharedStores::new(&db_path, 2).expect("shared stores"); + drop(stores); + + // Empty model cache: no model files anywhere under CODESEARCH_HOME. + let scratch = tempfile::tempdir().unwrap(); + let cs_home = scratch.path().join("cs-home"); + let bare_home = scratch.path().join("bare-home"); + std::fs::create_dir_all(&cs_home).unwrap(); + std::fs::create_dir_all(&bare_home).unwrap(); + let _env = crate::testing::EnvRestore::set(&[ + ( + crate::constants::CODESEARCH_HOME_ENV, + cs_home.to_str().unwrap(), + ), + ("HOME", bare_home.to_str().unwrap()), + ("USERPROFILE", bare_home.to_str().unwrap()), + ]); + + let mut config = ReposConfig::default(); + config + .register_with_alias(root.clone(), Some("solo".to_string())) + .unwrap(); + let config_file = tmp.path().join("repos.json"); + config.save_to(&config_file).unwrap(); + let state = std::sync::Arc::new(ServeState::new(config, Some(config_file))); + let service = crate::mcp::CodesearchService::new_for_serve(state).unwrap(); + + let req = crate::mcp::types::SearchRequest { + query: "anything".to_string(), + mode: Some("semantic".to_string()), + compact: None, + semantic_mode: None, + filter_path: None, + min_score: None, + regex: None, + phrase: None, + file_glob: None, + language: None, + format: None, + limit: None, + project: Some("solo".to_string()), + group: None, + }; + let res = service + .search(Parameters(req)) + .await + .expect("search must answer, not error"); + let text = tool_text(&res); + assert!( + text.contains("not available locally") && text.contains("codesearch setup"), + "semantic search with an empty model cache must answer the no-download \ + refusal naming the setup hint, got: {text}" + ); +} + // ═══ Interactive fan-outs skip busy stores instead of queueing behind the // write lock (review cluster B). The priming call opens the group's stores // so `get_opened_stores` hands back the SAME SharedStores the handlers diff --git a/src/watch/mod.rs b/src/watch/mod.rs index 17e939f1..6152bf18 100644 --- a/src/watch/mod.rs +++ b/src/watch/mod.rs @@ -672,6 +672,7 @@ impl GitHeadWatcher { mod tests { #[test] + #[serial] fn include_list_gates_the_watcher() { let dir = tempfile::TempDir::new().unwrap(); std::fs::write(dir.path().join("keep.rs"), "fn a() {}").unwrap(); @@ -683,7 +684,7 @@ mod tests { ) .unwrap(); - let watcher = FileWatcher::new(dir.path().to_path_buf()); + let watcher = watcher_with_isolated_home(dir.path().to_path_buf()); assert!( watcher.is_watchable(&dir.path().join("keep.rs")), "whitelisted file must be watchable" @@ -696,7 +697,7 @@ mod tests { // Absent include list: everything the ignore rules allow. let dir2 = tempfile::TempDir::new().unwrap(); std::fs::write(dir2.path().join("drop.py"), "print(1)").unwrap(); - let watcher2 = FileWatcher::new(dir2.path().to_path_buf()); + let watcher2 = watcher_with_isolated_home(dir2.path().to_path_buf()); assert!( watcher2.is_watchable(&dir2.path().join("drop.py")), "no include list = historical behavior"