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
@@ -0,0 +1,137 @@
"""Tests for core bootutils dependency checking."""
from __future__ import annotations
import importlib.util
from unittest.mock import MagicMock, patch
from tests.utils.import_isolation import isolated_sys_modules
class TestCheckDeps:
"""Tests for check_deps function."""
def _make_deps_import_mocks(self):
"""Create mocks for deps import."""
return {
'langbot.pkg.utils.pkgmgr': MagicMock(),
}
def test_check_deps_all_present(self):
"""check_deps returns empty list when all deps present."""
mocks = self._make_deps_import_mocks()
with isolated_sys_modules(mocks):
# Mock find_spec to always return a spec (module found)
with patch.object(importlib.util, 'find_spec', return_value=MagicMock()):
from langbot.pkg.core.bootutils.deps import check_deps
import asyncio
result = asyncio.get_event_loop().run_until_complete(check_deps())
assert result == []
def test_check_deps_missing_deps(self):
"""check_deps returns list of missing deps."""
mocks = self._make_deps_import_mocks()
with isolated_sys_modules(mocks):
# Mock find_spec to return None for some deps
def mock_find_spec(name):
if name in ['requests', 'openai']:
return None # Missing
return MagicMock() # Present
with patch.object(importlib.util, 'find_spec', side_effect=mock_find_spec):
from langbot.pkg.core.bootutils.deps import check_deps
import asyncio
result = asyncio.get_event_loop().run_until_complete(check_deps())
assert 'requests' in result
assert 'openai' in result
def test_check_deps_all_missing(self):
"""check_deps returns all deps when none present."""
mocks = self._make_deps_import_mocks()
with isolated_sys_modules(mocks):
# Mock find_spec to always return None
with patch.object(importlib.util, 'find_spec', return_value=None):
from langbot.pkg.core.bootutils.deps import check_deps, required_deps
import asyncio
result = asyncio.get_event_loop().run_until_complete(check_deps())
# Should include all required_deps keys
assert len(result) == len(required_deps)
def test_required_deps_dict_exists(self):
"""required_deps dictionary is defined."""
mocks = self._make_deps_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.bootutils.deps import required_deps
assert isinstance(required_deps, dict)
assert len(required_deps) > 0
# Check some expected deps
assert 'requests' in required_deps
assert 'yaml' in required_deps
def test_required_deps_maps_import_name_to_package_name(self):
"""required_deps maps import name to package name."""
mocks = self._make_deps_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.bootutils.deps import required_deps
# Some import names differ from package names
assert required_deps['PIL'] == 'pillow'
assert required_deps['yaml'] == 'pyyaml'
assert required_deps['jwt'] == 'pyjwt'
class TestPrecheckPluginDeps:
"""Tests for precheck_plugin_deps function."""
def _make_deps_import_mocks(self):
return {
'langbot.pkg.utils.pkgmgr': MagicMock(),
}
def test_precheck_plugin_deps_no_plugins_dir(self):
"""precheck_plugin_deps skips when plugins dir doesn't exist."""
mocks = self._make_deps_import_mocks()
with isolated_sys_modules(mocks):
with patch('os.path.exists', return_value=False):
from langbot.pkg.core.bootutils.deps import precheck_plugin_deps
import asyncio
asyncio.get_event_loop().run_until_complete(precheck_plugin_deps())
# Should not raise, just skip
def test_precheck_plugin_deps_with_plugins_dir(self):
"""precheck_plugin_deps checks plugins subdirectories."""
mocks = self._make_deps_import_mocks()
mock_pkgmgr = MagicMock()
mocks['langbot.pkg.utils.pkgmgr'].install_requirements = mock_pkgmgr
with isolated_sys_modules(mocks):
from langbot.pkg.core.bootutils.deps import precheck_plugin_deps
# Mock os functions
with patch('os.path.exists', return_value=True):
with patch('os.listdir', return_value=['plugin1', 'plugin2']):
with patch('os.path.isdir', return_value=True):
# plugin1 has requirements.txt, plugin2 doesn't
def mock_listdir_subdir(path):
if 'plugin1' in path:
return ['requirements.txt', 'main.py']
return ['main.py']
with patch('os.listdir', side_effect=lambda p: mock_listdir_subdir(p) if 'plugin' in p else ['plugin1', 'plugin2']):
import asyncio
asyncio.get_event_loop().run_until_complete(precheck_plugin_deps())
+238
View File
@@ -0,0 +1,238 @@
"""Tests for core migration registration and abstract classes."""
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from tests.utils.import_isolation import isolated_sys_modules
class TestMigrationClassDecorator:
"""Tests for @migration_class decorator."""
def _make_migration_import_mocks(self):
"""Create mocks for migration import."""
return {
'langbot.pkg.core.app': MagicMock(),
}
def test_migration_class_registers_migration(self):
"""@migration_class registers migration in preregistered_migrations."""
mocks = self._make_migration_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.migration import migration_class, preregistered_migrations
# Clear for clean test
preregistered_migrations.clear()
@migration_class('test-migration', 1)
class TestMigration:
pass
assert len(preregistered_migrations) == 1
assert preregistered_migrations[0] == TestMigration
def test_migration_class_sets_name_attribute(self):
"""@migration_class sets name attribute on class."""
mocks = self._make_migration_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.migration import migration_class
@migration_class('test-migration', 1)
class TestMigration:
pass
assert TestMigration.name == 'test-migration'
def test_migration_class_sets_number_attribute(self):
"""@migration_class sets number attribute on class."""
mocks = self._make_migration_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.migration import migration_class
@migration_class('test-migration', 42)
class TestMigration:
pass
assert TestMigration.number == 42
def test_migration_class_returns_original_class(self):
"""@migration_class returns the original class."""
mocks = self._make_migration_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.migration import migration_class
@migration_class('test', 1)
class TestMigration:
custom_attr = 'value'
assert TestMigration.custom_attr == 'value'
def test_migration_class_multiple_migrations(self):
"""Multiple migrations can be registered."""
mocks = self._make_migration_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.migration import migration_class, preregistered_migrations
preregistered_migrations.clear()
@migration_class('migration1', 1)
class Migration1:
pass
@migration_class('migration2', 2)
class Migration2:
pass
assert len(preregistered_migrations) == 2
assert preregistered_migrations[0] == Migration1
assert preregistered_migrations[1] == Migration2
class TestMigrationAbstractClass:
"""Tests for Migration abstract class."""
def _make_migration_import_mocks(self):
return {'langbot.pkg.core.app': MagicMock()}
def test_migration_is_abstract(self):
"""Migration is abstract and cannot be instantiated directly."""
mocks = self._make_migration_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.migration import Migration
with pytest.raises(TypeError):
Migration(MagicMock())
def test_migration_requires_need_migrate_method(self):
"""Subclass must implement need_migrate method."""
mocks = self._make_migration_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.migration import Migration
class IncompleteMigration(Migration):
async def run(self):
pass
with pytest.raises(TypeError):
IncompleteMigration(MagicMock())
def test_migration_requires_run_method(self):
"""Subclass must implement run method."""
mocks = self._make_migration_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.migration import Migration
class IncompleteMigration(Migration):
async def need_migrate(self) -> bool:
return False
with pytest.raises(TypeError):
IncompleteMigration(MagicMock())
def test_migration_subclass_works(self):
"""Complete subclass can be instantiated."""
mocks = self._make_migration_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.migration import Migration
class CompleteMigration(Migration):
async def need_migrate(self) -> bool:
return True
async def run(self):
pass
mock_ap = MagicMock()
migration = CompleteMigration(mock_ap)
assert migration.ap == mock_ap
def test_migration_stores_app_reference(self):
"""Migration stores ap reference in __init__."""
mocks = self._make_migration_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.migration import Migration
class TestMigration(Migration):
async def need_migrate(self) -> bool:
return False
async def run(self):
pass
mock_ap = MagicMock()
migration = TestMigration(mock_ap)
assert migration.ap is mock_ap
@pytest.mark.asyncio
async def test_migration_need_migrate_returns_bool(self):
"""need_migrate must return bool."""
mocks = self._make_migration_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.migration import Migration
class TestMigration(Migration):
async def need_migrate(self) -> bool:
return True
async def run(self):
pass
migration = TestMigration(MagicMock())
result = await migration.need_migrate()
assert isinstance(result, bool)
assert result == True
class TestPreregisteredMigrations:
"""Tests for preregistered_migrations global registry."""
def _make_migration_import_mocks(self):
return {'langbot.pkg.core.app': MagicMock()}
def test_preregistered_migrations_is_list(self):
"""preregistered_migrations is a list."""
mocks = self._make_migration_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.migration import preregistered_migrations
assert isinstance(preregistered_migrations, list)
def test_preregistered_migrations_order(self):
"""Migrations are registered in order of decoration."""
mocks = self._make_migration_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.migration import migration_class, preregistered_migrations
preregistered_migrations.clear()
@migration_class('first', 1)
class First:
pass
@migration_class('second', 2)
class Second:
pass
@migration_class('third', 3)
class Third:
pass
# Order should match decoration order
assert preregistered_migrations[0].number == 1
assert preregistered_migrations[1].number == 2
assert preregistered_migrations[2].number == 3
+178
View File
@@ -0,0 +1,178 @@
"""Tests for core boot stage registration and abstract classes."""
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from tests.utils.import_isolation import isolated_sys_modules
class TestStageClassDecorator:
"""Tests for @stage_class decorator."""
def _make_stage_import_mocks(self):
"""Create mocks for stage import."""
return {
'langbot.pkg.core.app': MagicMock(),
}
def test_stage_class_registers_stage(self):
"""@stage_class registers stage in preregistered_stages."""
mocks = self._make_stage_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.stage import stage_class, preregistered_stages
# Clear for clean test
preregistered_stages.clear()
@stage_class('TestStage')
class TestStage:
pass
assert 'TestStage' in preregistered_stages
assert preregistered_stages['TestStage'] == TestStage
def test_stage_class_returns_original_class(self):
"""@stage_class returns the original class unchanged."""
mocks = self._make_stage_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.stage import stage_class
@stage_class('TestStage')
class TestStage:
value = 42
# Class attributes should be preserved
assert TestStage.value == 42
def test_stage_class_multiple_stages(self):
"""Multiple stages can be registered."""
mocks = self._make_stage_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.stage import stage_class, preregistered_stages
preregistered_stages.clear()
@stage_class('Stage1')
class Stage1:
pass
@stage_class('Stage2')
class Stage2:
pass
assert len(preregistered_stages) == 2
assert preregistered_stages['Stage1'] == Stage1
assert preregistered_stages['Stage2'] == Stage2
class TestBootingStageAbstract:
"""Tests for BootingStage abstract class."""
def _make_stage_import_mocks(self):
return {'langbot.pkg.core.app': MagicMock()}
def test_booting_stage_is_abstract(self):
"""BootingStage is abstract and cannot be instantiated directly."""
mocks = self._make_stage_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.stage import BootingStage
with pytest.raises(TypeError):
BootingStage()
def test_booting_stage_requires_run_method(self):
"""Subclass must implement run method."""
mocks = self._make_stage_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.stage import BootingStage
class IncompleteStage(BootingStage):
pass
with pytest.raises(TypeError):
IncompleteStage()
def test_booting_stage_subclass_works(self):
"""Complete subclass can be instantiated."""
mocks = self._make_stage_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.stage import BootingStage
class CompleteStage(BootingStage):
name = 'CompleteStage'
async def run(self, ap):
pass
stage = CompleteStage()
assert stage.name == 'CompleteStage'
def test_booting_stage_name_attribute(self):
"""BootingStage has name attribute (None by default in abstract)."""
mocks = self._make_stage_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.stage import BootingStage
# Abstract class has name attribute defined as None
assert hasattr(BootingStage, 'name')
@pytest.mark.asyncio
async def test_booting_stage_run_signature(self):
"""run method receives Application parameter."""
mocks = self._make_stage_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.stage import BootingStage
class TestStage(BootingStage):
name = 'TestStage'
async def run(self, ap):
self.ap_received = ap
stage = TestStage()
mock_ap = MagicMock()
await stage.run(mock_ap)
assert stage.ap_received == mock_ap
class TestPreregisteredStages:
"""Tests for preregistered_stages global registry."""
def _make_stage_import_mocks(self):
return {'langbot.pkg.core.app': MagicMock()}
def test_preregistered_stages_is_dict(self):
"""preregistered_stages is a dictionary."""
mocks = self._make_stage_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.stage import preregistered_stages
assert isinstance(preregistered_stages, dict)
def test_preregistered_stages_key_is_string(self):
"""Registry keys are stage names (strings)."""
mocks = self._make_stage_import_mocks()
with isolated_sys_modules(mocks):
from langbot.pkg.core.stage import stage_class, preregistered_stages
preregistered_stages.clear()
@stage_class('MyStage')
class MyStage:
pass
for key in preregistered_stages:
assert isinstance(key, str)