"""Plugin declaration safeguards from #113851, through PM's real resolver.""" from __future__ import annotations import json import os from pathlib import Path import shutil import subprocess import sys import tomllib import pytest from pm.environment import PythonEnvironment from pm.plugin_declarations import read_python_declaration, unsupported_requirements from pm.workspace import enabled_member_dirs, lock_and_sync from tests.pm import _fixtures @pytest.mark.parametrize("modern", [False, True]) def test_declaration_policy_survives_cross_profile_resolution(tmp_path, monkeypatch, modern): home = tmp_path / "custom-home" sibling = home / "profiles" / "work" monkeypatch.setattr(Path, "home", lambda: tmp_path) monkeypatch.setenv("HERMES_HOME", str(sibling)) monkeypatch.setenv("HERMES_RUNTIME_DIR", str(tmp_path / "runtime")) core = tmp_path / "core" core.mkdir() wheels = tmp_path / "wheels" wheels.mkdir() _fixtures._wheel(wheels, "fixturedep", "1.0") (core / "pyproject.toml").write_text( '[project]\nname="core"\nversion="1"\nrequires-python=">=3.11"\n' '[tool.uv]\npackage=false\nno-index=true\n' f'find-links=[{json.dumps(wheels.as_posix())}]\n', encoding="utf-8", ) plugin = home / "plugins" / "member" external = sibling / "plugins" / "sidecar" plugin.mkdir(parents=True) external.mkdir(parents=True) (home / "config.yaml").write_text("plugins:\n enabled: [member]\n", encoding="utf-8") (sibling / "config.yaml").write_text("memory:\n provider: sidecar\n", encoding="utf-8") (external / "plugin.yaml").write_text( 'name: sidecar\npython_runtime: external\npython_dependencies: ["impossible==99"]\n', encoding="utf-8", ) (external / "pyproject.toml").write_text( '[project]\nname="sidecar"\nversion="1"\ndependencies=["impossible==99"]\n', encoding="utf-8", ) specs = ['fixturedep>=1,<2', 'hermes-agent>=0.1,<1', 'remote @ https://example.invalid/unreviewed.whl', 'missing-other-python; python_version < "3.0"'] (plugin / "plugin.yaml").write_text( 'name: member\npython_dependencies: ' + json.dumps(["ignored==99"] if modern else specs) + '\n', encoding="utf-8", ) if modern: (plugin / "pyproject.toml").write_text( '[project]\nname="member"\nversion="1"\nrequires-python=">=3.11"\n' f'dependencies={json.dumps(specs)}\n[tool.uv]\npackage=false\n', encoding="utf-8", ) before = {p: p.read_bytes() for directory in (plugin, external) for p in directory.iterdir()} assert enabled_member_dirs() == [plugin] assert read_python_declaration(external).external declaration = read_python_declaration(plugin) assert declaration.requirements == tuple(specs) assert unsupported_requirements(declaration.requirements) == (specs[2],) uv = shutil.which("uv") assert uv is not None, "real resolver is required" environment = PythonEnvironment( uv=Path(uv), python=Path(sys.executable), destination=tmp_path / "env", cache=tmp_path / "cache", env=dict(os.environ), offline=True, ) root = tmp_path / "snapshot" lock_and_sync(enabled_member_dirs(), [], root=root, source=core, seed_lock=None, environment=environment) probe = subprocess.run( [str(environment.executable), "-I", "-c", "import fixturedep; print(fixturedep.__version__)"], check=True, capture_output=True, text=True, timeout=30, ) assert probe.stdout.strip() == "1.0" [relative] = tomllib.loads((root / "pyproject.toml").read_text())["tool"]["uv"]["workspace"]["members"] generated = tomllib.loads((root / relative / "pyproject.toml").read_text())["project"]["dependencies"] assert generated == [specs[0], specs[3]], "target markers must reach uv, unsafe/self requirements must not" assert all(p.read_bytes() == body for p, body in before.items()) @pytest.mark.parametrize("modern", [False, True]) def test_invalid_requirement_refused_at_declaration_boundary(tmp_path, modern): if modern: path = tmp_path / "pyproject.toml" path.write_text('[project]\ndependencies=["not a requirement >="]\n', encoding="utf-8") else: path = tmp_path / "plugin.yaml" path.write_text('python_dependencies: ["not a requirement >="]\n', encoding="utf-8") before = path.read_bytes() with pytest.raises(ValueError): read_python_declaration(tmp_path) assert path.read_bytes() == before