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:
huanghuoguoguo
2026-05-09 18:40:40 +08:00
parent 9e1ff7f85c
commit 70ec75f9a2
52 changed files with 15990 additions and 6 deletions
+31
View File
@@ -62,6 +62,7 @@ def isolated_sys_modules(
- Modules in both mocks and clear will be mocked (not cleared)
- Original state is restored even if exception occurs
- Modules not in sys.modules before context are removed after
- Package attributes (e.g., my_pkg.submodule) are also saved/restored
"""
clear = clear or []
touched = set(mocks.keys()) | set(clear)
@@ -72,6 +73,14 @@ def isolated_sys_modules(
if name in sys.modules:
saved[name] = sys.modules[name]
# Save original package attributes that will be updated
saved_attrs: dict[str, tuple[str, object]] = {}
for mock_name, (pkg_name, attr_name) in _PACKAGE_ATTRIBUTE_UPDATES.items():
if mock_name in mocks and pkg_name in sys.modules:
pkg = sys.modules[pkg_name]
if hasattr(pkg, attr_name):
saved_attrs[mock_name] = (pkg_name, getattr(pkg, attr_name))
try:
# Clear modules first (force re-import)
for name in clear:
@@ -82,6 +91,13 @@ def isolated_sys_modules(
for name, module in mocks.items():
sys.modules[name] = module
# Update package attributes to point to mocks
# This is critical because `from package import submodule` gets the attribute,
# not sys.modules directly
for mock_name, (pkg_name, attr_name) in _PACKAGE_ATTRIBUTE_UPDATES.items():
if mock_name in mocks and pkg_name in sys.modules:
setattr(sys.modules[pkg_name], attr_name, mocks[mock_name])
yield
finally:
@@ -93,6 +109,11 @@ def isolated_sys_modules(
# Wasn't in sys.modules originally, remove it
sys.modules.pop(name, None)
# Restore package attributes
for mock_name, (pkg_name, original_value) in saved_attrs.items():
if pkg_name in sys.modules:
setattr(sys.modules[pkg_name], _PACKAGE_ATTRIBUTE_UPDATES[mock_name][1], original_value)
def make_pipeline_handler_import_mocks() -> dict[str, MagicMock]:
"""
@@ -141,6 +162,16 @@ def make_pipeline_handler_import_mocks() -> dict[str, MagicMock]:
}
# Package attributes that need to be updated alongside sys.modules mocking.
# When Python imports a submodule (e.g., langbot.pkg.provider.runner), it
# automatically sets an attribute on the parent package. The import statement
# `from ....provider import runner` gets this attribute, not sys.modules directly.
# This dict maps mock module names to the parent packages that need attribute updates.
_PACKAGE_ATTRIBUTE_UPDATES: dict[str, tuple[str, str]] = {
'langbot.pkg.provider.runner': ('langbot.pkg.provider', 'runner'),
}
def get_handler_modules_to_clear(handler_name: str) -> list[str]:
"""
Get list of handler-related modules to clear before import.