---
phase: 17-branch-and-restore
plan: 02
type: execute
wave: 2
depends_on: ["17-01"]
files_modified:
  - tests/test_branch.py
  - tests/test_restore.py
  - pyproject.toml
autonomous: true

must_haves:
  truths:
    - "BRANCH-01: git branch lists bookmarks"
    - "BRANCH-02: git branch <name> creates bookmark"
    - "BRANCH-03: git branch -d <name> deletes bookmark"
    - "BRANCH-04: git branch -D <name> deletes bookmark without stripping commits"
    - "RESTORE-01: git restore <file> discards changes to file"
    - "RESTORE-02: git restore . discards all changes"
  artifacts:
    - path: "tests/test_branch.py"
      provides: "E2E tests for BRANCH-01 through BRANCH-04"
      min_lines: 50
    - path: "tests/test_restore.py"
      provides: "E2E tests for RESTORE-01 and RESTORE-02"
      min_lines: 40
    - path: "pyproject.toml"
      provides: "Pytest markers for branch and restore"
      contains: "branch"
  key_links:
    - from: "tests/test_branch.py"
      to: "conftest.run_gitsl"
      via: "import"
      pattern: "from conftest import run_gitsl"
    - from: "tests/test_restore.py"
      to: "conftest.run_gitsl"
      via: "import"
      pattern: "from conftest import run_gitsl"
---

<objective>
Create E2E tests validating all 6 requirements for branch and restore commands.

Purpose: Verify that git branch and git restore commands correctly translate to Sapling equivalents, with special attention to the -D safety translation.

Output: Two test files (test_branch.py, test_restore.py) covering all requirements.
</objective>

<execution_context>
@~/.claude/get-shit-done/workflows/execute-plan.md
@~/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/17-branch-and-restore/17-RESEARCH.md
@.planning/phases/17-branch-and-restore/17-01-SUMMARY.md
@tests/conftest.py
@tests/test_switch.py
@tests/helpers/commands.py
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create E2E tests for branch command</name>
  <files>tests/test_branch.py</files>
  <action>
Create tests/test_branch.py following the pattern from test_switch.py:

```python
"""E2E tests for git branch command (BRANCH-01 through BRANCH-04)."""

import shutil
from pathlib import Path

import pytest

from conftest import run_gitsl
from helpers.commands import run_command


sl_available = shutil.which("sl") is not None
pytestmark = [
    pytest.mark.skipif(not sl_available, reason="Sapling (sl) not installed"),
    pytest.mark.branch,
]


class TestBranchList:
    """BRANCH-01: git branch translates to sl bookmark (list)."""

    def test_branch_lists_bookmarks(self, sl_repo_with_commit: Path):
        """git branch lists existing bookmarks."""
        # Create a bookmark first
        run_command(["sl", "bookmark", "feature"], cwd=sl_repo_with_commit)

        result = run_gitsl(["branch"], cwd=sl_repo_with_commit)
        assert result.exit_code == 0
        assert "feature" in result.stdout

    def test_branch_empty_list(self, sl_repo_with_commit: Path):
        """git branch with no bookmarks returns success."""
        result = run_gitsl(["branch"], cwd=sl_repo_with_commit)
        assert result.exit_code == 0


class TestBranchCreate:
    """BRANCH-02: git branch <name> translates to sl bookmark <name>."""

    def test_branch_creates_bookmark(self, sl_repo_with_commit: Path):
        """git branch <name> creates new bookmark."""
        result = run_gitsl(["branch", "new-feature"], cwd=sl_repo_with_commit)
        assert result.exit_code == 0

        # Verify bookmark was created
        bookmarks = run_command(["sl", "bookmark"], cwd=sl_repo_with_commit)
        assert "new-feature" in bookmarks.stdout

    def test_branch_duplicate_fails(self, sl_repo_with_commit: Path):
        """git branch <name> with existing name fails."""
        run_command(["sl", "bookmark", "existing"], cwd=sl_repo_with_commit)

        result = run_gitsl(["branch", "existing"], cwd=sl_repo_with_commit)
        assert result.exit_code != 0


class TestBranchDelete:
    """BRANCH-03: git branch -d <name> translates to sl bookmark -d."""

    def test_branch_delete(self, sl_repo_with_commit: Path):
        """git branch -d deletes existing bookmark."""
        run_command(["sl", "bookmark", "to-delete"], cwd=sl_repo_with_commit)

        result = run_gitsl(["branch", "-d", "to-delete"], cwd=sl_repo_with_commit)
        assert result.exit_code == 0

        # Verify bookmark was deleted
        bookmarks = run_command(["sl", "bookmark"], cwd=sl_repo_with_commit)
        assert "to-delete" not in bookmarks.stdout

    def test_branch_delete_nonexistent_fails(self, sl_repo_with_commit: Path):
        """git branch -d with nonexistent bookmark fails."""
        result = run_gitsl(["branch", "-d", "nonexistent"], cwd=sl_repo_with_commit)
        assert result.exit_code != 0


class TestBranchForceDelete:
    """BRANCH-04: git branch -D translates to sl bookmark -d (safe)."""

    def test_branch_force_delete(self, sl_repo_with_commit: Path):
        """git branch -D deletes bookmark."""
        run_command(["sl", "bookmark", "force-delete"], cwd=sl_repo_with_commit)

        result = run_gitsl(["branch", "-D", "force-delete"], cwd=sl_repo_with_commit)
        assert result.exit_code == 0

        # Verify bookmark was deleted
        bookmarks = run_command(["sl", "bookmark"], cwd=sl_repo_with_commit)
        assert "force-delete" not in bookmarks.stdout

    def test_branch_force_delete_preserves_commits(self, sl_repo_with_commit: Path):
        """git branch -D removes bookmark but preserves commits.

        CRITICAL: This verifies the -D to -d safety translation.
        sl bookmark -D would strip commits, which is NOT what git branch -D does.
        """
        # Create bookmark and make a commit on it
        run_command(["sl", "bookmark", "test-branch"], cwd=sl_repo_with_commit)
        (sl_repo_with_commit / "extra.txt").write_text("extra content\n")
        run_command(["sl", "add", "extra.txt"], cwd=sl_repo_with_commit)
        run_command(["sl", "commit", "-m", "Extra commit"], cwd=sl_repo_with_commit)

        # Get current commit hash
        log_before = run_command(["sl", "log", "-l", "1", "-T", "{node}"],
                                 cwd=sl_repo_with_commit)
        commit_hash = log_before.stdout.strip()

        # Force delete the bookmark
        result = run_gitsl(["branch", "-D", "test-branch"], cwd=sl_repo_with_commit)
        assert result.exit_code == 0

        # CRITICAL: Verify commit still exists (not stripped)
        log_after = run_command(["sl", "log", "-r", commit_hash[:12], "-T", "{node}"],
                                cwd=sl_repo_with_commit)
        assert commit_hash[:12] in log_after.stdout, \
            "Commit was stripped! -D should translate to -d to preserve commits"
```

Use sl_repo_with_commit fixture to ensure bookmarks can be created (requires at least one commit).
  </action>
  <verify>
    - File exists: `ls tests/test_branch.py`
    - Syntax check: `python -m py_compile tests/test_branch.py`
    - Run tests: `pytest tests/test_branch.py -v`
  </verify>
  <done>
    - tests/test_branch.py exists with tests for BRANCH-01 through BRANCH-04
    - All tests pass when run with pytest
    - Critical test verifies -D to -d safety translation preserves commits
  </done>
</task>

<task type="auto">
  <name>Task 2: Create E2E tests for restore command</name>
  <files>tests/test_restore.py</files>
  <action>
Create tests/test_restore.py:

```python
"""E2E tests for git restore command (RESTORE-01, RESTORE-02)."""

import shutil
from pathlib import Path

import pytest

from conftest import run_gitsl
from helpers.commands import run_command


sl_available = shutil.which("sl") is not None
pytestmark = [
    pytest.mark.skipif(not sl_available, reason="Sapling (sl) not installed"),
    pytest.mark.restore,
]


class TestRestoreFile:
    """RESTORE-01: git restore <file> translates to sl revert <file>."""

    def test_restore_discards_changes(self, sl_repo_with_commit: Path):
        """git restore <file> discards working tree changes."""
        readme = sl_repo_with_commit / "README.md"
        original_content = readme.read_text()

        # Modify the file
        readme.write_text("modified content\n")
        status = run_command(["sl", "status"], cwd=sl_repo_with_commit)
        assert "M README.md" in status.stdout

        # Restore it
        result = run_gitsl(["restore", "README.md"], cwd=sl_repo_with_commit)
        assert result.exit_code == 0

        # Verify content restored
        assert readme.read_text() == original_content

    def test_restore_nonexistent_file(self, sl_repo_with_commit: Path):
        """git restore on nonexistent file fails gracefully."""
        result = run_gitsl(["restore", "nonexistent.txt"], cwd=sl_repo_with_commit)
        # sl revert returns error for nonexistent files
        assert result.exit_code != 0


class TestRestoreAll:
    """RESTORE-02: git restore . translates to sl revert ."""

    def test_restore_all_discards_changes(self, sl_repo_with_commit: Path):
        """git restore . discards all working tree changes."""
        readme = sl_repo_with_commit / "README.md"
        original_content = readme.read_text()

        # Modify existing file
        readme.write_text("modified content\n")

        # Create and add new file
        new_file = sl_repo_with_commit / "new.txt"
        new_file.write_text("new content\n")
        run_command(["sl", "add", "new.txt"], cwd=sl_repo_with_commit)

        # Verify changes exist
        status_before = run_command(["sl", "status"], cwd=sl_repo_with_commit)
        assert "M README.md" in status_before.stdout
        assert "A new.txt" in status_before.stdout

        # Restore all
        result = run_gitsl(["restore", "."], cwd=sl_repo_with_commit)
        assert result.exit_code == 0

        # Verify modified file restored
        assert readme.read_text() == original_content

        # Verify added file is now untracked (forgotten)
        status_after = run_command(["sl", "status"], cwd=sl_repo_with_commit)
        assert "M README.md" not in status_after.stdout
        assert "A new.txt" not in status_after.stdout
        assert "? new.txt" in status_after.stdout

    def test_restore_all_clean_workdir(self, sl_repo_with_commit: Path):
        """git restore . on clean workdir succeeds."""
        result = run_gitsl(["restore", "."], cwd=sl_repo_with_commit)
        # Should succeed with no changes to revert
        assert result.exit_code == 0
```

Use sl_repo_with_commit fixture which provides a repo with README.md already committed.
  </action>
  <verify>
    - File exists: `ls tests/test_restore.py`
    - Syntax check: `python -m py_compile tests/test_restore.py`
    - Run tests: `pytest tests/test_restore.py -v`
  </verify>
  <done>
    - tests/test_restore.py exists with tests for RESTORE-01 and RESTORE-02
    - All tests pass when run with pytest
    - Tests verify file content is properly restored
  </done>
</task>

<task type="auto">
  <name>Task 3: Register pytest markers</name>
  <files>pyproject.toml</files>
  <action>
Add pytest markers for branch and restore to pyproject.toml under [tool.pytest.ini_options] markers section:

```toml
    "branch: tests for git branch command",
    "restore: tests for git restore command",
```

Add these after existing markers (like switch, clean, config from Phase 16).
  </action>
  <verify>
    - Check markers: `grep -A20 "markers = " pyproject.toml`
    - Run with marker: `pytest -m branch --collect-only`
    - Run with marker: `pytest -m restore --collect-only`
  </verify>
  <done>
    - pyproject.toml has branch and restore markers
    - pytest --collect-only shows tests are collected with markers
  </done>
</task>

</tasks>

<verification>
Run from project root:

```bash
# Run all Phase 17 tests
pytest tests/test_branch.py tests/test_restore.py -v

# Verify markers work
pytest -m "branch or restore" -v

# Run full test suite to ensure no regressions
pytest
```

All tests should pass. Critical BRANCH-04 test must verify commits are not stripped.
</verification>

<success_criteria>
1. tests/test_branch.py exists with tests for all BRANCH requirements
2. tests/test_restore.py exists with tests for all RESTORE requirements
3. All 6 requirements have passing tests
4. Critical safety test (BRANCH-04) verifies -D to -d translation preserves commits
5. pytest markers are registered
6. Full test suite passes (no regressions)
</success_criteria>

<output>
After completion, create `.planning/phases/17-branch-and-restore/17-02-SUMMARY.md`
</output>
