Files
hermes-agent/tests/pm/test_workspace_build_inputs.py
teknium1 9fa34cfb28 fix(pm): keep bundled plugins' dashboard dist/ in the build snapshot
`dist` sat in the same every-depth exclusion set, so the generation
workspace lost the tracked `plugins/kanban/dashboard/dist/` and
`plugins/hermes-achievements/dashboard/dist/` bundles. The managed
environment's editable install runs from that snapshot, so
`get_bundled_plugins_dir()` resolves into it and
`dashboard_ui.py::serve_plugin_asset` 404s both tabs' entry bundles.

Keep `dist` excluded at the root (build output) and copy it below a
package root.

Refs #124075

Co-authored-by: Rafsanjani Castro Satria Chandra <330302617+oswaldvalois@users.noreply.github.com>
2026-09-27 02:54:35 -07:00

285 lines
14 KiB
Python

"""Workspace generation carries the source inputs of a buildable core."""
import json
from pathlib import Path
import shutil
import subprocess
import sys
import pytest
from pm import workspace
from tests.pm import _fixtures
from pm.environment import managed_environment
def _buildable_source(plugin):
(plugin / "build").mkdir(parents=True, exist_ok=True)
(plugin / "pyproject.toml").write_text(
'[project]\nname="replay-plugin"\nversion="1.0"\nrequires-python=">=3.11"\nreadme="README.md"\nlicense="MIT"\nlicense-files=["LICENSE"]\n'
'[build-system]\nrequires=[]\nbuild-backend="backend"\nbackend-path=["build"]\n',
encoding="utf-8",
)
(plugin / "plugin.yaml").write_text("name: replay-plugin\n", encoding="utf-8")
(plugin / "replay_plugin").mkdir()
(plugin / "replay_plugin/__init__.py").write_text("from .values import VALUE\n", encoding="utf-8")
(plugin / "replay_plugin/values.py").write_text("VALUE = 'recorded plugin bytes'\n", encoding="utf-8")
(plugin / "README.md").write_text("# fixture readme")
(plugin / "LICENSE").write_text("MIT")
# The backend must consume the copied metadata inputs too.
(plugin / "build/backend.py").write_text('''
from pathlib import Path
from zipfile import ZipFile
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
assert Path("README.md").read_text() == "# fixture readme"
assert Path("LICENSE").read_text() == "MIT"
name = "replay_plugin-1.0-py3-none-any.whl"
dist = "replay_plugin-1.0.dist-info"
entries = {
"replay_plugin/__init__.py": Path("replay_plugin/__init__.py").read_bytes(),
"replay_plugin/values.py": Path("replay_plugin/values.py").read_bytes(),
dist + "/METADATA": "Metadata-Version: 2.1\\nName: replay-plugin\\nVersion: 1.0\\n",
dist + "/WHEEL": "Wheel-Version: 1.0\\nRoot-Is-Purelib: true\\nTag: py3-none-any\\n",
}
entries[dist + "/RECORD"] = "".join(path + ",,\\n" for path in entries)
with ZipFile(Path(wheel_directory) / name, "w") as wheel:
for path, body in entries.items():
wheel.writestr(path, body)
return name
build_editable = build_wheel
''', encoding="utf-8")
def test_real_build_inputs_stay_in_generated_root(tmp_path, monkeypatch):
core = tmp_path / "core"
core.mkdir()
_buildable_source(core)
# Core snapshots exclude build/ output; the member replay below deliberately
# retains that backend-path. Keep the core backend at the source root.
(core / "build/backend.py").rename(core / "backend.py")
metadata = core / "pyproject.toml"
metadata.write_text(metadata.read_text().replace('backend-path=["build"]', 'backend-path=["."]'))
(core / ".env").write_text("must not copy")
monkeypatch.setattr(workspace.paths, "repo_root", lambda: core)
root, venv = tmp_path / "staging", tmp_path / "venv"
uv = shutil.which("uv")
assert uv is not None
monkeypatch.setattr("pm._uv._toolchain", lambda **kwargs: (Path(uv), Path(sys.executable)))
workspace.lock_and_sync([], [], root=root, source=core, seed_lock=None,
environment=managed_environment(venv))
python = venv / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python")
probe = subprocess.run([str(python), "-c", "import replay_plugin; print(replay_plugin.VALUE)"],
cwd=tmp_path, text=True, capture_output=True, check=True, timeout=30)
assert probe.stdout.strip() == "recorded plugin bytes"
assert not (root / ".env").exists()
assert not list(core.glob("*.egg-info")), "build must not write into the original core"
assert not (core / "uv.lock").exists()
def test_repair_replays_saved_in_tree_build_backend(tmp_path):
import os
import tomllib
from pm.environment import PythonEnvironment
core, plugin = tmp_path / "core", tmp_path / "plugin"
core.mkdir()
(plugin / "build").mkdir(parents=True)
(core / "pyproject.toml").write_text(
'[project]\nname="replay-core"\nversion="1"\nrequires-python=">=3.11"\n'
'[tool.uv]\npackage=false\nno-index=true\n', encoding="utf-8",
)
_buildable_source(plugin)
inputs = {p.relative_to(plugin): p.read_bytes() for p in plugin.rglob("*") if p.is_file()}
uv = shutil.which("uv")
assert uv, "saved backend replay test requires real uv"
saved, repaired = tmp_path / "saved", tmp_path / "repaired"
initial = PythonEnvironment(
uv=Path(uv), python=Path(sys.executable), destination=tmp_path / "initial-env",
cache=tmp_path / "initial-cache", env=dict(os.environ), offline=True,
)
workspace.lock_and_sync([plugin], [], root=saved, source=core, seed_lock=None,
environment=initial)
[relative] = tomllib.loads((saved / "pyproject.toml").read_text())["tool"]["uv"]["workspace"]["members"]
assert all((saved / relative / path).read_bytes() == data for path, data in inputs.items())
saved_lock = (saved / "uv.lock").read_bytes()
# Neither live manifests nor the live backend can provide repair's build inputs.
(core / "pyproject.toml").write_text("damaged [", encoding="utf-8")
(plugin / "pyproject.toml").write_text("damaged [", encoding="utf-8")
(plugin / "plugin.yaml").write_text("damaged [", encoding="utf-8")
(plugin / "build/backend.py").unlink()
(plugin / "replay_plugin/values.py").write_text("raise RuntimeError('damaged live source')\n", encoding="utf-8")
repair = PythonEnvironment(
uv=Path(uv), python=Path(sys.executable), destination=tmp_path / "repair-env",
# A fresh cache forces uv to invoke the saved backend again, not reuse a wheel.
cache=tmp_path / "repair-cache", env=dict(os.environ), offline=True,
)
workspace.lock_and_sync([plugin], [], root=repaired, source=core, seed_lock=None,
replay=saved, environment=repair)
assert (repaired / "uv.lock").read_bytes() == saved_lock
assert (saved / "uv.lock").read_bytes() == saved_lock
for root in (saved, repaired):
assert all((root / relative / path).read_bytes() == data for path, data in inputs.items())
for environment in (initial, repair):
probe = subprocess.run(
[str(environment.executable), "-I", "-c", "import replay_plugin; print(replay_plugin.VALUE)"],
cwd=tmp_path, text=True, capture_output=True, check=True, timeout=30,
)
assert probe.stdout.strip() == "recorded plugin bytes"
assert (core / "pyproject.toml").read_text() == "damaged ["
assert (plugin / "pyproject.toml").read_text() == "damaged ["
assert (plugin / "plugin.yaml").read_text() == "damaged ["
assert not (plugin / "build/backend.py").exists()
def test_source_refresh_does_not_need_metadata_change_and_refuses_live_root(tmp_path, monkeypatch):
core = tmp_path / "core"
core.mkdir()
(core / "pyproject.toml").write_text('[project]\nname="x"\nversion="1"\nrequires-python=">=3.14"\n[tool.uv]\npackage=false\nno-index=true\n')
(core / "code.py").write_text("VALUE = 1\n")
monkeypatch.setattr(workspace.paths, "repo_root", lambda: core)
staged = tmp_path / "staged"
uv = shutil.which("uv")
assert uv
monkeypatch.setattr("pm._uv._toolchain", lambda **kwargs: (Path(uv), Path(sys.executable)))
workspace.lock_and_sync([], [], root=staged, source=core, seed_lock=None,
environment=managed_environment(tmp_path / "env"))
(core / "code.py").write_text("VALUE = 2\n")
fresh = tmp_path / "fresh"
workspace.lock_and_sync([], [], root=fresh, source=core, seed_lock=staged / "uv.lock",
environment=managed_environment(tmp_path / "fresh-env"))
assert (staged / "code.py").read_text() == "VALUE = 1\n"
staged = fresh
assert (staged / "code.py").read_text() == "VALUE = 2\n"
before = (core / "code.py").read_bytes()
with pytest.raises(workspace.InstallError, match="fresh"):
workspace.lock_and_sync([], [], root=core, source=core, seed_lock=None,
environment=managed_environment(tmp_path / "env"))
assert (core / "code.py").read_bytes() == before
def test_legacy_member_is_generated_only_inside_workspace(tmp_path, monkeypatch):
import tomllib
core, plugin = tmp_path / "core", tmp_path / "readonly-plugin"
core.mkdir(); plugin.mkdir()
wheels = tmp_path / "wheels"
wheels.mkdir()
_fixtures._wheel(wheels, "example", "1.0")
(core / "pyproject.toml").write_text(
'[project]\nname="core"\nversion="1"\nrequires-python=">=3.14"\n'
'[tool.uv]\npackage=false\nno-index=true\n'
f'find-links=[{json.dumps(wheels.as_posix())}]\n')
manifest = plugin / "plugin.yaml"
manifest.write_text('name: legacy\npython_dependencies: ["example>=1,<2"]\n')
monkeypatch.setattr(workspace.paths, "repo_root", lambda: core)
stamp = workspace.members_stamp([plugin])
uv = shutil.which("uv")
assert uv
monkeypatch.setattr("pm._uv._toolchain", lambda **kwargs: (Path(uv), Path(sys.executable)))
generated = tmp_path / "stage"
workspace.lock_and_sync([plugin], [], root=generated, source=core, seed_lock=None,
environment=managed_environment(tmp_path / "env"))
metadata = tomllib.loads((generated / "pyproject.toml").read_text())
member = (generated / metadata["tool"]["uv"]["workspace"]["members"][0]).resolve()
assert member.is_relative_to(generated)
assert not (plugin / "pyproject.toml").exists()
assert tomllib.loads((member / "pyproject.toml").read_text())["project"]["dependencies"] == ["example>=1,<2"]
manifest.write_text('name: legacy\npython_dependencies: ["example>=2,<3"]\n')
assert workspace.members_stamp([plugin]) != stamp
@pytest.mark.parametrize("exact", [False, True])
def test_plugin_can_move_compatible_transitive_but_not_exact_requirement(tmp_path, monkeypatch, exact):
import os
import tomllib
core = tmp_path / "core"
core.mkdir()
wheels = tmp_path / "wheels"
wheels.mkdir()
_fixtures._wheel(wheels, "pkga", "1.0", ["pkgb>=1.2,<2"])
_fixtures._wheel(wheels, "pkgb", "1.2")
core_requirement = '["pkga==1.0", "pkgb==1.2"]' if exact else '["pkga==1.0"]'
(core / "pyproject.toml").write_text(
'[project]\nname="core-proof"\nversion="1"\nrequires-python=">=3.11"\n'
f'dependencies={core_requirement}\n[tool.uv]\npackage=false\nno-index=true\n'
f'find-links=[{json.dumps(wheels.as_posix())}]\n', encoding="utf-8",
)
uv = shutil.which("uv")
assert uv
monkeypatch.setattr(workspace.paths, "repo_root", lambda: core)
monkeypatch.setattr("pm._uv._toolchain", lambda **kwargs: (Path(uv), Path(sys.executable)))
baseline, first_env = tmp_path / "baseline", tmp_path / "first-env"
workspace.lock_and_sync([], [], root=baseline, source=core, seed_lock=None,
environment=managed_environment(first_env))
first_lock = (baseline / "uv.lock").read_bytes()
assert next(p["version"] for p in tomllib.loads(first_lock.decode())["package"] if p["name"] == "pkgb") == "1.2"
_fixtures._wheel(wheels, "pkgb", "1.3")
plugin = tmp_path / "plugin"
plugin.mkdir()
(plugin / "pyproject.toml").write_text(
'[project]\nname="plugin-proof"\nversion="1"\nrequires-python=">=3.11"\ndependencies=["pkgb==1.3"]\n',
encoding="utf-8",
)
extended, candidate = tmp_path / "extended", tmp_path / "candidate"
if exact:
with pytest.raises(workspace.ResolutionConflict):
workspace.lock_and_sync([plugin], [], root=extended, source=core, seed_lock=baseline / "uv.lock",
environment=managed_environment(candidate))
else:
workspace.lock_and_sync([plugin], [], root=extended, source=core, seed_lock=baseline / "uv.lock",
environment=managed_environment(candidate))
installed = tomllib.loads((extended / "uv.lock").read_text(encoding="utf-8"))["package"]
assert next(p["version"] for p in installed if p["name"] == "pkgb") == "1.3"
python = candidate / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
result = subprocess.run([str(python), "-c", "import pkga, pkgb; print(pkga.__version__, pkgb.__version__)"],
capture_output=True, text=True, check=True, timeout=30)
assert result.stdout.strip() == "1.0 1.3"
assert (baseline / "uv.lock").read_bytes() == first_lock
def test_member_uv_lock_travels_with_its_member(tmp_path):
"""pm/runtime.py::_inputs reads <workspace>/pm/uv.lock; the snapshot must carry it."""
core = tmp_path / "core"
member = core / "pm"
member.mkdir(parents=True)
(core / "pyproject.toml").write_text(
'[project]\nname="core"\nversion="1"\nrequires-python=">=3.11"\n'
'[tool.setuptools.packages.find]\ninclude=["pm"]\n',
encoding="utf-8",
)
(core / "uv.lock").write_text("version = 1\n# root lock\n", encoding="utf-8")
(member / "pyproject.toml").write_text('[project]\nname="pm"\nversion="1"\n', encoding="utf-8")
(member / "uv.lock").write_text("version = 1\n# member lock\n", encoding="utf-8")
destination = tmp_path / "stage"
workspace._copy_core_inputs(core, destination)
assert (destination / "pm/uv.lock").read_text(encoding="utf-8") == "version = 1\n# member lock\n"
assert not (destination / "uv.lock").exists(), "the root lock is seeded by lock_and_sync, not copied"
def test_nested_dist_travels_but_root_dist_stays_out(tmp_path):
"""Bundled dashboard plugins ship a tracked dashboard/dist/ the snapshot's code serves."""
core = tmp_path / "core"
bundle = core / "plugins" / "kanban" / "dashboard" / "dist"
bundle.mkdir(parents=True)
(core / "dist").mkdir()
(core / "pyproject.toml").write_text(
'[project]\nname="core"\nversion="1"\nrequires-python=">=3.11"\n'
'[tool.setuptools.packages.find]\ninclude=["*"]\n',
encoding="utf-8",
)
(bundle / "index.js").write_text("ENTRY\n", encoding="utf-8")
(core / "dist" / "core-1.whl").write_text("OUTPUT\n", encoding="utf-8")
destination = tmp_path / "stage"
workspace._copy_core_inputs(core, destination)
assert (destination / "plugins/kanban/dashboard/dist/index.js").read_text(encoding="utf-8") == "ENTRY\n"
assert not (destination / "dist").exists(), "root build output never enters the snapshot"