"""Cheap source-driver checks: real shells, disposable installs, no installer.""" import hashlib import json import os from pathlib import Path import runpy import shutil import subprocess import sys import sysconfig import pytest ASSETS = Path(__file__).resolve().parents[1] / "install/e2e-assets" @pytest.mark.platforms("posix") def test_shell_selects_the_installed_command_not_the_phase_or_path(tmp_path): root = tmp_path / "installed source" legacy = root / "venv/bin/hermes" published = root / ".hermes/bin/hermes" for command, label in ((legacy, "legacy"), (published, "published")): command.parent.mkdir(parents=True) command.write_text(f'#!/bin/sh\nprintf "{label}:%s\\n" "$*"\n', encoding="utf-8") command.chmod(0o755) env = dict(os.environ, INSTALL_DIR=str(root), ASSETS=str(ASSETS), HOME=str(tmp_path)) result = subprocess.run(["bash", "-euc", ''' source "$ASSETS/source-driver.sh" command=$(source_hermes "$INSTALL_DIR") "$command" 'literal argument' rm "$INSTALL_DIR/venv/bin/hermes" test "$(source_hermes "$INSTALL_DIR")" = "$command" printf '#!/bin/sh\nexit 23\n' > "$command" "$command" || test "$?" = 23 rm "$command" if source_hermes "$INSTALL_DIR"; then exit 91; fi printf '#!/bin/sh\nprintf legacy\n' > "$INSTALL_DIR/venv/bin/hermes" chmod +x "$INSTALL_DIR/venv/bin/hermes" "$(source_hermes "$INSTALL_DIR")" # A PM checkout cannot hide missing publication behind its old venv. mkdir -p "$INSTALL_DIR/pm" printf '{}' > "$INSTALL_DIR/pm/lock.json" if source_hermes "$INSTALL_DIR"; then exit 92; fi '''], env=env, cwd=tmp_path, capture_output=True, text=True, timeout=30) assert result.returncode == 0, result.stdout + result.stderr assert result.stdout.splitlines() == ["published:literal argument", "legacy"] @pytest.mark.platforms("posix") def test_installer_marker_is_the_only_dirty_state_a_driver_accepts(tmp_path): root = tmp_path / "installed source" root.mkdir() git = ["git", "-C", str(root), "-c", "user.name=t", "-c", "user.email=t@t"] subprocess.run([*git, "init", "-q"], check=True) (root / "tracked").write_text("v1\n", encoding="utf-8") subprocess.run([*git, "add", "tracked"], check=True) subprocess.run([*git, "commit", "-qm", "base"], check=True) (root / ".install_method").write_text("git\n", encoding="utf-8") env = dict(os.environ, INSTALL_DIR=str(root), ASSETS=str(ASSETS), HOME=str(tmp_path)) def accept(): return subprocess.run(["bash", "-euc", 'source "$ASSETS/source-driver.sh"; accept_installer_marker "$INSTALL_DIR"'], env=env, capture_output=True, text=True, timeout=30) assert accept().returncode == 0 assert (root / ".install_method").read_text(encoding="utf-8") == "git\n" assert subprocess.run([*git, "status", "--porcelain", "--untracked-files=all"], capture_output=True, text=True, check=True).stdout == "" (root / "tracked").write_text("edited\n", encoding="utf-8") refused = accept() assert refused.returncode != 0 and "M tracked" in refused.stderr @pytest.mark.platforms("windows", "posix") def test_powershell_selects_exact_exe_or_cmd_and_legacy_fallback(tmp_path): # These are file-selection rules, not Windows execution emulation. Run # them in real PowerShell on any host that supplies it (native CI included). path = os.pathsep.join(p for p in os.get_exec_path() if ".hermes" not in Path(p).parts) pwsh = shutil.which("pwsh", path=path) or shutil.which("powershell", path=path) if not pwsh: if os.name == "nt": pytest.fail("native Windows acceptance requires PowerShell") pytest.skip("PowerShell is not available on this host") root = tmp_path / "installed source" root.mkdir() harness = tmp_path / "probe.ps1" harness.write_text('''$ErrorActionPreference = 'Stop' . (Join-Path $env:ASSETS 'source-driver.ps1') $root = $env:INSTALL_DIR $legacy = Join-Path $root 'venv/Scripts/hermes.exe' $exe = Join-Path $root '.hermes/bin/hermes.exe' $cmd = Join-Path $root '.hermes/bin/hermes.cmd' New-Item -ItemType Directory -Path (Split-Path $legacy), (Split-Path $exe) -Force | Out-Null Set-Content $legacy 'legacy' if ((Get-SourceHermes $root) -ne $legacy) { throw 'legacy fallback' } Set-Content $cmd 'cmd' if ((Get-SourceHermes $root) -ne $cmd) { throw 'published cmd' } Set-Content $exe 'exe' if ((Get-SourceHermes $root) -ne $exe) { throw 'exe precedence' } Remove-Item $legacy if ((Get-SourceHermes $root) -ne $exe) { throw 'requires legacy venv' } Remove-Item $exe, $cmd $refused = $false try { Get-SourceHermes $root } catch { $refused = $true } if (-not $refused) { throw 'missing command accepted' } Set-Content $legacy 'stale legacy' New-Item -ItemType Directory -Path (Join-Path $root 'pm') | Out-Null Set-Content (Join-Path $root 'pm/lock.json') '{}' $refused = $false try { Get-SourceHermes $root } catch { $refused = $true } if (-not $refused) { throw 'missing PM launcher accepted' } Write-Output 'selection verified' ''', encoding="utf-8") result = subprocess.run([pwsh, "-NoProfile", "-NonInteractive", "-File", str(harness)], env=dict(os.environ, INSTALL_DIR=str(root), ASSETS=str(ASSETS), HOME=str(tmp_path)), cwd=tmp_path, capture_output=True, text=True, timeout=30) assert result.returncode == 0, result.stdout + result.stderr assert result.stdout.strip() == "selection verified" def test_observer_preserves_no_desktop_and_refuses_incomplete_app(tmp_path): verify = runpy.run_path(str(ASSETS / "source_driver.py"))["verify_products"] root = tmp_path / "source" root.mkdir() verify(root, "absent") assert list(root.iterdir()) == [] with pytest.raises(RuntimeError, match="desktop"): verify(root, "present") release = root / "apps/desktop/release/linux-unpacked" release.mkdir(parents=True) (release / "hermes").write_bytes(b"incomplete fixture") with pytest.raises(RuntimeError, match="desktop"): verify(root, "absent") with pytest.raises(RuntimeError, match="desktop"): verify(root, "present") assert (release / "hermes").read_bytes() == b"incomplete fixture" @pytest.mark.platforms("posix") @pytest.mark.parametrize("fault,error", [ ("uv-lock", "dependency generation is not current"), ("no-desktop", "dependency generation is not current"), ("foreign-launcher", "belongs to another installation"), ("missing-launcher", ""), ("incomplete", "incomplete"), ("web-changed", "web output"), ("web-missing", "web output"), ]) def test_pm_observer_accepts_ready_fixture_and_leaves_failed_fixture_untouched(tmp_path, fault, error): # This exercises the complete observer process, not a whole install. The # fixture borrows prepared test dependencies and records real PM input # stamps / compiler receipts. It never resolves or acquires a package. repo = ASSETS.parents[2] root = tmp_path / "source" for directory in ("pm", "hermes_cli"): shutil.copytree(repo / directory, root / directory, ignore=shutil.ignore_patterns("__pycache__")) shutil.copy2(repo / "hermes_constants.py", root / "hermes_constants.py") (root / "hermes_bootstrap.py").write_text("raise RuntimeError('bootstrap must not run')", encoding="utf-8") (root / "uv.lock").write_text("fixture locked graph", encoding="utf-8") home = tmp_path / "home" store = tmp_path / "store" env = dict(os.environ, HOME=str(home), HERMES_HOME=str(home), HERMES_RUNTIME_DIR=str(store), PYTHONDONTWRITEBYTECODE="1", HERMES_DISABLE_LAZY_INSTALLS="1") env.pop("HERMES_INSTALL_ROOT", None) path = os.pathsep.join(p for p in os.get_exec_path() if ".hermes" not in Path(p).parts) node = shutil.which("node", path=path) assert node setup = '''import sys from pathlib import Path root, store, node, deps = map(Path, sys.argv[1:]) sys.path.insert(0, str(root)) from pm.lock import Facts, Lockfile from pm.packages import Venv from pm.environments import install_state_dir, runtime_facts_path, site_packages from hermes_cli._launchers import ensure_install_launchers lock = Lockfile(root / 'pm/lock.json') lock.set_pin('node', 'fixture', {}) lock.save() for name, executable in [('python', Path(sys.executable)), ('node', node)]: binary = store / name / 'bin' / ('python3' if name == 'python' else name) binary.parent.mkdir(parents=True) binary.symlink_to(executable) Facts(store / 'facts.json').record(name, 'fixture', name, {}, store) selected = install_state_dir(root) / 'environments/fixture/venv' selected.mkdir(parents=True) (selected / 'pyvenv.cfg').write_text('home = fixture') site = site_packages(selected) site.parent.mkdir(parents=True) site.symlink_to(deps, target_is_directory=True) Facts(runtime_facts_path(root)).record_state('venv', Venv(root).expected_stamp([]), [], environment=selected) ensure_install_launchers(root, root / '.hermes/bin') ''' subprocess.run([sys.executable, "-I", "-B", "-c", setup, str(root), str(store), node, sysconfig.get_path("purelib")], env=env, cwd=tmp_path, check=True, timeout=60) build = root / "scripts/build" build.mkdir(parents=True) for name in ("freshness.mjs", "frontend-common.mjs"): shutil.copy2(repo / "scripts/build" / name, build / name) record = '''import { mkdirSync, writeFileSync } from 'node:fs'; import { buildInputs, recordProduct } from './scripts/build/freshness.mjs'; const source = process.cwd(); for (const [product, out] of [['tui', 'ui-tui/dist'], ['web', 'hermes_cli/web_dist'], ['desktop', 'apps/desktop/release/linux-unpacked/resources/app.asar.unpacked/dist']]) { mkdirSync(out, { recursive: true }); writeFileSync(out + '/index.html', 'fixture product'); recordProduct({source, product, out, inputs: buildInputs(source, product)}); } ''' subprocess.run([node, "--input-type=module", "-e", record], cwd=root, env=env, check=True, timeout=30) (root / "apps/desktop/release/linux-unpacked/hermes").write_bytes(b"fixture executable") command = [sys.executable, "-B", str(ASSETS / "source_driver.py"), "--root", str(root), "--launcher", str(root / ".hermes/bin/hermes"), "--desktop", "present"] if fault == "no-desktop": shutil.rmtree(root / "apps/desktop") command[-1] = "absent" # The passive query must not write even import caches: -I ignores the # environment's bytecode switch, so the published query enforces it. def snapshot(): return {p: (hashlib.sha256(p.read_bytes()).hexdigest(), p.stat().st_mtime_ns) for tree in (root, home, store) for p in tree.rglob("*") if p.is_file()} before = snapshot() result = subprocess.run(command, env=env, cwd=tmp_path, capture_output=True, text=True, timeout=60) assert result.returncode == 0, result.stdout + result.stderr assert snapshot() == before if fault in {"uv-lock", "no-desktop"}: (root / "uv.lock").write_text("changed graph without preparation", encoding="utf-8") elif fault == "foreign-launcher": foreign = tmp_path / "foreign" shutil.copytree(root, foreign) from hermes_cli._launchers import mint_launcher launcher = mint_launcher("hermes", foreign, root / ".hermes/bin", Path(sys.executable), None) assert launcher is not None elif fault == "missing-launcher": (root / ".hermes/bin/hermes").unlink() elif fault == "incomplete": (root / ".update-incomplete").write_text("incomplete", encoding="utf-8") else: damaged = root / "hermes_cli/web_dist/index.html" if fault == "web-missing": damaged.unlink() else: damaged.write_bytes(b"damaged") before = snapshot() result = subprocess.run(command, env=env, cwd=tmp_path, capture_output=True, text=True, timeout=60) assert result.returncode != 0 assert error in result.stderr assert "bootstrap must not run" not in result.stderr assert snapshot() == before