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
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.pyTests for the pure-logic functions in
code_graph.py. These do NOT require graphifyy installed — they test the mapping/conversion logic with plain dicts.2. Add to
tests/unit/services/test_wiki_backend.py3. Add to
tests/functional/memory/test_memory_command.pyNote on Import Strategy
The test mocks need to patch at the correct location. Since
wiki_ingest_folder()imports fromcode_graphlazily: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 ofwiki_backend.pyat runtime, patchagent_notes.services.wiki_backend.graphify_available. The exact mock path depends on how #8 implements the import.Dependencies