"""Source-update launch completion must publish a real PM generation.

Only tool acquisition is substituted: the isolated worker uses the test host's
uv/Python. Currency checks, resolution, installation, validation, facts and
selection all run through production PM. The shared worker fixture stages PM's
small locked dependency graph; the application project needs no downloads.
"""
from __future__ import annotations

import importlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys

import pytest

import pm
from hermes_cli import venv_sync
from pm.environments import install_state_dir, runtime_facts_path, selected_venv, site_packages
from pm import paths
from pm.lock import Facts
from pm.package import InstallError
from tests.pm._fixtures import isolated_python  # noqa: F401

# Spawns children with a home it builds itself; the parent's must stay real.
pytestmark = pytest.mark.real_machine_home


@pytest.fixture
def source_launch(tmp_path, monkeypatch, isolated_python):
    client = importlib.import_module("pm.client")
    engine = importlib.import_module("pm.install")
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.setattr(Path, "home", lambda: tmp_path)
    monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
    monkeypatch.setenv("HERMES_RUNTIME_DIR", str(tmp_path / "store"))
    monkeypatch.delenv("HERMES_DISABLE_LAZY_INSTALLS", raising=False)
    monkeypatch.setattr(paths, "lockfile_path", lambda: tmp_path / "tool-lock.json")

    uv = shutil.which("uv")
    assert uv, "source launch integration requires real uv"
    worker = Path(client.__file__).with_name("worker.py")
    worker_code = (
        "import runpy, sys\n"
        "from pathlib import Path\n"
        f"sys.path.insert(0, {str(worker.parent.parent)!r})\n"
        "import pm._uv\n"
        f"pm._uv._toolchain = lambda **kwargs: (Path({uv!r}), Path({sys.executable!r}))\n"
        f"runpy.run_path({str(worker)!r}, run_name='__main__')\n"
    )
    command = [str(isolated_python), "-I", "-B", "-c", worker_code]
    monkeypatch.setattr(client, "runtime_command", lambda *args, **kwargs: command)
    monkeypatch.setattr(engine, "sync_venv", lambda *args, **kwargs: pytest.fail("sync escaped worker isolation"))

    root = tmp_path / "source with spaces"
    root.mkdir()
    (root / ".git").mkdir()
    (root / "install-stamp.json").write_text(
        json.dumps({"updateMechanism": "self"}), encoding="utf-8",
    )
    # The startup heal hands the shared completion tail (launchers, products,
    # maintenance) to the checkout's own hermes_cli/source_completion.py. This
    # source slice has no products; record the hand-off instead of running it.
    (root / "hermes_cli").mkdir()
    (root / "hermes_cli" / "source_completion.py").write_text(
        "import json, sys\n"
        f"open({str(tmp_path / 'completion-calls')!r}, 'a').write(json.dumps(sys.argv[1:]) + '\\n')\n",
        encoding="utf-8",
    )
    (root / "pyproject.toml").write_text(
        '[project]\nname = "launch-proof"\nversion = "1"\nrequires-python = ">=3.11"\n'
        '[project.optional-dependencies]\nall = []\nlaunch-extra = []\n'
        '[tool.uv]\npackage = false\nno-index = true\noffline = true\n', encoding="utf-8",
    )
    pm.lock_project(root, offline=True, explicit=True)

    # Acquisition uses the real host interpreter; publish its installed fact
    # through PM too. Unrecorded store bytes are deliberately not launchable.
    from pm.store import current_target, tree_digest

    store = paths.store_root()
    entry = store / "python-test"
    store_python = entry / "bin" / "python3"
    store_python.parent.mkdir(parents=True)
    # A Nix wrapper resets sys.executable; use the underlying binary instead.
    store_python.symlink_to(sys._base_executable)
    Facts(paths.facts_path()).record(
        "python", "test", entry.name, {"PATH": [str(store_python.parent)]}, store,
        target=current_target(), digest=tree_digest(entry),
    )
    return root, store_python, command


@pytest.mark.platforms("posix")
@pytest.mark.parametrize("update", ["launch", "sync", "pm-update"])
def test_source_python_pin_update_survives_real_gc(source_launch, tmp_path, monkeypatch, update):
    from hermes_cli import _launchers
    from pm.cli import cmd_gc
    from pm.lock import Lockfile
    from pm.store import current_target, tree_digest
    from tests.hermes_cli.test_source_launcher_publication import BOOT_FILES

    root, old_python, _ = source_launch
    repository = Path(__file__).resolve().parents[2]
    for relative in BOOT_FILES:
        destination = root / relative
        destination.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(repository / relative, destination)
    (root / "source_probe.py").write_text(
        "import json, sys, selected_probe\n"
        "print(json.dumps({'executable': sys.executable, 'value': selected_probe.VALUE}))\n",
        encoding="utf-8",
    )
    pin = Lockfile(paths.lockfile_path())
    pin.set_pin("python", "A", {})
    pin.save()
    pm.sync_venv(explicit=True, project_root=root)
    # Initial installation is allowed to publish. No explicit writer is called
    # after replacement: the source-update owner must refresh this same command.
    _launchers.ensure_install_launchers(root, root / ".hermes" / "bin")
    command = _launchers.installation_command(root, module="source_probe")
    (site_packages(selected_venv(root)) / "selected_probe.py").write_text("VALUE = 'A'\n")
    child_env = {**os.environ, "HERMES_DISABLE_LAZY_INSTALLS": "1"}
    before = subprocess.run(command, env=child_env, capture_output=True, text=True, timeout=30)
    assert before.returncode == 0, before.stderr
    assert json.loads(before.stdout)["executable"] == str(old_python)

    # Only acquisition is substituted. This is a new real interpreter entry,
    # a changed tool pin and a real Facts publication, not a launcher rewrite.
    store = paths.store_root()
    new_python = store / "python-B" / "bin" / "python3"
    new_python.parent.mkdir(parents=True)
    new_python.symlink_to(sys._base_executable)
    Facts(paths.facts_path()).record(
        "python", "B", "python-B", {"PATH": [str(new_python.parent)]}, store,
        target=current_target(), digest=tree_digest(new_python.parent.parent),
    )
    if update != "pm-update":
        pin.set_pin("python", "B", {})
        pin.save()
        assert not pm.venv_is_current(project_root=root)
    previous = selected_venv(root)
    if update == "launch":
        assert venv_sync.prepare_launch(root, []) == new_python
        # The heal finished the whole tail, with update wording, through the checkout's own completion.
        calls = [json.loads(line) for line in (tmp_path / "completion-calls").read_text().splitlines()]
        assert calls == [["--source", str(root), "--finish-update"]]
    elif update == "sync":
        # This is the PM sync -> launcher publication sequence now split across
        # update_completion._prepare and _complete_selected. The former checkout
        # case adds no distinct PM/GC coverage; the fresh-interpreter handoff is
        # exercised separately in test_update_completion_process.py.
        assert venv_sync.sync(root) == {"state": "synced", "ok": True}
    else:
        from types import SimpleNamespace
        from pm import cli
        from pm.update import Resolved

        monkeypatch.setattr(paths, "repo_root", lambda: root)
        monkeypatch.setattr(cli, "repo_root", lambda: root)
        # The latest release and artifact acquisition are fixture inputs; the
        # CLI must still pin, ensure, synchronize and publish through its owners.
        monkeypatch.setattr(cli, "resolve_package", lambda *a, **k: Resolved("python", "A", "semver", "B"))
        monkeypatch.setattr(cli, "_pin_artifacts", lambda *a: {})
        monkeypatch.setattr(importlib.import_module("pm.install"), "sync_venv", pm.sync_venv)
        assert cli.cmd_update(SimpleNamespace(names=["python"], target=None, check=False, uv=False, npm=False, termux=False)) == 0
        assert Lockfile(paths.lockfile_path()).version("python") == "B"
    assert pm.venv_is_current(project_root=root)
    assert selected_venv(root) != previous
    (site_packages(selected_venv(root)) / "selected_probe.py").write_text("VALUE = 'B'\n")
    monkeypatch.setattr(paths, "repo_root", lambda: root)
    assert cmd_gc(None) == 0
    assert not old_python.exists(), "real PM GC did not remove the superseded Python"
    assert new_python.is_file()
    after = subprocess.run(command, env=child_env, capture_output=True, text=True, timeout=30)
    assert after.returncode == 0, after.stderr
    assert json.loads(after.stdout) == {"executable": str(new_python), "value": "B"}


@pytest.mark.platforms("posix")
def test_launcher_publication_failure_retries_without_rebuilding_dependencies(source_launch, monkeypatch):
    from hermes_cli import _launchers

    root, store_python, _ = source_launch
    with monkeypatch.context() as failed_publication:
        failed_publication.setattr(_launchers, "ensure_install_launchers", lambda *a: [])
        result = venv_sync.sync(root)
    assert not result["ok"] and "launcher publication failed" in result["detail"]
    assert pm.venv_is_current(project_root=root)
    committed = runtime_facts_path(root).read_bytes()
    assert venv_sync.prepare_launch(root, []) == store_python
    assert runtime_facts_path(root).read_bytes() == committed
    assert (root / ".hermes" / "bin" / "hermes").is_file()


@pytest.mark.platforms("posix")
@pytest.mark.parametrize("checkout", [False, True])
def test_source_publication_leaves_external_install_launchers_alone(source_launch, checkout):
    root, _, _ = source_launch
    (root / "install-stamp.json").write_text(
        json.dumps({"updateMechanism": "external", "distribution": "nix"}), encoding="utf-8",
    )
    if not checkout:
        (root / ".git").rmdir()
    launcher = root / ".hermes" / "bin" / "hermes"
    launcher.parent.mkdir(parents=True)
    launcher.write_text("externally owned launcher\n", encoding="utf-8")
    assert venv_sync.sync(root)["ok"]
    assert launcher.read_text(encoding="utf-8") == "externally owned launcher\n"


def _fact(root):
    fact = Facts(runtime_facts_path(root), strict=True).get("venv")
    assert fact is not None
    selected = selected_venv(root)
    assert Path(fact["environment"]) == selected
    assert selected.is_relative_to(install_state_dir(root) / "environments")
    assert (selected / "pyvenv.cfg").is_file()
    assert Path(fact["resolved_lock"]).is_file()
    probe = subprocess.run(
        [str(selected / "bin" / "python"), "-I", "-c", "import json,sys; print(json.dumps(sys.prefix))"],
        capture_output=True, text=True, timeout=30,
    )
    assert probe.returncode == 0, probe.stderr
    assert Path(json.loads(probe.stdout)) == selected
    return fact


def _receipts(tmp_path):
    return set((tmp_path / "home" / "logs" / "update_receipts").glob("pm_*.json"))


@pytest.mark.platforms("posix")
def test_launch_without_marker_publishes_then_skips_and_rebuilds_on_lock_change(source_launch, tmp_path):
    root, store_python, _ = source_launch
    assert not runtime_facts_path(root).exists()
    assert not (root / ".update-incomplete").exists()
    assert not pm.venv_is_current(project_root=root)

    assert venv_sync.prepare_launch(root, []) == store_python
    first = _fact(root)
    assert first["extras"] == ["all"]
    assert pm.venv_is_current(project_root=root)
    facts_bytes = runtime_facts_path(root).read_bytes()
    generations = set((install_state_dir(root) / "environments").iterdir())
    receipts = _receipts(tmp_path)
    assert receipts

    # A caller still in its old interpreter must re-exec, but must not sync again.
    assert venv_sync.prepare_launch(root, []) == store_python
    assert runtime_facts_path(root).read_bytes() == facts_bytes
    assert set((install_state_dir(root) / "environments").iterdir()) == generations
    assert _receipts(tmp_path) == receipts, "current launch performed another sync"

    lock = root / "uv.lock"
    lock.write_bytes(lock.read_bytes() + b"\n# source update changes the committed lock\n")
    assert not pm.venv_is_current(project_root=root)
    assert venv_sync.prepare_launch(root, []) == store_python
    rebuilt = _fact(root)
    assert rebuilt["stamp"] != first["stamp"]
    assert rebuilt["environment"] != first["environment"]
    assert rebuilt["extras"] == first["extras"]
    assert Path(first["environment"]).is_dir()
    assert pm.venv_is_current(project_root=root)
    assert not (root / ".update-incomplete").exists()


@pytest.mark.platforms("posix")
def test_process_spawned_by_the_update_commits_dependencies_but_not_the_tail(source_launch, tmp_path):
    """A process an update spawns before its dependencies are current (its restarted gateway)
    must not boot on a tree built for another interpreter; it syncs, but leaves the tail alone."""
    import time
    from hermes_cli.update_lock import update_marker_path
    from pm.environments import committed_venv

    root, store_python, _ = source_launch
    marker = update_marker_path()
    marker.parent.mkdir(parents=True, exist_ok=True)
    marker.write_text(f"{os.getppid()}\n{int(time.time())}\n", encoding="utf-8")  # the updater is our ancestor
    assert committed_venv(root) is None

    assert venv_sync.prepare_launch(root, []) == store_python
    assert committed_venv(root) == Path(_fact(root)["environment"])
    assert not (tmp_path / "completion-calls").exists(), "the tail is the updater's, not its child's"
    assert not venv_sync.completion_pending_path(root).exists()

    facts_bytes, receipts = runtime_facts_path(root).read_bytes(), _receipts(tmp_path)
    venv_sync.prepare_launch(root, [])
    assert runtime_facts_path(root).read_bytes() == facts_bytes
    assert _receipts(tmp_path) == receipts, "a committed child synced again under the updater's claim"


@pytest.mark.platforms("posix")
def test_failed_real_sync_preserves_previous_selection_and_retries(source_launch, tmp_path):
    root, store_python, _ = source_launch
    # Established PM installs must retain their selection, not gain legacy [all].
    pm.sync_venv(["launch-extra"], explicit=True, project_root=root)
    previous = _fact(root)
    facts_bytes = runtime_facts_path(root).read_bytes()
    generations = set((install_state_dir(root) / "environments").iterdir())
    lock = root / "uv.lock"
    valid_lock = lock.read_bytes()
    lock.write_text("version = 1\nnot valid TOML\n", encoding="utf-8")
    markers = [root / name for name in (".update-incomplete", ".lazy-refresh-incomplete")]
    for marker in markers:
        marker.write_text("legacy pending install", encoding="utf-8")

    for _ in range(2):
        receipts = _receipts(tmp_path)
        with pytest.raises(InstallError, match="uv sync exited"):
            venv_sync.prepare_launch(root, [])
        failed, = _receipts(tmp_path) - receipts
        assert json.loads(failed.read_text(encoding="utf-8"))["outcome"] == "failed"
        assert runtime_facts_path(root).read_bytes() == facts_bytes
        assert _fact(root) == previous
        assert set((install_state_dir(root) / "environments").iterdir()) == generations
        assert not pm.venv_is_current(project_root=root)
        assert all(marker.read_text(encoding="utf-8") == "legacy pending install" for marker in markers)

    lock.write_bytes(valid_lock + b"\n# corrected source update\n")
    assert venv_sync.prepare_launch(root, []) == store_python
    rebuilt = _fact(root)
    assert rebuilt["environment"] != previous["environment"]
    assert rebuilt["extras"] == ["launch-extra"]
    assert pm.venv_is_current(project_root=root)
    assert not any(marker.exists() for marker in markers)


@pytest.mark.platforms("posix")
def test_source_update_that_removes_a_recorded_extra_still_syncs(source_launch):
    """A dropped extra (hindsight, 73c598e319) must not brick every later sync."""
    root, store_python, _ = source_launch
    pm.sync_venv(["all", "launch-extra"], explicit=True, project_root=root)
    assert _fact(root)["extras"] == ["all", "launch-extra"]

    pyproject = root / "pyproject.toml"
    pyproject.write_text(pyproject.read_text(encoding="utf-8").replace("launch-extra = []\n", ""),
                         encoding="utf-8")
    pm.lock_project(root, offline=True, explicit=True)
    assert not pm.venv_is_current(project_root=root)

    assert venv_sync.prepare_launch(root, []) == store_python
    assert _fact(root)["extras"] == ["all"]
    assert pm.venv_is_current(project_root=root)


@pytest.mark.platforms("posix")
def test_recorded_extra_spelled_differently_from_its_declaration_survives(source_launch):
    """uv matches extras by PEP 685 name; `launch_extra` is the declared `launch-extra`."""
    root, store_python, _ = source_launch
    pm.sync_venv(["all", "launch_extra"], explicit=True, project_root=root)
    assert _fact(root)["extras"] == ["all", "launch_extra"]

    lock = root / "uv.lock"
    lock.write_bytes(lock.read_bytes() + b"\n# source update changes the committed lock\n")
    assert venv_sync.prepare_launch(root, []) == store_python
    assert _fact(root)["extras"] == ["all", "launch_extra"]
    assert pm.venv_is_current(project_root=root)


@pytest.mark.platforms("posix")
@pytest.mark.parametrize("mode", ["script", "module", "command"])
def test_real_bootstrap_reexecs_before_app_imports(source_launch, tmp_path, isolated_python, mode):
    root, store_python, worker_command = source_launch
    repository = Path(__file__).resolve().parents[2]
    # Copy the real bootstrap so it owns this disposable source install. The
    # other modules remain real checkout imports; only acquisition is injected.
    shutil.copy2(repository / "hermes_bootstrap.py", root / "hermes_bootstrap.py")
    (root / "launch_test_tools.py").write_text(
        "import sys\n"
        "from pathlib import Path\n"
        f"sys.path.insert(1, {str(repository)!r})\n"
        "import pm.client\n"
        "from pm import paths\n"
        f"paths.lockfile_path = lambda: Path({str(tmp_path / 'tool-lock.json')!r})\n"
        f"pm.client.runtime_command = lambda *args, **kwargs: {worker_command!r}\n",
        encoding="utf-8",
    )
    entry = root / "launch_probe.py"
    entry.write_text(
        "import launch_test_tools\n"
        "import hermes_bootstrap\n"
        "import json, sys\n"
        "from pathlib import Path\n"
        "from pm.environments import selected_venv, site_packages\n"
        "from hermes_cli.venv_sync import prepare_launch\n"
        "root = Path(__file__).parent\n"
        "selected = selected_venv(root)\n"
        "print(json.dumps({'executable': sys.executable, 'args': sys.argv[1:],\n"
        "    'site': str(site_packages(selected)), 'path': sys.path,\n"
        "    'module': getattr(__spec__, 'name', None),\n"
        "    'launch_current': prepare_launch(root, sys.argv[1:]) is None}))\n",
        encoding="utf-8",
    )
    args = ["--profile", "name with spaces", "-c", "session"]
    invocation = {
        "script": [str(entry)],
        "module": ["-m", "launch_probe"],
        "command": ["-c", "import launch_probe"],
    }[mode]
    assert not runtime_facts_path(root).exists()
    activated_old = tmp_path / "activated-old-dependencies"
    if mode == "script":
        pm.sync_venv(["all"], explicit=True, project_root=root)
        old_site = site_packages(selected_venv(root))
        # Executable .pth files are real activation hooks. If bootstrap selects
        # the old generation before completing the update, this leaves evidence.
        (old_site / "old_dependency.pth").write_text(
            f"import pathlib; pathlib.Path({str(activated_old)!r}).touch()\n", encoding="utf-8",
        )
        activation_probe = subprocess.run(
            [str(isolated_python), "-I", "-c",
             f"import sys; sys.path.insert(0, {str(repository)!r}); "
             "from pathlib import Path; from pm.environments import activate_dependencies; "
             f"activate_dependencies(Path({str(root)!r}))"],
            capture_output=True, text=True, timeout=30,
        )
        assert activation_probe.returncode == 0, activation_probe.stderr
        assert activated_old.is_file(), "positive control did not execute the old activation hook"
        activated_old.unlink()
        lock = root / "uv.lock"
        lock.write_bytes(lock.read_bytes() + b"\n# update before activation\n")
    if mode == "module":
        # These leftovers must not trigger an immediate second (repair) sync
        # after launch completion, which would validate the full app graph.
        for name in (".update-incomplete", ".lazy-refresh-incomplete"):
            (root / name).write_text("legacy pending install", encoding="utf-8")
    before_receipts = _receipts(tmp_path)
    result = subprocess.run(
        [str(isolated_python), *invocation, *args], cwd=root,
        env=dict(os.environ), capture_output=True, text=True, timeout=60,
    )
    assert result.returncode == 0, result.stderr
    output = json.loads(result.stdout)
    assert output["executable"] == str(store_python)
    assert output["args"] == args
    assert output["launch_current"] is True
    assert output["module"] == (None if mode == "script" else "launch_probe")
    selected = selected_venv(root)
    assert Path(output["site"]).is_relative_to(selected)
    assert output["site"] in output["path"]
    assert not activated_old.exists(), "old dependencies activated before source-update completion"
    assert _fact(root)["extras"] == ["all"]
    receipt, = _receipts(tmp_path) - before_receipts
    assert json.loads(receipt.read_text(encoding="utf-8"))["outcome"] == "ok"
    assert not (root / ".update-incomplete").exists()
    assert not (root / ".lazy-refresh-incomplete").exists()


@pytest.mark.platforms("posix")
@pytest.mark.parametrize("argv", [[], ["--version"]])
def test_failed_launch_completion_degrades_to_a_warning(source_launch, tmp_path, isolated_python, argv):
    """An update whose dependency sync cannot finish (offline, bad lock) must leave a usable
    CLI on the previous generation with a warning — and a metadata query must not even try."""
    root, store_python, worker_command = source_launch
    repository = Path(__file__).resolve().parents[2]
    shutil.copy2(repository / "hermes_bootstrap.py", root / "hermes_bootstrap.py")
    (root / "launch_test_tools.py").write_text(
        "import sys\n"
        "from pathlib import Path\n"
        f"sys.path.insert(1, {str(repository)!r})\n"
        "import pm.client\n"
        "from pm import paths\n"
        f"paths.lockfile_path = lambda: Path({str(tmp_path / 'tool-lock.json')!r})\n"
        f"pm.client.runtime_command = lambda *args, **kwargs: {worker_command!r}\n",
        encoding="utf-8",
    )
    entry = root / "launch_probe.py"
    entry.write_text(
        "import launch_test_tools\n"
        "import hermes_bootstrap\n"
        "import json, sys\n"
        "print(json.dumps({'executable': sys.executable, 'args': sys.argv[1:]}))\n",
        encoding="utf-8",
    )
    pm.sync_venv(["all"], explicit=True, project_root=root)
    previous = _fact(root)
    lock = root / "uv.lock"
    lock.write_text("version = 1\nnot valid TOML\n", encoding="utf-8")
    assert not pm.venv_is_current(project_root=root)
    before_receipts = _receipts(tmp_path)

    result = subprocess.run(
        [str(isolated_python), str(entry), *argv], cwd=root,
        env=dict(os.environ), capture_output=True, text=True, timeout=60,
    )

    assert result.returncode == 0, result.stderr
    assert json.loads(result.stdout) == {"executable": str(isolated_python), "args": argv}
    assert _fact(root) == previous
    if argv:
        assert "source-update completion failed" not in result.stderr
        assert _receipts(tmp_path) == before_receipts, "a metadata query attempted a dependency sync"
    else:
        assert "source-update completion failed" in result.stderr
        assert "hermes update" in result.stderr


@pytest.mark.platforms("posix")
def test_completion_retry_backoff_and_cap(tmp_path, monkeypatch):
    """Consecutive completion-tail failures back off and finally stop self-starting (#122206:
    an environment where every launch re-runs a doomed ~4-minute tail)."""
    from hermes_cli import venv_sync

    monkeypatch.setattr(venv_sync, "completion_pending_path",
                        lambda root: tmp_path / "source-completion-pending")
    record = tmp_path / "source-completion-attempts"

    # No record yet: the first retry is immediate (historical behavior).
    assert venv_sync.completion_retry_state(tmp_path) == (True, 0, 0)

    # One recorded failure: the historical flaky case — retry immediately.
    record.write_text("1\n", encoding="utf-8")
    assert venv_sync.completion_retry_state(tmp_path) == (True, 1, 0)

    # Two consecutive failures: the next retry must wait out the backoff window.
    record.write_text("2\n", encoding="utf-8")
    may_retry, attempts, _backoff = venv_sync.completion_retry_state(tmp_path)
    assert (may_retry, attempts) == (False, 2)
    # After the backoff window the tail may run again.
    past = 12345.0
    old_mtime = past - venv_sync.COMPLETION_RETRY_BACKOFF_SECONDS - 1
    os.utime(record, (old_mtime, old_mtime))
    may_retry, attempts, _backoff = venv_sync.completion_retry_state(tmp_path, now=lambda: past)
    assert (may_retry, attempts) == (True, 2)

    # At the cap the tail never self-starts, regardless of age.
    record.write_text(f"{venv_sync.COMPLETION_RETRY_MAX_ATTEMPTS}\n", encoding="utf-8")
    for now_value in (past, past + 10 * venv_sync.COMPLETION_RETRY_BACKOFF_SECONDS):
        may_retry, attempts, _backoff = venv_sync.completion_retry_state(tmp_path, now=now_value)
        assert (may_retry, attempts) == (False, venv_sync.COMPLETION_RETRY_MAX_ATTEMPTS)

    # A corrupt record degrades to the historical unbounded behavior.
    record.write_text("not a number\n", encoding="utf-8")
    assert venv_sync.completion_retry_state(tmp_path) == (True, 0, 0)


@pytest.mark.platforms("posix")
def test_capped_completion_attempts_leave_marker_for_explicit_update(source_launch, tmp_path, capsys):
    """Past the retry cap a launch must NOT re-run the tail; it keeps the pending marker
    and points the operator at `hermes update` instead of burning another doomed attempt."""
    root, store_python, _ = source_launch
    from hermes_cli.venv_sync import (  # noqa: F401 — import for the paths under test
        _completion_attempts_path,
        arm_completion,
        completion_pending_path,
    )

    pm.sync_venv(["all"], explicit=True, project_root=root)
    arm_completion(root)
    attempts = _completion_attempts_path(root)
    attempts.write_text("99\n", encoding="utf-8")
    os.utime(attempts, (1.0, 1.0))  # ancient: age cannot rescue a capped record

    # The supervised-child carve-out does not apply: an unsupervised launch
    # would have re-run the tail here; capped, it must not. The launch itself
    # still resolves normally (the store python), only the tail is skipped.
    assert venv_sync.prepare_launch(root, []) == store_python
    out = capsys.readouterr().err
    assert "could not be finished automatically" in out
    assert "hermes update" in out
    assert completion_pending_path(root).is_file(), "the owed completion is still recorded"
    assert not (tmp_path / "completion-calls").exists(), "a capped launch re-ran the tail"


@pytest.mark.platforms("posix")
def test_failed_tail_attempt_is_counted_and_success_clears_it(source_launch, tmp_path, monkeypatch, capsys):
    """A failing completion tail records the attempt; the successful retry clears the record."""
    root, store_python, _ = source_launch
    from hermes_cli.venv_sync import _completion_attempts_path, arm_completion, completion_pending_path

    pm.sync_venv(["all"], explicit=True, project_root=root)
    arm_completion(root)
    # The tail script the fixture records calls with — make it fail, then succeed.
    calls = (root / "hermes_cli" / "source_completion.py")
    original = calls.read_text(encoding="utf-8")
    calls.write_text("import sys\nsys.exit(3)\n", encoding="utf-8")
    with pytest.raises(RuntimeError, match="source update completion failed"):
        venv_sync.prepare_launch(root, [])
    assert _completion_attempts_path(root).read_text(encoding="utf-8").strip() == "1"
    assert completion_pending_path(root).is_file(), "a failed attempt must keep the obligation"

    calls.write_text(original, encoding="utf-8")
    old = 1.0
    os.utime(_completion_attempts_path(root), (old, old))
    # A successful retry still launches normally and clears the obligation.
    assert venv_sync.prepare_launch(root, []) == store_python
    assert not completion_pending_path(root).exists()
    assert not _completion_attempts_path(root).exists(), "success cleared the attempt record"

