diff --git a/tests/test_mcp_v2.py b/tests/test_mcp_v2.py index 349e49f2..a8934f65 100644 --- a/tests/test_mcp_v2.py +++ b/tests/test_mcp_v2.py @@ -588,6 +588,25 @@ def test_search_unknown_filter_key_returns_failure(monkeypatch, ladybug_graph) - assert "typo_key" in out.message +def test_search_no_lance_index_returns_failure_envelope(monkeypatch, ladybug_graph, tmp_path) -> None: + """Real search error path (issue #358): every other search test monkeypatches + run_search, so the genuine `except Exception` envelope in search_v2 was never + exercised. With no Lance vector index present, search returns a structured + failure (success=False, non-empty message, no traceback) while the graph-only + tools still succeed against the same graph — proving the failure is + vector-specific, not a crash.""" + empty_index = tmp_path / "no-lance-index" + empty_index.mkdir() + monkeypatch.setenv("JAVA_CODEBASE_RAG_INDEX_DIR", str(empty_index)) + out = search_v2("ChatService", graph=ladybug_graph) + assert out.success is False + assert out.message is not None and out.message.strip() + # Graph-only tools still work (the failure is vector-specific, not a crash). + found = find_v2("symbol", {"role": "CONTROLLER"}, graph=ladybug_graph) + assert found.success is True + assert found.results + + def test_search_pushes_nodefilter_into_run_search(monkeypatch, ladybug_graph) -> None: """search forwards NodeFilter structural fields into run_search so the filter applies BEFORE pagination, not as a post-filter on the already-paginated page diff --git a/tests/test_search_lancedb.py b/tests/test_search_lancedb.py index bae58b7f..a6197f9a 100644 --- a/tests/test_search_lancedb.py +++ b/tests/test_search_lancedb.py @@ -37,6 +37,25 @@ def test_rrf_merge_weights_second_list_by_row() -> None: assert by_file["b.java"] > by_file["c.java"] +def test_rrf_merge_reinforced_row_across_lists_outranks_singleton() -> None: + """Multi-list RRF (issue #358): a row reinforced across two ranked lists + accumulates score and outranks a row appearing in only one list. Merging + dedups by (filename, range_start, range_end) and orders by summed score — + the core of multi-table fused ranking, previously covered only for the + weighted two-list case.""" + list_a = [{"filename": "a.java", "range_start": 1, "range_end": 5}] + list_b = [{"filename": "a.java", "range_start": 1, "range_end": 5}] # same key, distinct row + list_c = [{"filename": "z.java", "range_start": 9, "range_end": 99}] # singleton, rank 0 + merged = _rrf_merge([list_a, list_b, list_c], k=60) + by_file = {m["filename"]: float(m["_rrf_score"]) for m in merged} + # 'a.java' (rank 0 in two lists) sums two contributions; 'z.java' only one. + assert by_file["a.java"] > by_file["z.java"] + # The two a.java entries collapse to one (dedup by row key). + assert len(merged) == 2 + # Highest summed score first. + assert merged[0]["filename"] == "a.java" + + def test_java_enriched_columns_include_symbol_identity_fields() -> None: assert "symbol_id" in JAVA_ENRICHED_COLUMNS assert "metadata" in JAVA_ENRICHED_COLUMNS