abd52534bb
- Add tests for config, convo_miner, spellcheck, knowledge_graph - Fix Windows PermissionError in test cleanup (chromadb file locks) - Add UTF-8 encoding to split_mega_files, entity_registry, hooks_cli - Fix mcp_server parse_known_args logging for unknown args - Set coverage threshold to 85 in pyproject.toml and CI - Reset all version files to 3.0.11 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
265 lines
9.5 KiB
Python
265 lines
9.5 KiB
Python
"""Tests for mempalace.room_detector_local."""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from mempalace.room_detector_local import (
|
|
FOLDER_ROOM_MAP,
|
|
detect_rooms_from_files,
|
|
detect_rooms_from_folders,
|
|
detect_rooms_local,
|
|
get_user_approval,
|
|
print_proposed_structure,
|
|
save_config,
|
|
)
|
|
|
|
|
|
# ── FOLDER_ROOM_MAP ────────────────────────────────────────────────────
|
|
|
|
|
|
def test_folder_room_map_has_expected_mappings():
|
|
assert FOLDER_ROOM_MAP["frontend"] == "frontend"
|
|
assert FOLDER_ROOM_MAP["backend"] == "backend"
|
|
assert FOLDER_ROOM_MAP["docs"] == "documentation"
|
|
assert FOLDER_ROOM_MAP["tests"] == "testing"
|
|
assert FOLDER_ROOM_MAP["config"] == "configuration"
|
|
|
|
|
|
def test_folder_room_map_alternative_names():
|
|
assert FOLDER_ROOM_MAP["front-end"] == "frontend"
|
|
assert FOLDER_ROOM_MAP["back-end"] == "backend"
|
|
assert FOLDER_ROOM_MAP["server"] == "backend"
|
|
assert FOLDER_ROOM_MAP["client"] == "frontend"
|
|
assert FOLDER_ROOM_MAP["api"] == "backend"
|
|
|
|
|
|
# ── detect_rooms_from_folders ───────────────────────────────────────────
|
|
|
|
|
|
def test_detect_rooms_from_folders_standard_layout(tmp_path):
|
|
(tmp_path / "frontend").mkdir()
|
|
(tmp_path / "backend").mkdir()
|
|
(tmp_path / "docs").mkdir()
|
|
rooms = detect_rooms_from_folders(str(tmp_path))
|
|
room_names = {r["name"] for r in rooms}
|
|
assert "frontend" in room_names
|
|
assert "backend" in room_names
|
|
assert "documentation" in room_names
|
|
|
|
|
|
def test_detect_rooms_from_folders_always_has_general(tmp_path):
|
|
rooms = detect_rooms_from_folders(str(tmp_path))
|
|
room_names = {r["name"] for r in rooms}
|
|
assert "general" in room_names
|
|
|
|
|
|
def test_detect_rooms_from_folders_empty_dir(tmp_path):
|
|
rooms = detect_rooms_from_folders(str(tmp_path))
|
|
# Should at least have "general"
|
|
assert len(rooms) >= 1
|
|
assert any(r["name"] == "general" for r in rooms)
|
|
|
|
|
|
def test_detect_rooms_from_folders_skips_git(tmp_path):
|
|
(tmp_path / ".git").mkdir()
|
|
(tmp_path / "node_modules").mkdir()
|
|
(tmp_path / "frontend").mkdir()
|
|
rooms = detect_rooms_from_folders(str(tmp_path))
|
|
room_names = {r["name"] for r in rooms}
|
|
assert ".git" not in room_names
|
|
assert "node_modules" not in room_names
|
|
|
|
|
|
def test_detect_rooms_from_folders_nested_dirs(tmp_path):
|
|
src = tmp_path / "src"
|
|
src.mkdir()
|
|
(src / "components").mkdir()
|
|
(src / "routes").mkdir()
|
|
rooms = detect_rooms_from_folders(str(tmp_path))
|
|
room_names = {r["name"] for r in rooms}
|
|
# Nested dirs should be detected at one level deep
|
|
assert "frontend" in room_names or "backend" in room_names
|
|
|
|
|
|
def test_detect_rooms_from_folders_room_has_description(tmp_path):
|
|
(tmp_path / "docs").mkdir()
|
|
rooms = detect_rooms_from_folders(str(tmp_path))
|
|
doc_room = next((r for r in rooms if r["name"] == "documentation"), None)
|
|
assert doc_room is not None
|
|
assert "description" in doc_room
|
|
assert "docs" in doc_room["description"]
|
|
|
|
|
|
def test_detect_rooms_from_folders_room_has_keywords(tmp_path):
|
|
(tmp_path / "frontend").mkdir()
|
|
rooms = detect_rooms_from_folders(str(tmp_path))
|
|
fe_room = next((r for r in rooms if r["name"] == "frontend"), None)
|
|
assert fe_room is not None
|
|
assert "keywords" in fe_room
|
|
assert len(fe_room["keywords"]) > 0
|
|
|
|
|
|
def test_detect_rooms_from_folders_custom_named_dirs(tmp_path):
|
|
(tmp_path / "mylib").mkdir()
|
|
rooms = detect_rooms_from_folders(str(tmp_path))
|
|
room_names = {r["name"] for r in rooms}
|
|
# Custom dir names that don't match FOLDER_ROOM_MAP get added as-is
|
|
assert "mylib" in room_names or "general" in room_names
|
|
|
|
|
|
# ── detect_rooms_from_files ─────────────────────────────────────────────
|
|
|
|
|
|
def test_detect_rooms_from_files_with_matching_filenames(tmp_path):
|
|
# Create files whose names contain room keywords
|
|
for name in ["test_auth.py", "test_login.py", "test_api.py"]:
|
|
(tmp_path / name).write_text("content")
|
|
rooms = detect_rooms_from_files(str(tmp_path))
|
|
room_names = {r["name"] for r in rooms}
|
|
assert "testing" in room_names or "general" in room_names
|
|
|
|
|
|
def test_detect_rooms_from_files_empty_dir(tmp_path):
|
|
rooms = detect_rooms_from_files(str(tmp_path))
|
|
assert len(rooms) >= 1
|
|
assert any(r["name"] == "general" for r in rooms)
|
|
|
|
|
|
def test_detect_rooms_from_files_caps_at_six(tmp_path):
|
|
# Create many files with different keywords to hit the cap
|
|
for keyword in ["test", "doc", "api", "config", "frontend", "backend", "design", "meeting"]:
|
|
for i in range(3):
|
|
(tmp_path / f"{keyword}_file_{i}.txt").write_text("content")
|
|
rooms = detect_rooms_from_files(str(tmp_path))
|
|
assert len(rooms) <= 6
|
|
|
|
|
|
# ── save_config ─────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_save_config_creates_yaml(tmp_path):
|
|
rooms = [
|
|
{"name": "frontend", "description": "UI files", "keywords": ["frontend"]},
|
|
{"name": "backend", "description": "Server files", "keywords": ["backend"]},
|
|
]
|
|
save_config(str(tmp_path), "myproject", rooms)
|
|
config_file = tmp_path / "mempalace.yaml"
|
|
assert config_file.exists()
|
|
content = config_file.read_text()
|
|
assert "myproject" in content
|
|
assert "frontend" in content
|
|
assert "backend" in content
|
|
|
|
|
|
def test_save_config_valid_yaml(tmp_path):
|
|
import yaml
|
|
|
|
rooms = [{"name": "general", "description": "All files", "keywords": []}]
|
|
save_config(str(tmp_path), "test_proj", rooms)
|
|
config_file = tmp_path / "mempalace.yaml"
|
|
data = yaml.safe_load(config_file.read_text())
|
|
assert data["wing"] == "test_proj"
|
|
assert len(data["rooms"]) == 1
|
|
assert data["rooms"][0]["name"] == "general"
|
|
|
|
|
|
# ── print_proposed_structure ──────────────────────────────────────────
|
|
|
|
|
|
def test_print_proposed_structure(capsys):
|
|
rooms = [
|
|
{"name": "frontend", "description": "UI files"},
|
|
{"name": "general", "description": "Everything else"},
|
|
]
|
|
print_proposed_structure("myapp", rooms, 42, "folder structure")
|
|
out = capsys.readouterr().out
|
|
assert "myapp" in out
|
|
assert "frontend" in out
|
|
assert "42 files" in out
|
|
assert "folder structure" in out
|
|
|
|
|
|
# ── get_user_approval ─────────────────────────────────────────────────
|
|
|
|
|
|
def test_get_user_approval_accept_all():
|
|
rooms = [{"name": "frontend", "description": "UI"}]
|
|
with patch("builtins.input", return_value=""):
|
|
result = get_user_approval(rooms)
|
|
assert result == rooms
|
|
|
|
|
|
def test_get_user_approval_edit_remove():
|
|
rooms = [
|
|
{"name": "frontend", "description": "UI"},
|
|
{"name": "backend", "description": "Server"},
|
|
]
|
|
with patch("builtins.input", side_effect=["edit", "1", "n"]):
|
|
result = get_user_approval(rooms)
|
|
# Room 1 (frontend) removed
|
|
assert len(result) == 1
|
|
assert result[0]["name"] == "backend"
|
|
|
|
|
|
def test_get_user_approval_add_room():
|
|
rooms = [{"name": "general", "description": "All files"}]
|
|
with patch(
|
|
"builtins.input",
|
|
side_effect=[
|
|
"add",
|
|
"custom_room",
|
|
"My custom room",
|
|
"",
|
|
],
|
|
):
|
|
result = get_user_approval(rooms)
|
|
names = [r["name"] for r in result]
|
|
assert "custom_room" in names
|
|
|
|
|
|
# ── detect_rooms_local ────────────────────────────────────────────────
|
|
|
|
|
|
def test_detect_rooms_local_yes_mode(tmp_path):
|
|
(tmp_path / "docs").mkdir()
|
|
(tmp_path / "docs" / "readme.md").write_text("hello")
|
|
mock_miner = MagicMock()
|
|
mock_miner.scan_project.return_value = ["file1.py"]
|
|
with patch.dict("sys.modules", {"mempalace.miner": mock_miner}):
|
|
detect_rooms_local(str(tmp_path), yes=True)
|
|
assert (tmp_path / "mempalace.yaml").exists()
|
|
|
|
|
|
def test_detect_rooms_local_fallback_to_files(tmp_path):
|
|
"""When folder detection gives only 'general', falls back to file patterns."""
|
|
for i in range(3):
|
|
(tmp_path / f"test_file_{i}.py").write_text("content")
|
|
mock_miner = MagicMock()
|
|
mock_miner.scan_project.return_value = ["f1", "f2"]
|
|
with patch.dict("sys.modules", {"mempalace.miner": mock_miner}):
|
|
detect_rooms_local(str(tmp_path), yes=True)
|
|
assert (tmp_path / "mempalace.yaml").exists()
|
|
|
|
|
|
def test_detect_rooms_local_missing_dir():
|
|
"""Non-existent directory causes sys.exit."""
|
|
import pytest
|
|
|
|
with pytest.raises(SystemExit):
|
|
detect_rooms_local("/nonexistent/path/that/does/not/exist", yes=True)
|
|
|
|
|
|
def test_detect_rooms_local_interactive(tmp_path):
|
|
(tmp_path / "src").mkdir()
|
|
(tmp_path / "src" / "main.py").write_text("code")
|
|
mock_miner = MagicMock()
|
|
mock_miner.scan_project.return_value = ["f1"]
|
|
with (
|
|
patch.dict("sys.modules", {"mempalace.miner": mock_miner}),
|
|
patch(
|
|
"mempalace.room_detector_local.get_user_approval",
|
|
return_value=[{"name": "general", "description": "All files", "keywords": []}],
|
|
),
|
|
):
|
|
detect_rooms_local(str(tmp_path), yes=False)
|
|
assert (tmp_path / "mempalace.yaml").exists()
|