"""Opt-in clean install verification test. Clones the Manual Slop repo to tmp_path, runs `uv sync`, launches `sloppy.py --enable-test-hooks`, and verifies the Hook API responds. Catches "works on my machine" failures by exercising the full install-and-launch path in an isolated environment. Opt-in: set RUN_CLEAN_INSTALL_TEST=1 to enable. Otherwise skipped. Requires network access to REPO_URL. """ import os import socket import subprocess import sys import time import urllib.error import urllib.request from pathlib import Path import pytest REPO_URL = "https://git.cozyair.dev/ed/manual_slop" STARTUP_TIMEOUT_SECONDS = 30 READINESS_POLL_INTERVAL = 0.5 HOOK_PORT = 8999 def _http_get_json(url: str, timeout: float) -> dict: with urllib.request.urlopen(url, timeout=timeout) as response: raw = response.read().decode("utf-8") return _safe_json_loads(raw) def _safe_json_loads(raw: str) -> dict: import json return json.loads(raw) @pytest.mark.clean_install def test_clean_install_runs_with_hooks(tmp_path: Path) -> None: if os.environ.get("RUN_CLEAN_INSTALL_TEST") != "1": pytest.skip("Set RUN_CLEAN_INSTALL_TEST=1 to enable") clone_dir = tmp_path / "manual_slop" project_root = Path(__file__).resolve().parent.parent subprocess.run( ["git", "clone", REPO_URL, str(clone_dir)], capture_output=True, text=True, timeout=60, check=True, ) subprocess.run( ["uv", "sync"], cwd=str(clone_dir), capture_output=True, text=True, timeout=180, check=True, ) creationflags = 0 if os.name == "nt": creationflags = subprocess.CREATE_NEW_PROCESS_GROUP process = subprocess.Popen( ["uv", "run", "sloppy.py", "--enable-test-hooks"], cwd=str(clone_dir), stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=creationflags, ) try: start = time.time() ready = False while time.time() - start < STARTUP_TIMEOUT_SECONDS: if process.poll() is not None: stderr_out = process.stderr.read(2000) if process.stderr else b"" pytest.fail( f"Process exited early. stderr: {stderr_out.decode('utf-8', errors='replace')}" ) try: data = _http_get_json(f"http://127.0.0.1:{HOOK_PORT}/status", timeout=1.0) if data.get("status") in ("running", "ready", "ok"): ready = True break except (urllib.error.URLError, socket.timeout, ConnectionError, TimeoutError, OSError): pass time.sleep(READINESS_POLL_INTERVAL) assert ready, ( f"Hook server did not respond within {STARTUP_TIMEOUT_SECONDS}s. " f"stderr: {process.stderr.read(2000).decode('utf-8', errors='replace') if process.stderr else 'N/A'}" ) mma_data = _http_get_json(f"http://127.0.0.1:{HOOK_PORT}/api/gui/mma_status", timeout=5.0) assert isinstance(mma_data, dict), f"mma_status returned non-dict: {mma_data!r}" assert "mma_status" in mma_data, f"mma_status missing 'mma_status' key: {mma_data!r}" finally: _cleanup_process(process) def _cleanup_process(process: subprocess.Popen) -> None: if os.name == "nt": subprocess.run( ["taskkill", "/F", "/T", "/PID", str(process.pid)], capture_output=True, ) else: process.terminate() try: process.wait(timeout=5) except subprocess.TimeoutExpired: process.kill()