Private
Public Access
0
0
Files
manual_slop/docs/superpowers/specs/2026-06-02-clean-install-test-design.md
T

6.2 KiB

Clean Install Test

Date: 2026-06-02 Status: Draft (pending review)


Context & Motivation

The user wants a "clean install" test that verifies Manual Slop works correctly when installed from scratch in an isolated environment. The test should:

  1. Clone the repo to a temp directory (no shared state with the source)
  2. Install dependencies via uv sync
  3. Launch sloppy.py --enable-test-hooks
  4. Verify the Hook API responds (smoke test that the app is functional)

This is a defense against:

  • Repository changes that only work on the developer's machine
  • Dependency drift (works on dev, fails on fresh install)
  • Build/launch issues that only appear in clean environments

The target is the user's private Gitea server: https://git.cozyair.dev/ed/manual_slop. This is intentionally NOT a public GitHub URL — the test must work in the user's private infrastructure.


Scope

In Scope

  • tests/test_clean_install.py — Opt-in pytest test
  • pyproject.toml update: add clean_install marker
  • Gating via RUN_CLEAN_INSTALL_TEST=1 env var

Out of Scope

  • Auto-clone in CI (the user can opt in via a future CI workflow)
  • Continuous monitoring
  • Cloning from a specific branch/tag (uses HEAD of main by default)

Design

Test File Structure

# tests/test_clean_install.py
import os
import shutil
import subprocess
import time
from pathlib import Path

import pytest
import requests


REPO_URL = "https://git.cozyair.dev/ed/manual_slop"
STARTUP_TIMEOUT_SECONDS = 30
READINESS_POLL_INTERVAL = 0.5


@pytest.mark.clean_install
def test_clean_install_runs_with_hooks(tmp_path):
    """Clone the repo, install deps, launch sloppy.py, verify Hook API."""
    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"
    
    # 1. Clone
    result = subprocess.run(
        ["git", "clone", REPO_URL, str(clone_dir)],
        capture_output=True, text=True, timeout=60,
    )
    assert result.returncode == 0, f"Clone failed: {result.stderr}"
    
    # 2. Install deps
    result = subprocess.run(
        ["uv", "sync"],
        cwd=str(clone_dir),
        capture_output=True, text=True, timeout=180,
    )
    assert result.returncode == 0, f"uv sync failed: {result.stderr}"
    
    # 3. Launch sloppy.py with hooks
    process = subprocess.Popen(
        ["uv", "run", "sloppy.py", "--enable-test-hooks"],
        cwd=str(clone_dir),
        stdout=subprocess.PIPE, stderr=subprocess.PIPE,
        creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if os.name == 'nt' else 0,
    )
    
    try:
        # 4. Poll /status endpoint
        start = time.time()
        ready = False
        while time.time() - start < STARTUP_TIMEOUT_SECONDS:
            if process.poll() is not None:
                pytest.fail(f"Process exited early. stderr: {process.stderr.read()[:2000]}")
            try:
                response = requests.get(
                    "http://127.0.0.1:8999/status",
                    timeout=1.0,
                )
                if response.status_code == 200:
                    payload = response.json()
                    if payload.get("status") == "running":
                        ready = True
                        break
            except (requests.ConnectionError, requests.Timeout):
                pass
            time.sleep(READINESS_POLL_INTERVAL)
        
        assert ready, f"Hook server did not respond within {STARTUP_TIMEOUT_SECONDS}s"
        
        # 5. Test a write hook (any POST endpoint that should respond)
        response = requests.get(
            "http://127.0.0.1:8999/api/gui/mma_status",
            timeout=5.0,
        )
        assert response.status_code == 200
        # The mma_status endpoint returns a dict; verify it has expected keys
        data = response.json()
        assert "status" in data or "mma_state" in data
    
    finally:
        # 6. Cleanup
        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()

pyproject.toml Update

[tool.pytest.ini_options]
markers = [
    "integration: integration tests requiring live GUI",
    "strict: tests that require strict mode",
    "clean_install: clean install verification (opt-in via RUN_CLEAN_INSTALL_TEST=1)",
]

Running the Test

Default (skip):

uv run pytest tests/test_clean_install.py -v
# SKIPPED: Set RUN_CLEAN_INSTALL_TEST=1 to enable

Opt-in:

RUN_CLEAN_INSTALL_TEST=1 uv run pytest tests/test_clean_install.py -v

Just the clean_install marker:

RUN_CLEAN_INSTALL_TEST=1 uv run pytest -m clean_install -v

File Structure

  • tests/test_clean_install.py — NEW
  • pyproject.toml — MODIFY: add clean_install marker

Acceptance Criteria

  • RUN_CLEAN_INSTALL_TEST=1 uv run pytest tests/test_clean_install.py -v passes when run in an environment with network access to git.cozyair.dev
  • Without the env var, the test skips (no network access required)
  • The test takes 30-90 seconds to run (clone + install + launch)
  • A failure in any step (clone, sync, launch, hook response) results in a clear error message
  • Process cleanup is robust (no orphaned processes on Windows or Unix)

Risks

  1. Network dependency: The test requires network access to git.cozyair.dev. In CI environments without that access, the test will fail. Mitigation: the env var gating makes this opt-in.
  2. Clone target is private: Unlike GitHub, the URL is on a private Gitea server. Test failure on a public CI would leak the existence of the private repo. Mitigation: only run on private infrastructure; the test is opt-in.
  3. Port conflicts: The test uses port 8999. If another process is using it, the test will fail. Mitigation: the polling loop detects early exit and reports the port-in-use error.
  4. uv sync is slow: On a fresh machine, uv sync can take 30-60 seconds. The test budget is 180 seconds which should be sufficient.