diff --git a/tests/test_absence_diagnosis.py b/tests/test_absence_diagnosis.py index 55e296c..6bc3a6b 100644 --- a/tests/test_absence_diagnosis.py +++ b/tests/test_absence_diagnosis.py @@ -394,12 +394,20 @@ def _find_http_route_with_handlers(graph) -> str | None: def _find_symbol_with_edges(graph) -> NodeRef: - """Find a real Symbol node that has at least one edge.""" + """Find a real, resolved project Symbol node that has at least one edge. + + Restricted to ``s.resolved = true``: an unresolved/phantom symbol (external / + JDK / library reference) is classified ``external_dependency`` by diagnose + (external-wins precedence), not ``refine_query``. ``LIMIT 1`` without + ``ORDER BY`` is platform-nondeterministic in Kùzu, so the resolved filter + guarantees a genuine project symbol on every platform (was flaky on Windows). + """ rows = graph._rows( # noqa: SLF001 - "MATCH (s:Symbol)--() RETURN s.id AS id, s.name AS name, s.fqn AS fqn, " + "MATCH (s:Symbol)--() WHERE s.resolved = true " + "RETURN s.id AS id, s.name AS name, s.fqn AS fqn, " "s.kind AS kind LIMIT 1" ) - assert rows, "corpus has no symbol with edges" + assert rows, "corpus has no resolved symbol with edges" row = rows[0] return NodeRef( id=str(row.get("id") or ""), diff --git a/tests/test_absence_mcp_integration.py b/tests/test_absence_mcp_integration.py index c019da0..ac77437 100644 --- a/tests/test_absence_mcp_integration.py +++ b/tests/test_absence_mcp_integration.py @@ -5,6 +5,8 @@ """ from __future__ import annotations +import importlib.util + import pytest from mcp_v2 import describe_v2, find_v2, neighbors_v2, search_v2 @@ -12,6 +14,24 @@ from absence_types import AbsenceVerdict +def _vector_stack_available() -> bool: + """True when the optional vector stack (torch/sentence-transformers/lancedb) is installed. + + The ``search`` tool loads a SentenceTransformer model, so tests that monkeypatch + ``run_search`` still need the model importable (the patch makes ``run_search is None`` + False, forcing the semantic path). Skip them on graph-only installs (macOS Intel, + where the vector trio is gated off by PEP 508 markers). Mirrors test_mcp_v2.py. + """ + return all(importlib.util.find_spec(m) is not None for m in ("sentence_transformers", "lancedb")) + + +needs_vectors = pytest.mark.skipif( + not _vector_stack_available(), + reason="vector stack not installed (graph-only install; macOS Intel)", +) + + +@needs_vectors def test_search_empty_result_has_absence_diagnosis(ladybug_graph, monkeypatch) -> None: """Empty search result should have absence field populated with diagnosis.""" # Monkeypatch run_search to return empty results @@ -29,6 +49,7 @@ def test_search_empty_result_has_absence_diagnosis(ladybug_graph, monkeypatch) - assert out.absence.closest_symbols is not None +@needs_vectors def test_search_typo_has_absence_diagnosis(ladybug_graph, monkeypatch) -> None: """Search with a typo should have refine_query verdict with closest symbols.""" monkeypatch.setattr("mcp_v2.run_search", lambda *args, **kwargs: []) @@ -42,6 +63,7 @@ def test_search_typo_has_absence_diagnosis(ladybug_graph, monkeypatch) -> None: assert out.absence.closest_symbols # should have did-you-mean suggestions +@needs_vectors def test_search_external_dependency_has_absence_diagnosis(ladybug_graph, monkeypatch) -> None: """Search for an external dependency should have external_dependency verdict.""" monkeypatch.setattr("mcp_v2.run_search", lambda *args, **kwargs: []) @@ -55,6 +77,7 @@ def test_search_external_dependency_has_absence_diagnosis(ladybug_graph, monkeyp assert "java.util" in out.absence.external_identity.fqn or "java.util.List" in out.absence.external_identity.fqn +@needs_vectors def test_search_non_empty_result_has_no_absence(ladybug_graph, monkeypatch) -> None: """Non-empty search result should have absence=None.""" # Mock search to return results