Skip to content

Tests for Graphify integration paths #12

@verkligheten

Description

@verkligheten

Parent Epic

Part of #5 — Integrate Graphify for zero-cost code entity extraction

Task

Write tests covering all Graphify integration paths: the code_graph module, the modified wiki_ingest_folder, the CLI folder detection, and cross-referencing enrichment.

Test Files

1. NEW: tests/unit/services/test_code_graph.py

Tests for the pure-logic functions in code_graph.py. These do NOT require graphifyy installed — they test the mapping/conversion logic with plain dicts.

import json
import pytest
from pathlib import Path
from unittest.mock import patch


class TestGraphifyAvailable:
    def test_returns_false_when_not_installed(self):
        """graphify_available() returns False when graphifyy is not importable."""
        from agent_notes.services.code_graph import graphify_available
        with patch.dict("sys.modules", {"graphify": None, "graphify.extract": None}):
            # Force re-evaluation
            assert graphify_available() is False

    def test_returns_true_when_installed(self):
        """graphify_available() returns True when graphify.extract is importable."""
        from agent_notes.services.code_graph import graphify_available
        mock_module = type("MockModule", (), {})()
        with patch.dict("sys.modules", {"graphify": mock_module, "graphify.extract": mock_module}):
            assert graphify_available() is True


class TestGraphToWikiTerms:
    """Pure logic tests — no graphify dependency needed."""

    @pytest.fixture
    def sample_graph(self):
        return {
            "nodes": [
                {"id": "auth_userservice", "label": "UserService", "source_file": "auth.py", "source_location": "L42", "type": "class"},
                {"id": "auth_login", "label": "login", "source_file": "auth.py", "source_location": "L10", "type": "function"},
                {"id": "auth_validate", "label": "validate_token", "source_file": "auth.py", "source_location": "L80", "type": "function"},
                {"id": "payments_gateway", "label": "PaymentGateway", "source_file": "payments.py", "source_location": "L5", "type": "class"},
                {"id": "payments_process", "label": "process_payment", "source_file": "payments.py", "source_location": "L20", "type": "function"},
            ],
            "edges": [
                {"source": "auth_userservice", "target": "payments_gateway", "relation": "calls", "confidence": "EXTRACTED"},
                {"source": "auth_login", "target": "auth_userservice", "relation": "calls", "confidence": "EXTRACTED"},
                {"source": "auth_userservice", "target": "auth_validate", "relation": "calls", "confidence": "EXTRACTED"},
                {"source": "payments_gateway", "target": "payments_process", "relation": "contains", "confidence": "EXTRACTED"},
                {"source": "auth_userservice", "target": "auth_login", "relation": "contains", "confidence": "EXTRACTED"},
            ],
            "communities": {
                0: ["auth_userservice", "auth_login", "auth_validate"],
                1: ["payments_gateway", "payments_process"],
            },
            "cohesion": {0: 0.85, 1: 0.72},
            "god_nodes": [{"label": "UserService", "degree": 4}],
            "stats": {"files_parsed": 2, "nodes": 5, "edges": 5, "communities": 2},
        }

    def test_maps_high_degree_class_to_entity(self, sample_graph):
        from agent_notes.services.code_graph import graph_to_wiki_terms
        result = graph_to_wiki_terms(sample_graph)
        assert "UserService" in result["entities"]

    def test_maps_high_degree_function_to_entity(self, sample_graph):
        from agent_notes.services.code_graph import graph_to_wiki_terms
        result = graph_to_wiki_terms(sample_graph)
        # PaymentGateway has degree 2 (calls + contains), should be entity
        assert "PaymentGateway" in result["entities"]

    def test_skips_low_degree_function(self, sample_graph):
        from agent_notes.services.code_graph import graph_to_wiki_terms
        result = graph_to_wiki_terms(sample_graph)
        # process_payment has degree 1 (only "contains" edge), should be skipped
        assert "process_payment" not in result["entities"]

    def test_maps_multi_member_community_to_concept(self, sample_graph):
        from agent_notes.services.code_graph import graph_to_wiki_terms
        result = graph_to_wiki_terms(sample_graph)
        assert len(result["concepts"]) >= 1

    def test_skips_single_member_community(self):
        from agent_notes.services.code_graph import graph_to_wiki_terms
        graph = {
            "nodes": [{"id": "solo", "label": "Solo", "source_file": "solo.py", "source_location": "L1", "type": "class"}],
            "edges": [],
            "communities": {0: ["solo"]},
            "cohesion": {0: 1.0},
            "god_nodes": [],
            "stats": {"files_parsed": 1, "nodes": 1, "edges": 0, "communities": 1},
        }
        result = graph_to_wiki_terms(graph)
        assert result["concepts"] == []

    def test_empty_graph_returns_empty_lists(self):
        from agent_notes.services.code_graph import graph_to_wiki_terms
        empty = {"nodes": [], "edges": [], "communities": {}, "cohesion": {}, "god_nodes": [], "stats": {"files_parsed": 0, "nodes": 0, "edges": 0, "communities": 0}}
        result = graph_to_wiki_terms(empty)
        assert result["entities"] == []
        assert result["concepts"] == []
        assert result["edges_by_entity"] == {}

    def test_edges_by_entity_populated(self, sample_graph):
        from agent_notes.services.code_graph import graph_to_wiki_terms
        result = graph_to_wiki_terms(sample_graph)
        # UserService calls PaymentGateway, login, validate_token
        assert "UserService" in result["edges_by_entity"]
        targets = [e["target"] for e in result["edges_by_entity"]["UserService"]]
        assert "PaymentGateway" in targets

    def test_skips_private_nodes(self):
        from agent_notes.services.code_graph import graph_to_wiki_terms
        graph = {
            "nodes": [
                {"id": "mod__init", "label": "__init__", "source_file": "mod.py", "source_location": "L1", "type": "function"},
                {"id": "mod_public", "label": "PublicClass", "source_file": "mod.py", "source_location": "L5", "type": "class"},
            ],
            "edges": [
                {"source": "mod_public", "target": "mod__init", "relation": "calls", "confidence": "EXTRACTED"},
                {"source": "mod__init", "target": "mod_public", "relation": "calls", "confidence": "EXTRACTED"},
            ],
            "communities": {0: ["mod__init", "mod_public"]},
            "cohesion": {0: 1.0}, "god_nodes": [],
            "stats": {"files_parsed": 1, "nodes": 2, "edges": 2, "communities": 1},
        }
        result = graph_to_wiki_terms(graph)
        assert "__init__" not in result["entities"]


class TestSaveGraphJson:
    def test_writes_to_raw_directory(self, tmp_path):
        from agent_notes.services.code_graph import save_graph_json
        graph_data = {"nodes": [], "edges": [], "stats": {}}
        path = save_graph_json(tmp_path, "my-project", graph_data)
        assert path.exists()
        assert path.parent.name == "raw"
        assert path.name == "my-project-graph.json"

    def test_output_is_valid_json(self, tmp_path):
        from agent_notes.services.code_graph import save_graph_json
        graph_data = {"nodes": [{"id": "a"}], "edges": [], "stats": {"nodes": 1}}
        path = save_graph_json(tmp_path, "test", graph_data)
        loaded = json.loads(path.read_text())
        assert loaded["nodes"][0]["id"] == "a"

    def test_creates_raw_dir_if_missing(self, tmp_path):
        from agent_notes.services.code_graph import save_graph_json
        wiki_root = tmp_path / "new_wiki"
        # Don't create it — save_graph_json should handle it
        path = save_graph_json(wiki_root, "slug", {"nodes": []})
        assert path.exists()


class TestEmptyGraph:
    def test_returns_expected_structure(self):
        from agent_notes.services.code_graph import _empty_graph
        result = _empty_graph()
        assert result["nodes"] == []
        assert result["edges"] == []
        assert result["communities"] == {}
        assert result["stats"]["nodes"] == 0

2. Add to tests/unit/services/test_wiki_backend.py

class TestWikiIngestFolderGraphify:
    """Tests for Graphify integration in wiki_ingest_folder."""

    def test_extracts_entities_when_graphify_available(self, tmp_path):
        """When graphify is available and folder has code, entities are auto-discovered."""
        from agent_notes.services.wiki_backend import wiki_ingest_folder, wiki_init

        wiki_root = tmp_path / "wiki_root"
        wiki_init(wiki_root)
        folder = tmp_path / "project"
        folder.mkdir()
        (folder / "service.py").write_text("class UserService:\n    def login(self): pass\n")

        mock_graph = {
            "nodes": [{"id": "service_userservice", "label": "UserService", "source_file": "service.py", "source_location": "L1", "type": "class"}],
            "edges": [{"source": "service_userservice", "target": "service_login", "relation": "contains", "confidence": "EXTRACTED"}],
            "communities": {0: ["service_userservice"]},
            "cohesion": {0: 1.0},
            "god_nodes": [{"label": "UserService", "degree": 1}],
            "stats": {"files_parsed": 1, "nodes": 1, "edges": 1, "communities": 1},
        }
        mock_terms = {"entities": ["UserService"], "concepts": [], "edges_by_entity": {}}

        with patch("agent_notes.services.wiki_backend.graphify_available", return_value=True) as mock_avail, \
             patch("agent_notes.services.wiki_backend.extract_code_graph", return_value=mock_graph) as mock_extract, \
             patch("agent_notes.services.wiki_backend.graph_to_wiki_terms", return_value=mock_terms) as mock_terms_fn, \
             patch("agent_notes.services.wiki_backend.save_graph_json", return_value=wiki_root / "raw" / "test-graph.json"):
            result = wiki_ingest_folder(wiki_root, folder_path=folder)
            assert len(result.get("entities", [])) >= 1
            mock_extract.assert_called_once()

    def test_falls_back_when_graphify_unavailable(self, tmp_path):
        """Without graphify, folder ingestion works normally with no entities discovered."""
        from agent_notes.services.wiki_backend import wiki_ingest_folder, wiki_init

        wiki_root = tmp_path / "wiki_root"
        wiki_init(wiki_root)
        folder = tmp_path / "project"
        folder.mkdir()
        (folder / "main.py").write_text("print('hello')")

        result = wiki_ingest_folder(wiki_root, folder_path=folder)
        assert len(result.get("source", [])) >= 1
        assert result.get("entities", []) == []

    def test_falls_back_on_extraction_error(self, tmp_path):
        """Graphify errors should not break folder ingestion."""
        from agent_notes.services.wiki_backend import wiki_ingest_folder, wiki_init

        wiki_root = tmp_path / "wiki_root"
        wiki_init(wiki_root)
        folder = tmp_path / "project"
        folder.mkdir()
        (folder / "buggy.py").write_text("class Broken: pass")

        with patch("agent_notes.services.wiki_backend.graphify_available", return_value=True), \
             patch("agent_notes.services.wiki_backend.extract_code_graph", side_effect=RuntimeError("tree-sitter crash")):
            result = wiki_ingest_folder(wiki_root, folder_path=folder)
            assert len(result.get("source", [])) >= 1  # Ingestion still works

    def test_no_overhead_for_non_code_folders(self, tmp_path):
        """Folders with only docs/config should not attempt Graphify extraction."""
        from agent_notes.services.wiki_backend import wiki_ingest_folder, wiki_init

        wiki_root = tmp_path / "wiki_root"
        wiki_init(wiki_root)
        folder = tmp_path / "docs"
        folder.mkdir()
        (folder / "README.md").write_text("# Docs\nSome documentation")

        with patch("agent_notes.services.code_graph.graphify_available") as mock_avail:
            result = wiki_ingest_folder(wiki_root, folder_path=folder)
            mock_avail.assert_not_called()  # Should never check — no code files


class TestMergeUnique:
    def test_merges_without_duplicates(self):
        from agent_notes.services.wiki_backend import _merge_unique
        assert _merge_unique(["A", "B"], ["B", "C"]) == ["A", "B", "C"]

    def test_case_insensitive_dedup(self):
        from agent_notes.services.wiki_backend import _merge_unique
        assert _merge_unique(["UserService"], ["userservice", "Gateway"]) == ["UserService", "Gateway"]

    def test_preserves_order(self):
        from agent_notes.services.wiki_backend import _merge_unique
        assert _merge_unique(["Z", "A"], ["M", "B"]) == ["Z", "A", "M", "B"]

    def test_empty_inputs(self):
        from agent_notes.services.wiki_backend import _merge_unique
        assert _merge_unique([], []) == []
        assert _merge_unique([], ["A"]) == ["A"]
        assert _merge_unique(["A"], []) == ["A"]

3. Add to tests/functional/memory/test_memory_command.py

class TestIngestFolderDetection:
    def test_detects_folder_path_and_routes_to_folder_ingest(self, tmp_path):
        """When ingest title is a folder path, routes to wiki_ingest_folder."""
        from agent_notes.commands.memory import do_ingest

        folder = tmp_path / "my_project"
        folder.mkdir()
        (folder / "app.py").write_text("class App: pass")

        with patch("agent_notes.commands.memory._load_memory_config", return_value=("wiki", tmp_path / "wiki")), \
             patch("agent_notes.services.wiki_backend.wiki_init"), \
             patch("agent_notes.services.wiki_backend.wiki_ingest_folder", return_value={"source": [], "concepts": [], "entities": []}) as mock_folder:
            do_ingest(str(folder), "test body")
            mock_folder.assert_called_once()

    def test_non_folder_title_routes_to_text_ingest(self):
        """When ingest title is plain text, routes to wiki_ingest."""
        from agent_notes.commands.memory import do_ingest

        with patch("agent_notes.commands.memory._load_memory_config", return_value=("wiki", Path("/tmp/wiki"))), \
             patch("agent_notes.services.wiki_backend.wiki_ingest", return_value={"source": [], "concepts": [], "entities": []}) as mock_text:
            do_ingest("My Title", "My body")
            mock_text.assert_called_once()

Note on Import Strategy

The test mocks need to patch at the correct location. Since wiki_ingest_folder() imports from code_graph lazily:

from .code_graph import graphify_available, extract_code_graph, ...

The mock targets should be where the names are used, not where they're defined. If the imports are inline in the function, patch agent_notes.services.code_graph.graphify_available. If they're imported at the top of wiki_backend.py at runtime, patch agent_notes.services.wiki_backend.graphify_available. The exact mock path depends on how #8 implements the import.

Dependencies

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions