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

7.9 KiB

Clean Install Test Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add an opt-in pytest test that clones the Manual Slop repo to a temp dir, 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.

Architecture: Standard subprocess-based integration test. git clone from the user's Gitea server to tmp_path. uv sync in the cloned dir. Launch the app as a background subprocess. Poll the Hook API endpoint with requests until ready. Test a write hook. Clean up the process tree.

Tech Stack: Python 3.11+, pytest, subprocess, requests, git CLI

Spec: docs/superpowers/specs/2026-06-02-clean-install-test-design.md


Execution Constraints

  • No subagents. Execute as a single agent.
  • Pre-edit checkpoint: git add . before any file edit.
  • Per-file atomic commits.
  • Commit message format: <type>(<scope>): <imperative description>.
  • Git note format: 3-8 line rationale per commit.
  • Style baseline: 1-space indent, no comments, type hints.

File Structure

File Action Responsibility
tests/test_clean_install.py Create Opt-in test (RUN_CLEAN_INSTALL_TEST=1)
pyproject.toml Modify Add clean_install marker

Task 1: Add the clean_install marker to pyproject.toml

Files:

  • Modify: pyproject.toml

  • Step 1.1: Pre-edit checkpoint

git -C C:\projects\manual_slop add .
  • Step 1.2: Read current markers section

Use manual-slop_py_get_code_outline or grep for markers in pyproject.toml.

  • Step 1.3: Add the clean_install marker

Find the existing markers list (e.g., under [tool.pytest.ini_options]) and add:

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)",
]

If markers aren't already in pyproject.toml, add them. If they are, append the new entry.

  • Step 1.4: Commit
git -C C:\projects\manual_slop add pyproject.toml
git -C C:\projects\manual_slop commit -m "test(pytest): add clean_install marker"
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Registers the clean_install marker so tests can be selected with pytest -m clean_install or filtered with -m 'not clean_install'." $_ }

Task 2: Write the clean install test

Files:

  • Create: tests/test_clean_install.py

  • Step 2.1: Pre-edit checkpoint

git -C C:\projects\manual_slop add .
  • Step 2.2: Create the test file
# tests/test_clean_install.py
import os
import subprocess
import sys
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
HOOK_PORT = 8999


@pytest.mark.clean_install
def test_clean_install_runs_with_hooks(tmp_path):
    """Clone the repo, install deps, launch sloppy.py, verify Hook API.

    Opt-in: set RUN_CLEAN_INSTALL_TEST=1 to enable. Otherwise skipped.
    Requires network access to the configured REPO_URL.
    """
    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:
                response = requests.get(
                    f"http://127.0.0.1:{HOOK_PORT}/status",
                    timeout=1.0,
                )
                if response.status_code == 200:
                    payload = response.json()
                    if payload.get("status") in ("running", "ready", "ok"):
                        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. "
            f"stderr: {process.stderr.read(2000).decode('utf-8', errors='replace') if process.stderr else 'N/A'}"
        )

        response = requests.get(
            f"http://127.0.0.1:{HOOK_PORT}/api/gui/mma_status",
            timeout=5.0,
        )
        assert response.status_code == 200, f"mma_status returned {response.status_code}"
        data = response.json()
        assert isinstance(data, dict), f"mma_status returned non-dict: {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()
  • Step 2.3: Run the test in skip mode
uv run pytest tests/test_clean_install.py -v

Expected: SKIPPED (RUN_CLEAN_INSTALL_TEST not set).

  • Step 2.4: Commit
git -C C:\projects\manual_slop add tests/test_clean_install.py
git -C C:\projects\manual_slop commit -m "test(clean-install): add opt-in clone-and-verify pytest test"
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Clones the Gitea repo to tmp_path, runs uv sync, launches sloppy.py --enable-test-hooks, polls :8999/status until ready, then tests /api/gui/mma_status write hook. Robust Windows/Unix process cleanup. Skipped unless RUN_CLEAN_INSTALL_TEST=1." $_ }

Task 3: Phase Completion Verification

  • Step 3.1: Confirm test is properly gated
uv run pytest tests/test_clean_install.py -v

Expected: 1 skipped.

  • Step 3.2: Manual opt-in run (if network is available)
RUN_CLEAN_INSTALL_TEST=1 uv run pytest tests/test_clean_install.py -v

Expected: 1 passed (or 1 failed with clear diagnostic if the clone target is unreachable).

  • Step 3.3: Create the checkpoint commit
git -C C:\projects\manual_slop commit --allow-empty -m "conductor(checkpoint): Clean install test complete"
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Track complete. Opt-in test (RUN_CLEAN_INSTALL_TEST=1) added. Verifies clone + uv sync + launch + hook API. Marked with @pytest.mark.clean_install." $_ }

Self-Review

  • Spec coverage: All design tasks have a plan task. ✓
  • Placeholder scan: Test code is complete. ✓
  • Type consistency: tmp_path, process, response used consistently. ✓
  • Robust cleanup: _cleanup_process handles Windows + Unix. ✓