A failed lookup was reported as no change and returned success. Track failures where exceptions occur rather than parsing status text. Report independent successful lookups, then stop before any pin, install or dependency refresh if one lookup failed. Verification: the real PM CLI exercises check and apply with failed, mixed, current and manual-source results. Mutation boundaries stay unreached and the temporary lock stays unchanged. The focused gate passed 36 tests with no failures. Lint passed. No upstream package, real pin table or installed environment changed.
76 lines
2.8 KiB
Python
76 lines
2.8 KiB
Python
"""A failed update lookup is not a current result or permission to apply."""
|
|
import importlib
|
|
|
|
import pytest
|
|
|
|
from pm import cli, paths, registry
|
|
from pm.lock import Lockfile
|
|
from pm.package import Package
|
|
from pm.store import current_target
|
|
|
|
|
|
class UpdateFixture(Package):
|
|
def __init__(self, name, versions):
|
|
self.name = name
|
|
self.versions = versions
|
|
self.lookups = 0
|
|
|
|
def missing_reason(self, target):
|
|
return None if target == current_target() else "not a fixture target"
|
|
|
|
def latest_versions(self, target, locked=None):
|
|
self.lookups += 1
|
|
if isinstance(self.versions, Exception):
|
|
raise self.versions
|
|
return self.versions
|
|
|
|
|
|
def prepare(tmp_path, monkeypatch, packages):
|
|
lock = Lockfile(tmp_path / "lock.json")
|
|
for package in packages:
|
|
lock.set_pin(package.name, "1.0", {})
|
|
monkeypatch.setitem(registry._packages, package.name, package)
|
|
lock.save()
|
|
monkeypatch.setattr(paths, "lockfile_path", lambda: lock.path)
|
|
monkeypatch.setattr(paths, "repo_root", lambda: tmp_path)
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
|
|
monkeypatch.setenv("HERMES_RUNTIME_DIR", str(tmp_path / "tools"))
|
|
|
|
def forbidden(*args, **kwargs):
|
|
pytest.fail("lookup-only result reached a mutation or dependency refresh")
|
|
|
|
for name in ("_pin_artifacts", "_install_names", "_run_live", "pm_uv"):
|
|
monkeypatch.setattr(cli, name, forbidden)
|
|
monkeypatch.setattr(importlib.import_module("pm.ensure"), "sync_venv", forbidden)
|
|
return lock
|
|
|
|
|
|
@pytest.mark.parametrize("check", [True, False])
|
|
@pytest.mark.parametrize("mixed", [True, False])
|
|
def test_failed_resolution_stops_every_apply_path(tmp_path, monkeypatch, capsys, check, mixed):
|
|
failed = UpdateFixture("failed-lookup", TimeoutError("fixture index unavailable"))
|
|
healthy = UpdateFixture("healthy-lookup", ["2.0"])
|
|
packages = [failed, healthy] if mixed else [failed]
|
|
lock = prepare(tmp_path, monkeypatch, packages)
|
|
before = lock.path.read_bytes()
|
|
args = ["update", *[p.name for p in packages], "--uv", "--npm"]
|
|
if check:
|
|
args.append("--check")
|
|
assert cli.main(args) == 1
|
|
assert "fixture index unavailable" in capsys.readouterr().out
|
|
assert lock.path.read_bytes() == before
|
|
assert not (tmp_path / "tools").exists()
|
|
if mixed:
|
|
assert healthy.lookups == 1
|
|
|
|
|
|
@pytest.mark.parametrize("versions", [["1.0"], []])
|
|
def test_current_and_manual_results_are_successful_without_writes(tmp_path, monkeypatch, versions):
|
|
package = UpdateFixture("no-update", versions)
|
|
lock = prepare(tmp_path, monkeypatch, [package])
|
|
before = lock.path.read_bytes()
|
|
for flags in (["--check"], []):
|
|
assert cli.main(["update", package.name, *flags]) == 0
|
|
assert lock.path.read_bytes() == before
|
|
assert not (tmp_path / "tools").exists()
|