fix(plugins): preserve dependency manifest sources (#6370)

This commit is contained in:
InfinityPacer
2026-08-20 20:51:35 +08:00
committed by GitHub
parent 638ea6f01e
commit 2d80f9f04f
7 changed files with 189 additions and 61 deletions
+30 -4
View File
@@ -17,7 +17,7 @@ def test_build_env_maps_proxy_and_cache(tmp_path, monkeypatch):
monkeypatch.delenv("PACKAGE_CACHE_ROOT", raising=False)
monkeypatch.setenv("HTTP_PROXY", "http://old.example:8080")
request = PackageInstallRequest(
dependency_file=tmp_path / "requirements.txt",
dependency_files=(tmp_path / "requirements.txt",),
python_bin=Path("/venv/bin/python"),
config_dir=tmp_path / "config",
package_index_url="https://user:pass@mirror.example/simple",
@@ -38,7 +38,7 @@ def test_build_env_uses_package_cache_root_and_preserves_tool_cache_overrides(tm
monkeypatch.setenv("PACKAGE_CACHE_ROOT", str(tmp_path / "custom-package-cache"))
monkeypatch.delenv("UV_CACHE_DIR", raising=False)
request = PackageInstallRequest(
dependency_file=tmp_path / "requirements.txt",
dependency_files=(tmp_path / "requirements.txt",),
python_bin=Path("/venv/bin/python"),
config_dir=tmp_path / "config",
)
@@ -59,7 +59,7 @@ def test_build_strategies_prefers_uv_network_matrix_and_preserves_find_links(tmp
uv_bin.write_text("", encoding="utf-8")
request = PackageInstallRequest(
dependency_file=req,
dependency_files=(req,),
python_bin=tmp_path / "venv" / "bin" / "python",
find_links_dirs=[wheels],
config_dir=tmp_path / "config",
@@ -92,7 +92,7 @@ def test_build_strategies_fail_closed_when_uv_missing(tmp_path):
req = tmp_path / "requirements.txt"
req.write_text("demo\n", encoding="utf-8")
request = PackageInstallRequest(
dependency_file=req,
dependency_files=(req,),
python_bin=tmp_path / "venv" / "bin" / "python",
config_dir=tmp_path / "config",
)
@@ -103,6 +103,32 @@ def test_build_strategies_fail_closed_when_uv_missing(tmp_path):
assert strategies == []
def test_build_strategies_passes_all_manifests_to_one_uv_process(tmp_path):
"""多个插件清单必须进入同一个 uv 命令并保持输入顺序。"""
modern = tmp_path / "modern" / "pyproject.toml"
modern.parent.mkdir()
modern.write_text("[project]\nname='modern'\nversion='1'\n", encoding="utf-8")
legacy = tmp_path / "legacy" / "requirements.txt"
legacy.parent.mkdir()
legacy.write_text("demo\n", encoding="utf-8")
uv_bin = tmp_path / "venv" / "bin" / "uv"
uv_bin.parent.mkdir(parents=True)
uv_bin.write_text("", encoding="utf-8")
request = PackageInstallRequest(
dependency_files=(modern, legacy),
python_bin=tmp_path / "venv" / "bin" / "python",
)
strategies = build_package_install_strategies(request)
command = strategies[0].command
first_requirement = command.index("-r")
second_requirement = command.index("-r", first_requirement + 1)
assert command.count("-r") == 2
assert command[first_requirement + 1] == str(modern)
assert command[second_requirement + 1] == str(legacy)
def test_redact_url_removes_userinfo():
assert redact_url("https://user:pass@mirror.example/simple") == "https://mirror.example/simple"
+33 -18
View File
@@ -408,24 +408,38 @@ def test_load_dependency_file_accepts_custom_legacy_filename(tmp_path):
]
def test_install_uses_adapter_owned_temporary_requirements(tmp_path, monkeypatch):
"""批量依赖文件由依赖适配器创建并在安装返回后清理"""
helper = Mock()
installed_contents = []
def test_install_passes_all_active_manifests_to_one_install(tmp_path):
"""缺失依赖恢复必须保留 modern 与 legacy 清单的原始内容"""
plugin_root = tmp_path / "plugins"
modern_dir = _write_pyproject(
plugin_root,
"Alpha",
"""
[project]
name = "alpha"
version = "1.0.0"
dependencies = ["demo>=2"]
def _install_packages(dependency_file, _wheels_dirs):
installed_contents.append(dependency_file.read_text(encoding="utf-8"))
return True, "installed"
[[tool.uv.index]]
name = "private"
url = "https://packages.example/simple"
explicit = true
helper.install_packages_with_fallback.side_effect = _install_packages
monkeypatch.setattr(
"app.adapters.system.plugin.dependency.settings",
SimpleNamespace(ROOT_PATH=tmp_path, TEMP_PATH=tmp_path / "temp"),
[tool.uv.sources]
demo = { index = "private" }
""",
)
_write_requirements(
plugin_root,
"Beta",
"--extra-index-url https://legacy.example/simple\nother\n",
)
helper = Mock()
helper.install_packages_with_fallback.return_value = (True, "installed")
installer = PluginDependencyInstaller(
helper,
installed_plugins_provider=lambda: [],
plugin_dir=tmp_path / "plugins",
installed_plugins_provider=lambda: ["Alpha", "Beta"],
plugin_dir=plugin_root,
)
result = installer.install([
@@ -434,9 +448,10 @@ def test_install_uses_adapter_owned_temporary_requirements(tmp_path, monkeypatch
])
assert result == (True, "installed")
assert installed_contents == [
"demo[feature] @ https://example.com/demo.whl\nother\n"
manifest_paths = helper.install_packages_with_fallback.call_args.args[0]
assert manifest_paths == [
modern_dir / "pyproject.toml",
plugin_root / "beta" / "requirements.txt",
]
requirements_file = helper.install_packages_with_fallback.call_args.args[0]
assert requirements_file.name == "requirements.txt"
assert not requirements_file.exists()
assert "[tool.uv.sources]" in manifest_paths[0].read_text(encoding="utf-8")
assert "--extra-index-url" in manifest_paths[1].read_text(encoding="utf-8")
+69
View File
@@ -1062,6 +1062,75 @@ class TestPluginHelper:
assert env["HTTPS_PROXY"] == "http://proxy.example:7890"
assert "user:pass" not in " ".join(safe_command)
def test_uv_install_keeps_multiple_original_manifests_in_one_command(self):
"""批量恢复必须让 uv 直接读取每个插件的原始生效清单。"""
try:
from app.adapters.external.market import PluginHelper
except ModuleNotFoundError as exc:
pytest.skip(f"missing dependency: {exc}")
seen_commands = []
def fake_execute(command, env=None, safe_command=None):
seen_commands.append(command)
return True, "ok"
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
modern = root / "modern" / "pyproject.toml"
modern.parent.mkdir()
modern.write_text(
"""
[project]
name = "modern"
version = "1.0.0"
dependencies = ["demo>=2"]
[[tool.uv.index]]
name = "private"
url = "https://packages.example/simple"
explicit = true
[tool.uv.sources]
demo = { index = "private" }
""",
encoding="utf-8",
)
legacy = root / "legacy" / "requirements.txt"
legacy.parent.mkdir()
legacy.write_text(
"--extra-index-url https://legacy.example/simple\nother\n",
encoding="utf-8",
)
uv_bin = _create_fake_uv(root)
with patch("app.adapters.system.package.find_uv", return_value=uv_bin), \
patch.object(PluginHelper, "_PluginHelper__get_installed_packages", return_value={}), \
patch.object(PluginHelper, "_PluginHelper__get_protected_runtime_packages", return_value={}), \
patch.object(
PluginHelper,
"_PluginHelper__run_runtime_healthcheck",
return_value={"uv check": (True, "ok"), "核心依赖导入检查": (True, "ok")},
), \
patch("app.adapters.external.market.SystemUtils.execute_with_subprocess", side_effect=fake_execute):
success, message = PluginHelper.install_packages_with_fallback(
[modern, legacy]
)
assert success
assert message == "ok"
install_command = next(
command for command in seen_commands
if command[:3] == [str(uv_bin), "pip", "install"]
)
requirement_positions = [
index for index, value in enumerate(install_command) if value == "-r"
]
assert [install_command[index + 1] for index in requirement_positions] == [
str(modern),
str(legacy),
]
def test_uv_install_serializes_concurrent_calls(self):
"""
验证多个依赖安装请求会复用同一把锁串行执行 uv。