first commit

This commit is contained in:
2026-02-25 23:49:54 -05:00
commit 4d097161cb
1775 changed files with 452827 additions and 0 deletions

View File

@@ -0,0 +1,283 @@
"""Tests for ArchitectureTracker."""
import asyncio
import json
import os
import tempfile
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from app_factory.core.architecture_tracker import ArchitectureTracker
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def tmp_data_dir(tmp_path):
"""Temporary data directory."""
return str(tmp_path / "data")
@pytest.fixture
def tracker(tmp_data_dir):
"""ArchitectureTracker with no Claude SDK client."""
return ArchitectureTracker(data_dir=tmp_data_dir)
@pytest.fixture
def tracker_with_client(tmp_data_dir):
"""ArchitectureTracker with a mocked Claude SDK client."""
mock_client = AsyncMock()
t = ArchitectureTracker(data_dir=tmp_data_dir)
t._client = mock_client
return t, mock_client
# ---------------------------------------------------------------------------
# Initialization
# ---------------------------------------------------------------------------
class TestInitialization:
def test_creates_default_architecture(self, tracker):
arch = tracker._architecture
assert "modules" in arch
assert "utilities" in arch
assert "design_patterns" in arch
assert "naming_conventions" in arch
assert "tech_stack" in arch
assert arch["version"] == 1
assert "last_updated" in arch
def test_creates_data_directory(self, tmp_data_dir):
tracker = ArchitectureTracker(data_dir=tmp_data_dir)
assert os.path.isdir(tmp_data_dir)
def test_default_architecture_has_empty_lists(self, tracker):
assert tracker._architecture["modules"] == []
assert tracker._architecture["utilities"] == []
assert tracker._architecture["design_patterns"] == []
def test_default_naming_conventions(self, tracker):
conventions = tracker._architecture["naming_conventions"]
assert conventions["variables"] == "snake_case"
assert conventions["classes"] == "PascalCase"
# ---------------------------------------------------------------------------
# Load / Save Persistence
# ---------------------------------------------------------------------------
class TestPersistence:
def test_save_and_load(self, tmp_data_dir):
tracker1 = ArchitectureTracker(data_dir=tmp_data_dir)
tracker1.add_module("TestModule", "A test module", "test.py")
tracker2 = ArchitectureTracker(data_dir=tmp_data_dir)
assert len(tracker2._architecture["modules"]) == 1
assert tracker2._architecture["modules"][0]["name"] == "TestModule"
def test_save_updates_timestamp(self, tracker):
old_ts = tracker._architecture.get("last_updated", "")
tracker.save_architecture(tracker._architecture)
new_ts = tracker._architecture["last_updated"]
# Timestamp should be updated (or at least present)
assert new_ts is not None
assert len(new_ts) > 0
def test_load_corrupt_file_returns_default(self, tmp_data_dir):
os.makedirs(tmp_data_dir, exist_ok=True)
corrupt_path = os.path.join(tmp_data_dir, "global_architecture.json")
with open(corrupt_path, "w") as f:
f.write("NOT VALID JSON {{{")
tracker = ArchitectureTracker(data_dir=tmp_data_dir)
assert tracker._architecture["version"] == 1
assert tracker._architecture["modules"] == []
# ---------------------------------------------------------------------------
# add_module
# ---------------------------------------------------------------------------
class TestAddModule:
def test_add_module(self, tracker):
tracker.add_module("MyModule", "Does something", "src/my_module.py")
assert len(tracker._architecture["modules"]) == 1
mod = tracker._architecture["modules"][0]
assert mod["name"] == "MyModule"
assert mod["purpose"] == "Does something"
assert mod["file_path"] == "src/my_module.py"
def test_add_module_persists(self, tmp_data_dir):
t1 = ArchitectureTracker(data_dir=tmp_data_dir)
t1.add_module("Persisted", "persists", "p.py")
t2 = ArchitectureTracker(data_dir=tmp_data_dir)
assert any(m["name"] == "Persisted" for m in t2._architecture["modules"])
# ---------------------------------------------------------------------------
# add_utility
# ---------------------------------------------------------------------------
class TestAddUtility:
def test_add_utility(self, tracker):
tracker.add_utility("helper_func", "Helps with things", "utils.py")
assert len(tracker._architecture["utilities"]) == 1
util = tracker._architecture["utilities"][0]
assert util["name"] == "helper_func"
assert util["description"] == "Helps with things"
assert util["file_path"] == "utils.py"
def test_add_utility_persists(self, tmp_data_dir):
t1 = ArchitectureTracker(data_dir=tmp_data_dir)
t1.add_utility("persisted_func", "persists", "p.py")
t2 = ArchitectureTracker(data_dir=tmp_data_dir)
assert any(u["name"] == "persisted_func" for u in t2._architecture["utilities"])
# ---------------------------------------------------------------------------
# get_architecture_summary
# ---------------------------------------------------------------------------
class TestGetArchitectureSummary:
def test_returns_formatted_string(self, tracker):
tracker.add_module("GraphEngine", "Runs the graph", "graph.py")
tracker.add_utility("parse_json", "Parses JSON input", "utils.py")
summary = tracker.get_architecture_summary()
assert isinstance(summary, str)
assert "Project Architecture Summary" in summary
assert "GraphEngine" in summary
assert "parse_json" in summary
def test_includes_tech_stack(self, tracker):
summary = tracker.get_architecture_summary()
assert "Tech Stack" in summary
assert "Python" in summary
def test_includes_naming_conventions(self, tracker):
summary = tracker.get_architecture_summary()
assert "Naming Conventions" in summary
assert "snake_case" in summary
def test_respects_max_tokens_limit(self, tracker):
# Add many modules to produce a large summary
for i in range(200):
tracker._architecture["modules"].append({
"name": f"Module_{i}",
"purpose": f"Purpose of module {i} with extra text for padding",
"file_path": f"src/module_{i}.py",
})
# Very small token limit
summary = tracker.get_architecture_summary(max_tokens=50)
# 50 tokens * 4 chars = 200 chars max
assert len(summary) <= 200
def test_empty_architecture_still_returns_summary(self, tracker):
summary = tracker.get_architecture_summary()
assert "Project Architecture Summary" in summary
# ---------------------------------------------------------------------------
# update_architecture (async)
# ---------------------------------------------------------------------------
class TestUpdateArchitecture:
def test_basic_extraction_no_client(self, tracker, tmp_path):
# Create a sample Python file
py_file = tmp_path / "sample.py"
py_file.write_text(
'"""Sample module."""\n\n'
'class SampleClass:\n'
' """A sample class for testing."""\n'
' pass\n\n'
'def sample_function():\n'
' """A sample utility function."""\n'
' pass\n\n'
'def _private_function():\n'
' """Should be skipped."""\n'
' pass\n'
)
task = {"title": "Test task", "description": "Testing extraction"}
asyncio.run(tracker.update_architecture(task, [str(py_file)]))
modules = tracker._architecture["modules"]
utilities = tracker._architecture["utilities"]
assert any(m["name"] == "SampleClass" for m in modules)
assert any(u["name"] == "sample_function" for u in utilities)
# Private functions should be skipped
assert not any(u["name"] == "_private_function" for u in utilities)
def test_skips_non_python_files(self, tracker, tmp_path):
txt_file = tmp_path / "readme.txt"
txt_file.write_text("Not a Python file")
task = {"title": "Test task"}
asyncio.run(tracker.update_architecture(task, [str(txt_file)]))
assert tracker._architecture["modules"] == []
assert tracker._architecture["utilities"] == []
def test_skips_nonexistent_files(self, tracker):
task = {"title": "Test task"}
asyncio.run(tracker.update_architecture(task, ["/nonexistent/file.py"]))
assert tracker._architecture["modules"] == []
def test_ai_extraction_with_mock_client(self, tracker_with_client, tmp_path):
tracker, mock_client = tracker_with_client
py_file = tmp_path / "ai_sample.py"
py_file.write_text(
'class AIExtracted:\n'
' """Extracted by AI."""\n'
' pass\n'
)
mock_response = MagicMock()
mock_response.text = json.dumps({
"classes": [{"name": "AIExtracted", "purpose": "AI detected class"}],
"functions": [{"name": "ai_helper", "description": "AI detected function"}],
})
mock_client.complete.return_value = mock_response
task = {"title": "AI test task"}
asyncio.run(tracker.update_architecture(task, [str(py_file)]))
modules = tracker._architecture["modules"]
utilities = tracker._architecture["utilities"]
assert any(m["name"] == "AIExtracted" for m in modules)
assert any(u["name"] == "ai_helper" for u in utilities)
def test_ai_extraction_fallback_on_failure(self, tracker_with_client, tmp_path):
tracker, mock_client = tracker_with_client
py_file = tmp_path / "fallback_sample.py"
py_file.write_text(
'class FallbackClass:\n'
' """Falls back to basic extraction."""\n'
' pass\n'
)
mock_client.complete.side_effect = Exception("API error")
task = {"title": "Fallback test"}
asyncio.run(tracker.update_architecture(task, [str(py_file)]))
# Should fall back to basic extraction
modules = tracker._architecture["modules"]
assert any(m["name"] == "FallbackClass" for m in modules)
def test_no_duplicate_modules(self, tracker, tmp_path):
py_file = tmp_path / "dup.py"
py_file.write_text('class DupClass:\n pass\n')
task = {"title": "Dup test"}
asyncio.run(tracker.update_architecture(task, [str(py_file)]))
asyncio.run(tracker.update_architecture(task, [str(py_file)]))
names = [m["name"] for m in tracker._architecture["modules"]]
assert names.count("DupClass") == 1