mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-27 16:04:21 +00:00
feat(test): Phase 1.5 coverage expansion - COV-001 to COV-013
Coverage baseline raised from 13.65% to 26% (+12.35%) Gate raised from 12% to 18% Tasks completed: - COV-001: Command system unit tests (100% coverage) - COV-002: API service unit tests batch 1 (user/apikey/model/provider) - COV-003: Provider model manager unit tests - COV-004: Pipeline remaining stage tests (aggregator/cntfilter/longtext/msgtrun) - COV-005: Storage and utils coverage pass - COV-006: Gate ratchet 12%→15% - COV-007: Gate ratchet 15%→18% - COV-008: API service batch 2 (bot/pipeline/webhook/space/maintenance/mcp) - COV-009: Blocked - API controller circular import issue documented - COV-010: Plugin runtime unit tests (+0.08%) - COV-011: RAG and vector unit tests (+0.68%) - COV-012: Core boot and migration unit tests - COV-013: Provider requester logic unit tests (+0.62%) Key additions: - tests/utils/import_isolation.py: sys.modules isolation for circular imports - Provider requester mock tests: proved HTTP-dependent code can be tested locally - Vector filter utilities: 100% coverage on pure functions - API services: fake persistence pattern for unit testing Blocked issue COV-009 documented in langbot-test-plan/1.5/issues/ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
"""
|
||||
Tests for langbot.pkg.utils.importutil module.
|
||||
|
||||
Tests import utility functions:
|
||||
- import_dir: imports modules from a directory
|
||||
- import_modules_in_pkg: imports all modules in a package
|
||||
- import_modules_in_pkgs: imports all modules in multiple packages
|
||||
- import_dot_style_dir: imports modules using dot notation path
|
||||
- read_resource_file: reads a text resource file
|
||||
- read_resource_file_bytes: reads a binary resource file
|
||||
- list_resource_files: lists files in a resource directory
|
||||
|
||||
Uses mocking for import operations to avoid actual module imports.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import importlib
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
||||
class TestImportDir:
|
||||
"""Test import_dir function."""
|
||||
|
||||
def test_calls_importlib_for_each_python_file(self, tmp_path):
|
||||
"""Should call importlib.import_module for each .py file."""
|
||||
module_dir = tmp_path / "test_modules"
|
||||
module_dir.mkdir()
|
||||
|
||||
(module_dir / "__init__.py").write_text("")
|
||||
(module_dir / "module_a.py").write_text("VALUE_A = 'a'\n")
|
||||
(module_dir / "module_b.py").write_text("VALUE_B = 'b'\n")
|
||||
(module_dir / "readme.txt").write_text("not a module")
|
||||
|
||||
from langbot.pkg.utils import importutil
|
||||
|
||||
with patch.object(importlib, "import_module") as mock_import:
|
||||
importutil.import_dir(str(module_dir), path_prefix="test_prefix.")
|
||||
# Should call import_module for each .py file (excluding __init__.py)
|
||||
assert mock_import.call_count == 2
|
||||
|
||||
def test_skips_init_py(self, tmp_path):
|
||||
"""Should skip __init__.py when importing."""
|
||||
module_dir = tmp_path / "test_modules"
|
||||
module_dir.mkdir()
|
||||
|
||||
(module_dir / "__init__.py").write_text("")
|
||||
(module_dir / "regular.py").write_text("VALUE = 1\n")
|
||||
|
||||
from langbot.pkg.utils import importutil
|
||||
|
||||
with patch.object(importlib, "import_module") as mock_import:
|
||||
importutil.import_dir(str(module_dir), path_prefix="test_prefix.")
|
||||
# __init__.py should be skipped
|
||||
mock_import.assert_called_once()
|
||||
# The call should not include __init__
|
||||
call_args = mock_import.call_args[0][0]
|
||||
assert "__init__" not in call_args
|
||||
|
||||
def test_ignores_non_py_files(self, tmp_path):
|
||||
"""Should ignore non-.py files."""
|
||||
module_dir = tmp_path / "test_modules"
|
||||
module_dir.mkdir()
|
||||
|
||||
(module_dir / "module.py").write_text("VALUE = 1\n")
|
||||
(module_dir / "readme.txt").write_text("text")
|
||||
(module_dir / "data.json").write_text("{}")
|
||||
|
||||
from langbot.pkg.utils import importutil
|
||||
|
||||
with patch.object(importlib, "import_module") as mock_import:
|
||||
importutil.import_dir(str(module_dir), path_prefix="test_prefix.")
|
||||
# Only .py files should be imported
|
||||
assert mock_import.call_count == 1
|
||||
|
||||
|
||||
class TestImportModulesInPkg:
|
||||
"""Test import_modules_in_pkg function."""
|
||||
|
||||
def test_imports_modules_from_package(self, tmp_path):
|
||||
"""Should import all modules from a package object."""
|
||||
mock_pkg = MagicMock()
|
||||
mock_pkg.__file__ = str(tmp_path / "__init__.py")
|
||||
|
||||
(tmp_path / "__init__.py").write_text("")
|
||||
(tmp_path / "mod1.py").write_text("MOD1 = 1\n")
|
||||
|
||||
from langbot.pkg.utils import importutil
|
||||
|
||||
with patch.object(importutil, "import_dir") as mock_import_dir:
|
||||
importutil.import_modules_in_pkg(mock_pkg)
|
||||
mock_import_dir.assert_called_once()
|
||||
call_path = mock_import_dir.call_args[0][0]
|
||||
assert call_path == str(tmp_path)
|
||||
|
||||
|
||||
class TestImportModulesInPkgs:
|
||||
"""Test import_modules_in_pkgs function."""
|
||||
|
||||
def test_imports_from_multiple_packages(self):
|
||||
"""Should call import_modules_in_pkg for each package."""
|
||||
from langbot.pkg.utils import importutil
|
||||
|
||||
mock_pkg1 = MagicMock()
|
||||
mock_pkg1.__file__ = "/path/to/pkg1/__init__.py"
|
||||
mock_pkg2 = MagicMock()
|
||||
mock_pkg2.__file__ = "/path/to/pkg2/__init__.py"
|
||||
|
||||
with patch.object(importutil, "import_modules_in_pkg") as mock_import:
|
||||
importutil.import_modules_in_pkgs([mock_pkg1, mock_pkg2])
|
||||
assert mock_import.call_count == 2
|
||||
|
||||
|
||||
class TestImportDotStyleDir:
|
||||
"""Test import_dot_style_dir function."""
|
||||
|
||||
def test_converts_dot_notation_to_path(self, tmp_path):
|
||||
"""Should convert dot notation to path and import."""
|
||||
# Create structure matching the dot notation
|
||||
(tmp_path / "my").mkdir()
|
||||
(tmp_path / "my" / "pkg").mkdir()
|
||||
(tmp_path / "my" / "pkg" / "test").mkdir()
|
||||
|
||||
from langbot.pkg.utils import importutil
|
||||
|
||||
with patch.object(importutil, "import_dir") as mock_import_dir:
|
||||
importutil.import_dot_style_dir("my.pkg.test")
|
||||
# The path should be converted using os.path.join
|
||||
call_path = mock_import_dir.call_args[0][0]
|
||||
# Should contain the path components joined
|
||||
assert "my" in call_path
|
||||
|
||||
|
||||
class TestReadResourceFile:
|
||||
"""Test read_resource_file function."""
|
||||
|
||||
def test_reads_resource_file_content(self):
|
||||
"""Should read content from a resource file."""
|
||||
from langbot.pkg.utils import importutil
|
||||
|
||||
try:
|
||||
content = importutil.read_resource_file("templates/config.yaml")
|
||||
assert isinstance(content, str)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def test_raises_for_nonexistent_file(self):
|
||||
"""Should raise exception for non-existent resource file."""
|
||||
from langbot.pkg.utils import importutil
|
||||
|
||||
with pytest.raises((FileNotFoundError, Exception)):
|
||||
importutil.read_resource_file("nonexistent/path/file.txt")
|
||||
|
||||
|
||||
class TestReadResourceFileBytes:
|
||||
"""Test read_resource_file_bytes function."""
|
||||
|
||||
def test_reads_resource_file_as_bytes(self):
|
||||
"""Should read content as bytes from a resource file."""
|
||||
from langbot.pkg.utils import importutil
|
||||
|
||||
try:
|
||||
content = importutil.read_resource_file_bytes("templates/config.yaml")
|
||||
assert isinstance(content, bytes)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def test_raises_for_nonexistent_file_bytes(self):
|
||||
"""Should raise exception for non-existent resource file."""
|
||||
from langbot.pkg.utils import importutil
|
||||
|
||||
with pytest.raises((FileNotFoundError, Exception)):
|
||||
importutil.read_resource_file_bytes("nonexistent/path/file.txt")
|
||||
|
||||
|
||||
class TestListResourceFiles:
|
||||
"""Test list_resource_files function."""
|
||||
|
||||
def test_lists_files_in_resource_directory(self):
|
||||
"""Should list files in a resource directory."""
|
||||
from langbot.pkg.utils import importutil
|
||||
|
||||
try:
|
||||
files = importutil.list_resource_files("templates")
|
||||
assert isinstance(files, list)
|
||||
for f in files:
|
||||
assert isinstance(f, str)
|
||||
except (FileNotFoundError, Exception):
|
||||
pass
|
||||
|
||||
def test_raises_for_nonexistent_directory(self):
|
||||
"""Should raise exception for non-existent directory."""
|
||||
from langbot.pkg.utils import importutil
|
||||
|
||||
with pytest.raises((FileNotFoundError, Exception)):
|
||||
importutil.list_resource_files("nonexistent_directory_xyz")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user