The venv ledger unions its recorded extras into every sync. When a source update removes an extra (hindsight, 73c598e319/285768fdfb), installs that recorded it pass `--extra hindsight` to `uv sync` forever and fail with "Extra `hindsight` is not defined in any project's optional-dependencies table". Every CLI launch then retries the failed source-update completion, and gateways keep booting the previous generation's code. Drop recorded extras the checkout's pyproject no longer declares, in both the sync target and the currency probe. An explicitly requested unknown extra still fails loudly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
134 lines
5.9 KiB
Python
134 lines
5.9 KiB
Python
"""Checkout freshness follows the selected PM generation, not a second stamp."""
|
|
import importlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
|
|
import hermes_yaml as yaml
|
|
|
|
from hermes_cli import venv_sync
|
|
from pm.environments import install_state_dir, selected_venv
|
|
from pm import paths
|
|
from pm.lock import Lockfile
|
|
from tests.pm.test_plugin_survival_contract import admission_env # noqa: F401
|
|
|
|
|
|
def test_check_uses_real_pm_selection_and_keeps_invalid_evidence(admission_env, monkeypatch, capsys):
|
|
monkeypatch.setattr("pm.client.is_runtime", lambda: True)
|
|
root, home = admission_env
|
|
core = root / 'core'
|
|
ensure = importlib.import_module('pm.install')
|
|
pin_path = root / 'pins.json'
|
|
pins = Lockfile(pin_path)
|
|
pins.set_pin('python', '1.0', {'any': {'url': 'https://example.invalid/python', 'sha256': 'a' * 64}})
|
|
pins.save()
|
|
monkeypatch.setattr(paths, 'lockfile_path', lambda: pin_path)
|
|
# A historical foreign-root stamp must not certify a PM environment.
|
|
old_stamp = core / ".hermes-runtime" / "cache" / "venv-sync.json"
|
|
old_stamp.parent.mkdir(parents=True)
|
|
old_stamp.write_text('{"lockDigest": "old-bootstrap-stamp"}')
|
|
cached = old_stamp.read_bytes()
|
|
|
|
def check(expected, code=0):
|
|
capsys.readouterr()
|
|
result = venv_sync.main(['--project-root', str(core), '--check', '--json'])
|
|
output = json.loads(capsys.readouterr().out)
|
|
assert result == code, output
|
|
assert output['state'] == expected, output
|
|
assert old_stamp.read_bytes() == cached
|
|
return output
|
|
|
|
check('would-sync')
|
|
ensure.sync_venv(explicit=True)
|
|
facts_path = paths.runtime_facts_path()
|
|
pristine = facts_path.read_bytes()
|
|
selected = selected_venv(core)
|
|
assert selected.is_relative_to(install_state_dir(core) / 'environments')
|
|
check('current')
|
|
assert not any(problem.startswith('venv:') for problem in ensure.check())
|
|
|
|
config = home / 'config.yaml'
|
|
old_config = config.read_bytes()
|
|
member = home / 'plugins' / 'extra-member'
|
|
member.mkdir(parents=True)
|
|
(member / 'plugin.yaml').write_text('name: extra-member\npython_dependencies: ["example-dep==1"]\n', encoding='utf-8')
|
|
config.write_text(yaml.safe_dump({'plugins': {'enabled': ['extra-member']}}), encoding='utf-8')
|
|
check('would-sync')
|
|
assert any(problem.startswith('venv:') for problem in ensure.check())
|
|
config.write_bytes(old_config)
|
|
check('current')
|
|
|
|
# A recorded extra only counts while the tree still declares it.
|
|
pyproject = core / 'pyproject.toml'
|
|
pyproject.write_text(pyproject.read_text(encoding='utf-8')
|
|
+ '[project.optional-dependencies]\nchanged-extra = []\n', encoding='utf-8')
|
|
altered = json.loads(pristine)
|
|
altered['packages']['venv']['extras'] = ['changed-extra']
|
|
facts_path.write_text(json.dumps(altered), encoding='utf-8')
|
|
check('would-sync')
|
|
assert any(problem.startswith('venv:') for problem in ensure.check())
|
|
facts_path.write_bytes(pristine)
|
|
|
|
pins.set_pin('python', '1.0', {'any': {'url': 'https://example.invalid/python', 'sha256': 'b' * 64}})
|
|
pins.save()
|
|
check('would-sync')
|
|
assert any(problem.startswith('venv:') for problem in ensure.check())
|
|
pins.set_pin('python', '1.0', {'any': {'url': 'https://example.invalid/python', 'sha256': 'a' * 64}})
|
|
pins.save()
|
|
|
|
marker = selected / 'pyvenv.cfg'
|
|
marker_bytes = marker.read_bytes()
|
|
marker.unlink()
|
|
check('would-sync')
|
|
assert any(problem.startswith('venv:') for problem in ensure.check())
|
|
marker.write_bytes(marker_bytes)
|
|
check('current')
|
|
|
|
outside = root / 'foreign-environment'
|
|
outside.mkdir()
|
|
(outside / 'pyvenv.cfg').write_bytes(marker_bytes)
|
|
altered = json.loads(pristine)
|
|
altered['packages']['venv']['environment'] = str(outside)
|
|
facts_path.write_text(json.dumps(altered), encoding='utf-8')
|
|
check('would-sync')
|
|
assert any(problem.startswith('venv:') for problem in ensure.check())
|
|
|
|
for invalid in (b'not JSON', b'{"schema":1,"packages":{"venv":[]}}'):
|
|
facts_path.write_bytes(invalid)
|
|
output = check('failed', 1)
|
|
assert output.get('detail')
|
|
assert any(problem.startswith('venv:') for problem in ensure.check())
|
|
assert facts_path.read_bytes() == invalid
|
|
assert not facts_path.with_suffix('.corrupt').exists()
|
|
facts_path.unlink()
|
|
check('would-sync')
|
|
assert not facts_path.exists()
|
|
|
|
|
|
def test_own_tree_sync_reuses_pm_without_writing_an_extra_stamp(admission_env, monkeypatch):
|
|
# Keep the public client seam; run its in-process path against admission's real engine.
|
|
monkeypatch.setattr("pm.client.is_runtime", lambda: True)
|
|
root, home = admission_env
|
|
core = root / 'core'
|
|
assert not (core / ".hermes-runtime" / "cache" / "venv-sync.json").exists()
|
|
assert venv_sync.sync(core) == {'state': 'synced', 'ok': True}
|
|
environment = selected_venv(core)
|
|
saved = paths.runtime_facts_path().read_bytes()
|
|
assert not (core / ".hermes-runtime" / "cache" / "venv-sync.json").exists()
|
|
assert venv_sync.sync(core) == {'state': 'current', 'ok': True}
|
|
assert paths.runtime_facts_path().read_bytes() == saved
|
|
python = environment / ('Scripts/python.exe' if os.name == 'nt' else 'bin/python')
|
|
result = subprocess.run([str(python), '-I', '-c', 'import sys; print(sys.prefix)'],
|
|
cwd=home, capture_output=True, text=True, check=True, timeout=30)
|
|
assert Path(result.stdout.strip()).resolve() == environment.resolve()
|
|
marker = environment / 'pyvenv.cfg'
|
|
marker_bytes = marker.read_bytes()
|
|
marker.unlink()
|
|
failure = venv_sync.sync(core)
|
|
assert failure['state'] == 'failed'
|
|
assert 'dependency environment is missing' in failure['detail']
|
|
marker.write_bytes(marker_bytes)
|
|
assert venv_sync.sync(core) == {'state': 'current', 'ok': True}
|
|
assert not (core / ".hermes-runtime" / "cache" / "venv-sync.json").exists()
|