test: reconcile master's unit tests with feat/sandbox refactors

The merge from master brought in new unit tests that target pre-refactor
APIs on feat/sandbox. Reconcile each:

- factories/app.py: FakeApp now exposes a Mock skill_mgr (with empty .skills
  dict + inert prompt-addition builder) and a Mock pipeline_service so the
  PreProcessor skill-index injection branch can run end-to-end in tests.

- pipeline/conftest.py: eagerly import langbot.pkg.pipeline.pipelinemgr so
  pipeline.stage is fully initialised before any individual stage test
  (preproc, longtext, ...) tries to lazy-load it. Without this preload,
  running test_preproc.py in isolation hit a circular-import error via the
  stage -> app -> pipelinemgr -> stage chain.

- provider/test_tool_manager.py: ToolManager now probes four loaders
  (native -> plugin -> mcp -> skill). Inject inert native + skill mocks in
  the execute_func_call fixture and assert all four shutdowns fire.

- utils/test_paths.py: drop the three cwd-dependent _check_if_source_install
  cases. The refactor walks Path(__file__).resolve().parents looking for
  pyproject.toml + main.py, so cwd no longer factors in and there's no
  file read to mock-fail. The positive case and caching test still apply.

- utils/test_version.py: delete entirely. is_newer and compare_version_str
  were removed when VersionManager was refactored to use the Space API for
  release checks (1b4107a9); the tests targeted a surface that no longer
  exists.
This commit is contained in:
Junyan Qin
2026-05-21 00:04:34 +08:00
parent e65f851b2a
commit d1ddff9cdb
5 changed files with 128 additions and 285 deletions

View File

@@ -11,7 +11,6 @@ Uses tmp_path for file system isolation where applicable.
import os
import pytest
from unittest.mock import patch
class TestCheckIfSourceInstall:
@@ -19,7 +18,7 @@ class TestCheckIfSourceInstall:
def test_returns_true_for_source_install(self, tmp_path, monkeypatch):
"""Should return True when main.py with LangBot marker exists."""
main_py = tmp_path / "main.py"
main_py = tmp_path / 'main.py'
main_py.write_text('# LangBot/main.py\n# This is the entry point')
monkeypatch.chdir(tmp_path)
@@ -33,52 +32,14 @@ class TestCheckIfSourceInstall:
paths._is_source_install = None
def test_returns_false_when_no_main_py(self, tmp_path, monkeypatch):
"""Should return False when main.py doesn't exist."""
monkeypatch.chdir(tmp_path)
from langbot.pkg.utils import paths
paths._is_source_install = None
result = paths._check_if_source_install()
assert result is False
paths._is_source_install = None
def test_returns_false_when_main_py_without_marker(self, tmp_path, monkeypatch):
"""Should return False when main.py exists but lacks LangBot marker."""
main_py = tmp_path / "main.py"
main_py.write_text('# Some other project\nprint("hello")')
monkeypatch.chdir(tmp_path)
from langbot.pkg.utils import paths
paths._is_source_install = None
result = paths._check_if_source_install()
assert result is False
paths._is_source_install = None
def test_handles_io_error_gracefully(self, tmp_path, monkeypatch):
"""Should return False when main.py cannot be read."""
main_py = tmp_path / "main.py"
main_py.write_text('# LangBot/main.py\n')
monkeypatch.chdir(tmp_path)
from langbot.pkg.utils import paths
paths._is_source_install = None
# Patch open to raise IOError
with patch("builtins.open", side_effect=IOError("Cannot read")):
result = paths._check_if_source_install()
assert result is False
paths._is_source_install = None
# Note: ``_check_if_source_install`` was refactored to walk
# ``Path(__file__).resolve().parents`` looking for ``pyproject.toml`` +
# ``main.py`` instead of relying on the cwd. That makes it robust to where
# the process is launched from but also means the old "cwd doesn't have
# main.py" / "main.py without marker" / "IOError on read" cases no longer
# apply — there's no file read at all. The corresponding negative tests
# were removed; ``test_returns_true_for_source_install`` still exercises
# the positive path because the repo checkout itself is a source install.
class TestGetFrontendPath:
@@ -92,16 +53,16 @@ class TestGetFrontendPath:
result = paths.get_frontend_path()
# The result should contain web/dist or be an absolute path to it
assert "web/dist" in result or result.endswith("dist")
assert 'web/dist' in result or result.endswith('dist')
paths._is_source_install = None
def test_finds_dist_directory_in_source_mode(self, tmp_path, monkeypatch):
"""Should find web/dist when running from source mode."""
main_py = tmp_path / "main.py"
main_py = tmp_path / 'main.py'
main_py.write_text('# LangBot/main.py\n')
web_dist = tmp_path / "web" / "dist"
web_dist = tmp_path / 'web' / 'dist'
web_dist.mkdir(parents=True)
monkeypatch.chdir(tmp_path)
@@ -111,18 +72,18 @@ class TestGetFrontendPath:
paths._is_source_install = None
result = paths.get_frontend_path()
assert result == "web/dist"
assert result == 'web/dist'
paths._is_source_install = None
def test_prefers_dist_over_out_in_source_mode(self, tmp_path, monkeypatch):
"""Should prefer web/dist over web/out when both exist in source mode."""
main_py = tmp_path / "main.py"
main_py = tmp_path / 'main.py'
main_py.write_text('# LangBot/main.py\n')
web_dist = tmp_path / "web" / "dist"
web_dist = tmp_path / 'web' / 'dist'
web_dist.mkdir(parents=True)
web_out = tmp_path / "web" / "out"
web_out = tmp_path / 'web' / 'out'
web_out.mkdir(parents=True)
monkeypatch.chdir(tmp_path)
@@ -132,7 +93,7 @@ class TestGetFrontendPath:
paths._is_source_install = None
result = paths.get_frontend_path()
assert result == "web/dist"
assert result == 'web/dist'
paths._is_source_install = None
@@ -148,19 +109,19 @@ class TestGetResourcePath:
paths._is_source_install = None
result = paths.get_resource_path("nonexistent/file.txt")
assert result == "nonexistent/file.txt"
result = paths.get_resource_path('nonexistent/file.txt')
assert result == 'nonexistent/file.txt'
paths._is_source_install = None
def test_finds_resource_in_current_directory_source_mode(self, tmp_path, monkeypatch):
"""Should find resource in current directory when in source mode."""
main_py = tmp_path / "main.py"
main_py = tmp_path / 'main.py'
main_py.write_text('# LangBot/main.py\n')
resource_file = tmp_path / "templates" / "config.yaml"
resource_file = tmp_path / 'templates' / 'config.yaml'
resource_file.parent.mkdir(parents=True, exist_ok=True)
resource_file.write_text("test: value")
resource_file.write_text('test: value')
monkeypatch.chdir(tmp_path)
@@ -168,18 +129,18 @@ class TestGetResourcePath:
paths._is_source_install = None
result = paths.get_resource_path("templates/config.yaml")
result = paths.get_resource_path('templates/config.yaml')
assert os.path.exists(result)
paths._is_source_install = None
def test_returns_relative_path_in_source_mode(self, tmp_path, monkeypatch):
"""Should return relative path if resource exists in source mode."""
main_py = tmp_path / "main.py"
main_py = tmp_path / 'main.py'
main_py.write_text('# LangBot/main.py\n')
resource_file = tmp_path / "test_resource.txt"
resource_file.write_text("test content")
resource_file = tmp_path / 'test_resource.txt'
resource_file.write_text('test content')
monkeypatch.chdir(tmp_path)
@@ -187,8 +148,8 @@ class TestGetResourcePath:
paths._is_source_install = None
result = paths.get_resource_path("test_resource.txt")
assert result == "test_resource.txt"
result = paths.get_resource_path('test_resource.txt')
assert result == 'test_resource.txt'
paths._is_source_install = None
@@ -198,7 +159,7 @@ class TestPathFunctionsCaching:
def test_source_install_cache_is_used(self, tmp_path, monkeypatch):
"""_check_if_source_install should use cached result."""
main_py = tmp_path / "main.py"
main_py = tmp_path / 'main.py'
main_py.write_text('# LangBot/main.py\n')
monkeypatch.chdir(tmp_path)
@@ -219,5 +180,5 @@ class TestPathFunctionsCaching:
paths._is_source_install = None
if __name__ == "__main__":
pytest.main([__file__, "-v"])
if __name__ == '__main__':
pytest.main([__file__, '-v'])

View File

@@ -1,136 +0,0 @@
"""
Unit tests for version utility functions.
Tests version comparison logic without network calls.
"""
from __future__ import annotations
from unittest.mock import Mock
from langbot.pkg.utils.version import VersionManager
class TestVersionComparison:
"""Tests for version comparison functions."""
def _create_version_manager(self):
"""Create a VersionManager with mock app."""
mock_app = Mock()
mock_app.proxy_mgr = Mock()
mock_app.proxy_mgr.get_forward_providers = Mock(return_value={})
mock_app.logger = Mock()
return VersionManager(mock_app)
def test_is_newer_same_version(self):
"""is_newer returns False for same version."""
vm = self._create_version_manager()
result = vm.is_newer('v1.0.0', 'v1.0.0')
assert result is False
def test_is_newer_different_major_version(self):
"""is_newer returns False for different major version."""
# Note: is_newer ignores major version changes
vm = self._create_version_manager()
result = vm.is_newer('v2.0.0', 'v1.0.0')
assert result is False
def test_is_newer_minor_update(self):
"""is_newer returns True for minor update within same major."""
vm = self._create_version_manager()
result = vm.is_newer('v1.1.0', 'v1.0.0')
assert result is True
def test_is_newer_patch_update(self):
"""is_newer returns True for patch update within same major."""
vm = self._create_version_manager()
result = vm.is_newer('v1.0.1', 'v1.0.0')
assert result is True
def test_is_newer_with_fourth_segment(self):
"""is_newer ignores fourth version segment."""
# Both have same first 3 segments
vm = self._create_version_manager()
result = vm.is_newer('v1.0.0.1', 'v1.0.0.0')
assert result is False
def test_is_newer_short_version(self):
"""is_newer handles short version numbers."""
vm = self._create_version_manager()
result = vm.is_newer('v1.0', 'v1.0')
assert result is False
def test_is_newer_older_version(self):
"""is_newer returns True when new > old."""
vm = self._create_version_manager()
result = vm.is_newer('v1.2.0', 'v1.1.0')
assert result is True
class TestCompareVersionStr:
"""Tests for compare_version_str static method."""
def test_compare_equal_versions(self):
"""Equal versions return 0."""
result = VersionManager.compare_version_str('v1.0.0', 'v1.0.0')
assert result == 0
def test_compare_without_v_prefix(self):
"""Versions without v prefix work the same."""
result = VersionManager.compare_version_str('1.0.0', '1.0.0')
assert result == 0
def test_compare_mixed_prefix(self):
"""Mixed v prefix works correctly."""
result = VersionManager.compare_version_str('v1.0.0', '1.0.0')
assert result == 0
def test_compare_first_greater(self):
"""First version greater returns 1."""
result = VersionManager.compare_version_str('v1.1.0', 'v1.0.0')
assert result == 1
def test_compare_first_smaller(self):
"""First version smaller returns -1."""
result = VersionManager.compare_version_str('v1.0.0', 'v1.1.0')
assert result == -1
def test_compare_different_lengths(self):
"""Different length versions are padded with zeros."""
result = VersionManager.compare_version_str('v1.0', 'v1.0.0')
assert result == 0
def test_compare_shorter_greater(self):
"""Shorter version padded, first still greater."""
result = VersionManager.compare_version_str('v1.1', 'v1.0.0')
assert result == 1
def test_compare_longer_greater(self):
"""Longer version, first smaller."""
result = VersionManager.compare_version_str('v1.0', 'v1.0.1')
assert result == -1
def test_compare_major_version(self):
"""Major version comparison."""
result = VersionManager.compare_version_str('v2.0.0', 'v1.9.9')
assert result == 1
def test_compare_minor_version(self):
"""Minor version comparison."""
result = VersionManager.compare_version_str('v1.5.0', 'v1.4.9')
assert result == 1
def test_compare_patch_version(self):
"""Patch version comparison."""
result = VersionManager.compare_version_str('v1.0.1', 'v1.0.0')
assert result == 1
def test_compare_four_segments(self):
"""Four segment version comparison."""
result = VersionManager.compare_version_str('v1.0.0.1', 'v1.0.0.0')
assert result == 1
def test_compare_long_versions(self):
"""Long version strings work correctly."""
result = VersionManager.compare_version_str('v1.2.3.4.5', 'v1.2.3.4.4')
assert result == 1