---
phase: 02-e2e-test-infrastructure
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - tests/__init__.py
  - tests/helpers/__init__.py
  - tests/helpers/commands.py
  - tests/helpers/comparison.py
  - tests/conftest.py
autonomous: true

must_haves:
  truths:
    - "Test can create temp git repo and it has .git directory"
    - "Test can run git command and capture stdout, stderr, exit code"
    - "Test can run gitsl command and capture stdout, stderr, exit code"
    - "Test can compare two command outputs exactly"
    - "Test can compare two command outputs semantically (ignoring whitespace)"
  artifacts:
    - path: "tests/helpers/commands.py"
      provides: "CommandResult dataclass and run_command helper"
      exports: ["CommandResult", "run_command"]
    - path: "tests/helpers/comparison.py"
      provides: "Output comparison utilities"
      exports: ["compare_exact", "compare_semantic", "assert_output_match", "assert_commands_equal"]
    - path: "tests/conftest.py"
      provides: "Shared pytest fixtures and command runners"
      exports: ["run_git", "run_gitsl", "git_repo", "git_repo_with_commit", "git_repo_with_changes", "git_repo_with_branch"]
  key_links:
    - from: "tests/conftest.py"
      to: "tests/helpers/commands.py"
      via: "imports CommandResult, run_command"
      pattern: "from helpers.commands import"
    - from: "tests/conftest.py"
      to: "gitsl.py"
      via: "run_gitsl executes gitsl.py via subprocess"
      pattern: "gitsl_path.*gitsl.py"
---

<objective>
Create the core E2E test infrastructure: subprocess command helpers, output comparison utilities, and git repo fixtures.

Purpose: Establish the foundation for golden-master testing where git and gitsl outputs are compared on identical temp repositories.

Output:
- `tests/helpers/commands.py` - CommandResult dataclass, run_command helper
- `tests/helpers/comparison.py` - Exact and semantic comparison utilities
- `tests/conftest.py` - run_git, run_gitsl, and repo fixtures
</objective>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/02-e2e-test-infrastructure/02-RESEARCH.md
@gitsl.py
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create command execution helpers</name>
  <files>
    - tests/__init__.py
    - tests/helpers/__init__.py
    - tests/helpers/commands.py
  </files>
  <action>
Create the test package structure and command execution helpers.

1. Create `tests/__init__.py` (empty, marks package)

2. Create `tests/helpers/__init__.py` (empty, marks subpackage)

3. Create `tests/helpers/commands.py` with:
   - `CommandResult` dataclass with fields: `stdout: str`, `stderr: str`, `exit_code: int`
   - Property `success -> bool` that returns `exit_code == 0`
   - `run_command(cmd: List[str], cwd: Optional[Path] = None, env: Optional[dict] = None) -> CommandResult`
   - Uses `subprocess.run(capture_output=True, text=True)`
   - Merges custom env with `os.environ.copy()` if env is provided

Follow the pattern from 02-RESEARCH.md Pattern 1 and Pattern 2.
  </action>
  <verify>
    python -c "from tests.helpers.commands import CommandResult, run_command; print('Import OK')"
  </verify>
  <done>
    - CommandResult dataclass exists with stdout, stderr, exit_code fields
    - run_command function captures subprocess output and returns CommandResult
    - Import succeeds from project root
  </done>
</task>

<task type="auto">
  <name>Task 2: Create output comparison utilities</name>
  <files>tests/helpers/comparison.py</files>
  <action>
Create comparison utilities for exact and semantic output matching.

Create `tests/helpers/comparison.py` with:

1. `normalize_for_semantic(text: str) -> List[str]` - internal helper
   - Strips leading/trailing whitespace per line
   - Collapses multiple whitespace to single space (re.sub(r'\s+', ' ', line))
   - Removes empty lines
   - Returns list of normalized lines

2. `compare_exact(expected: str, actual: str) -> bool`
   - Simple `expected == actual`

3. `compare_semantic(expected: str, actual: str) -> bool`
   - Uses normalize_for_semantic on both inputs
   - Compares normalized line lists

4. `generate_diff(expected: str, actual: str, context_lines: int = 3) -> str`
   - Uses difflib.unified_diff
   - fromfile='expected', tofile='actual'
   - Returns joined diff string

5. `assert_output_match(expected: str, actual: str, mode: str = "exact", message: str = "Output mismatch") -> None`
   - Chooses compare_fn based on mode ("exact" or "semantic")
   - On mismatch: raises AssertionError with diff

6. `assert_commands_equal(git_result: CommandResult, gitsl_result: CommandResult, mode: str = "exact") -> None`
   - Asserts exit codes match (always exact)
   - Asserts stdout matches using specified mode
   - Error messages include both stderr values for debugging

Import CommandResult from .commands (relative import).

Follow patterns from 02-RESEARCH.md Pattern 5 and Pattern 6.
  </action>
  <verify>
    python -c "from tests.helpers.comparison import compare_exact, compare_semantic, assert_commands_equal; print('Import OK')"
  </verify>
  <done>
    - compare_exact returns True for identical strings, False otherwise
    - compare_semantic returns True for whitespace-different but semantically same strings
    - assert_commands_equal raises AssertionError with helpful diff on mismatch
  </done>
</task>

<task type="auto">
  <name>Task 3: Create conftest.py with fixtures and runners</name>
  <files>tests/conftest.py</files>
  <action>
Create shared pytest fixtures and command runner helpers.

Create `tests/conftest.py` with:

1. Import helpers:
   ```python
   from pathlib import Path
   from typing import List, Optional
   import pytest
   from helpers.commands import CommandResult, run_command
   ```

2. `run_git(args: List[str], cwd: Path, env: Optional[dict] = None) -> CommandResult`
   - Runs ["git"] + args via run_command

3. `run_gitsl(args: List[str], cwd: Path, env: Optional[dict] = None) -> CommandResult`
   - Gets gitsl_path = Path(__file__).parent.parent / "gitsl.py"
   - Runs ["python", str(gitsl_path)] + args via run_command
   - This tests the actual CLI, not imported functions

4. Fixtures (each using pytest.fixture decorator):

   a. `git_repo(tmp_path: Path) -> Path`
      - Runs git init in tmp_path
      - Configures user.email = "test@test.com"
      - Configures user.name = "Test User"
      - Returns tmp_path

   b. `git_repo_with_commit(git_repo: Path) -> Path`
      - Creates README.md with "# Test Repository\n"
      - Runs git add README.md
      - Runs git commit -m "Initial commit"
      - Returns git_repo

   c. `git_repo_with_changes(git_repo_with_commit: Path) -> Path`
      - Modifies README.md to add "\nModified content.\n"
      - Creates new_file.txt with "New untracked file\n"
      - Returns git_repo_with_commit

   d. `git_repo_with_branch(git_repo_with_commit: Path) -> Path`
      - Runs git branch feature
      - Returns git_repo_with_commit

Follow patterns from 02-RESEARCH.md Pattern 3 and Pattern 4.
  </action>
  <verify>
    python -c "import pytest; from tests.conftest import run_git, run_gitsl, git_repo, git_repo_with_commit; print('Import OK')"
  </verify>
  <done>
    - run_git and run_gitsl functions exist and return CommandResult
    - All four fixtures defined: git_repo, git_repo_with_commit, git_repo_with_changes, git_repo_with_branch
    - Fixtures properly chain (git_repo_with_commit uses git_repo, etc.)
  </done>
</task>

</tasks>

<verification>
After all tasks complete:

```bash
# Verify package structure
ls -la tests/
ls -la tests/helpers/

# Verify all imports work
python -c "
from tests.helpers.commands import CommandResult, run_command
from tests.helpers.comparison import compare_exact, compare_semantic, assert_commands_equal
from tests.conftest import run_git, run_gitsl
print('All imports successful')
"

# Quick sanity test - run git status in temp dir
python -c "
import tempfile
from pathlib import Path
from tests.helpers.commands import run_command

with tempfile.TemporaryDirectory() as tmp:
    result = run_command(['git', 'init'], cwd=Path(tmp))
    print(f'git init exit code: {result.exit_code}')
    assert result.exit_code == 0, 'git init failed'
print('Sanity test passed')
"
```
</verification>

<success_criteria>
1. tests/ directory structure exists with __init__.py files
2. CommandResult dataclass captures stdout, stderr, exit_code
3. run_command wraps subprocess.run correctly
4. compare_exact and compare_semantic comparison modes work
5. run_git and run_gitsl helpers invoke correct commands
6. All four repo fixtures defined with proper chaining
7. All imports work from project root
</success_criteria>

<output>
After completion, create `.planning/phases/02-e2e-test-infrastructure/02-01-SUMMARY.md`

Include:
- Files created
- Key exports from each module
- Any decisions made during implementation
- Verification results
</output>
