---
phase: 19-testing
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
  - tests/test_setup_wizard.py
autonomous: true
requirements:
  - TEST-02

must_haves:
  truths:
    - "Agent platform configs contain correct skill_dir values for all 8 agents"
    - "EnvChecker.check_python detects Python version correctly"
    - "EnvChecker.check_vault detects missing required directories"
    - "EnvChecker.check_dependencies detects missing Python packages"
    - "EnvChecker.get_exports_dir resolves the correct path"
    - "_find_zotero searches registry, common paths, and which/where"
    - "CheckResult class correctly tracks passed/detail/action_required state"
  artifacts:
    - path: "tests/test_setup_wizard.py"
      provides: "Unit tests for setup_wizard.py standalone functions and EnvChecker class"
      min_lines: 120
  key_links:
    - from: "tests/test_setup_wizard.py"
      to: "setup_wizard.py"
      via: "from setup_wizard import AGENT_CONFIGS, EnvChecker, CheckResult, _find_vault"
---

<objective>
Create setup wizard tests validating agent platform detection, vault path resolution, environment checks, and configuration file generation.

Purpose: Close TEST-02 — validate the setup wizard's standalone functions and EnvChecker class logic. Tests use the Textual testing harness if available, but primarily focus on testing the standalone detection and validation functions independently.
Output: `tests/test_setup_wizard.py` with 4-5 test classes covering all wizard logic.
</objective>

<execution_context>
@C:/Users/Lin/.opencode/get-shit-done/workflows/execute-plan.md
</execution_context>

<context>
@setup_wizard.py
@tests/conftest.py
@tests/test_smoke.py::TestSetupWizard
</context>

<interfaces>
Key functions and classes under test from setup_wizard.py:

```python
# Constants
AGENT_CONFIGS = {
    "opencode": {"name": "OpenCode", "skill_dir": ".opencode/skills", ...},
    "cursor": {"name": "Cursor", "skill_dir": ".cursor/skills", ...},
    "claude": {"name": "Claude Code", "skill_dir": ".claude/skills", ...},
    "windsurf": {"name": "Windsurf", "skill_dir": ".windsurf/skills", ...},
    "github_copilot": {"name": "GitHub Copilot", "skill_dir": ".github/skills", ...},
    "cline": {"name": "Cline", "skill_dir": ".clinerules/skills", ...},
    "augment": {"name": "Augment", "skill_dir": ".augment/skills", ...},
    "trae": {"name": "Trae", "skill_dir": ".trae/skills", ...},
}

# Classes
class CheckResult:
    def __init__(self, name: str): ...
    # Fields: name, passed, detail, action_required

class EnvChecker:
    def __init__(self, vault: Path): ...
    def get_exports_dir(self) -> Path: ...
    def check_python(self) -> CheckResult: ...
    def check_dependencies(self) -> CheckResult: ...
    def check_vault(self) -> CheckResult: ...
    def _find_zotero(self, manual_path: Path | None = None) -> Path | None: ...
    def check_zotero(self) -> CheckResult: ...
    def check_bbt(self) -> CheckResult: ...
    def check_json(self) -> CheckResult: ...

# Standalone function
def _find_vault() -> Path | None: ...
```

IMPORTANT: The Textual-based App class (SetupWizardApp) and screen classes are best tested via integration / visual testing and are NOT the focus of this task. Focus on standalonetestable logic: AGENT_CONFIGS, EnvChecker methods, CheckResult, and _find_vault().
</interfaces>

<tasks>

<task type="auto">
  <name>Task 1: Create test_setup_wizard.py — wizard standalone function tests</name>
  <files>tests/test_setup_wizard.py</files>
  <action>
Create `tests/test_setup_wizard.py` with 5 test classes. These tests validate standalone functions and classes from `setup_wizard.py` without requiring the Textual UI framework.

```python
"""Unit tests for PaperForge Lite setup wizard standalone functions.

Covers: AGENT_CONFIGS, EnvChecker (check_python, check_vault, check_dependencies,
get_exports_dir, _find_zotero), CheckResult, and _find_vault.

Test strategy:
- Textual App and StepScreen classes require a running terminal and are NOT tested here.
- Focus on standalone detection/validation logic that can run without Textual.
- Use monkeypatch for environment/platform-dependent tests.
"""

from __future__ import annotations

import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

# Import standalone functions and classes (not Textual-dependent)
from setup_wizard import AGENT_CONFIGS, CheckResult, EnvChecker, _find_vault


# ===================================================================
# AGENT_CONFIGS
# ===================================================================


class TestAgentConfigs:
    """Agent platform configuration completeness."""

    def test_has_all_expected_agents(self) -> None:
        """Verify all 8 agent platforms are configured."""
        expected_agents = {"opencode", "cursor", "claude", "windsurf", "github_copilot", "cline", "augment", "trae"}
        assert set(AGENT_CONFIGS.keys()) == expected_agents

    def test_each_agent_has_name_and_skill_dir(self) -> None:
        """Every agent config must have name and skill_dir."""
        for key, cfg in AGENT_CONFIGS.items():
            assert "name" in cfg, f"{key} missing name"
            assert "skill_dir" in cfg, f"{key} missing skill_dir"
            assert isinstance(cfg["name"], str) and cfg["name"], f"{key} has empty name"
            assert isinstance(cfg["skill_dir"], str) and cfg["skill_dir"], f"{key} has empty skill_dir"

    def test_opencode_skill_dir_is_dot_opencode(self) -> None:
        """OpenCode skill dir must be .opencode/skills."""
        assert AGENT_CONFIGS["opencode"]["skill_dir"] == ".opencode/skills"

    def test_cursor_skill_dir_is_dot_cursor(self) -> None:
        assert AGENT_CONFIGS["cursor"]["skill_dir"] == ".cursor/skills"

    def test_claude_skill_dir_is_dot_claude(self) -> None:
        assert AGENT_CONFIGS["claude"]["skill_dir"] == ".claude/skills"

    def test_opencode_has_command_dir(self) -> None:
        """OpenCode is the only agent with command_dir."""
        assert AGENT_CONFIGS["opencode"].get("command_dir") == ".opencode/command"


# ===================================================================
# CheckResult
# ===================================================================


class TestCheckResult:
    """CheckResult data class."""

    def test_default_state(self) -> None:
        r = CheckResult("Test Check")
        assert r.name == "Test Check"
        assert r.passed is False
        assert r.detail == ""
        assert r.action_required is False

    def test_can_set_passed(self) -> None:
        r = CheckResult("X")
        r.passed = True
        assert r.passed is True

    def test_can_set_detail(self) -> None:
        r = CheckResult("X")
        r.detail = "Some detail"
        assert r.detail == "Some detail"

    def test_can_set_action_required(self) -> None:
        r = CheckResult("X")
        r.action_required = True
        assert r.action_required is True


# ===================================================================
# EnvChecker
# ===================================================================


class TestEnvCheckerInit:
    """EnvChecker initialization."""

    def test_init_with_vault_path(self, tmp_path: Path) -> None:
        checker = EnvChecker(tmp_path)
        assert checker.vault == tmp_path
        assert checker.system_dir == "99_System"
        assert set(checker.results.keys()) == {"python", "vault", "zotero", "bbt", "json"}

    def test_results_are_checkresult_instances(self, tmp_path: Path) -> None:
        checker = EnvChecker(tmp_path)
        for key, result in checker.results.items():
            assert isinstance(result, CheckResult), f"{key} is not CheckResult"
            assert result.name, f"{key} has empty name"

    def test_get_exports_dir(self, tmp_path: Path) -> None:
        checker = EnvChecker(tmp_path)
        expected = tmp_path / "99_System" / "PaperForge" / "exports"
        assert checker.get_exports_dir() == expected

    def test_get_exports_dir_custom_system_dir(self, tmp_path: Path) -> None:
        checker = EnvChecker(tmp_path)
        checker.system_dir = "CustomSystem"
        expected = tmp_path / "CustomSystem" / "PaperForge" / "exports"
        assert checker.get_exports_dir() == expected


class TestEnvCheckerCheckPython:
    """EnvChecker.check_python()"""

    def test_python_version_passes(self, tmp_path: Path) -> None:
        checker = EnvChecker(tmp_path)
        result = checker.check_python()
        # Should pass on any modern Python (>= 3.8)
        assert result.passed is True
        assert "Python" in result.detail

    def test_python_version_in_detail(self, tmp_path: Path) -> None:
        checker = EnvChecker(tmp_path)
        result = checker.check_python()
        v = sys.version_info
        assert f"{v.major}.{v.minor}" in result.detail


class TestEnvCheckerCheckVault:
    """EnvChecker.check_vault()"""

    def test_missing_directories_fails(self, tmp_path: Path) -> None:
        """Empty vault should fail vault structure check."""
        checker = EnvChecker(tmp_path)
        result = checker.check_vault()
        assert result.passed is False
        assert result.action_required is True
        assert "缺少" in result.detail or "missing" in result.detail.lower() or "missing" in result.detail

    def test_existing_directories_passes(self, tmp_path: Path) -> None:
        """Vault with correct structure should pass."""
        (tmp_path / "99_System" / "PaperForge" / "exports").mkdir(parents=True)
        (tmp_path / "99_System" / "PaperForge" / "ocr").mkdir(parents=True)
        checker = EnvChecker(tmp_path)
        result = checker.check_vault()
        assert result.passed is True

    def test_partial_directories_fails(self, tmp_path: Path) -> None:
        """Vault with only one required dir should fail."""
        (tmp_path / "99_System" / "PaperForge" / "exports").mkdir(parents=True)
        checker = EnvChecker(tmp_path)
        result = checker.check_vault()
        assert result.passed is False


class TestEnvCheckerCheckDependencies:
    """EnvChecker.check_dependencies()"""

    def test_installed_deps_pass(self, tmp_path: Path) -> None:
        """If requests/pymupdf/PIL are installed, this should pass (in CI/test env)."""
        checker = EnvChecker(tmp_path)
        result = checker.check_dependencies()
        # In CI these should be installed since they're test deps
        # Accept either pass or fail — what matters is the result is valid
        assert isinstance(result.passed, bool)
        assert isinstance(result.detail, str)

    def test_missing_dep_fails(self, tmp_path: Path) -> None:
        """Simulate a missing import to verify failure path."""
        checker = EnvChecker(tmp_path)
        with patch("builtins.__import__", side_effect=ImportError("missing")):
            result = checker.check_dependencies()
            assert result.passed is False
            assert result.action_required is True


class TestEnvCheckerFindZotero:
    """EnvChecker._find_zotero() — platform-dependent search."""

    def test_manual_path_is_used_when_valid(self, tmp_path: Path) -> None:
        """If a manual path is provided and exists, it should be returned."""
        checker = EnvChecker(tmp_path)
        fake_zotero = tmp_path / "zotero.exe"
        fake_zotero.write_text("fake binary", encoding="utf-8")
        result = checker._find_zotero(manual_path=fake_zotero)
        assert result == fake_zotero

    def test_manual_path_none_falls_through(self, tmp_path: Path) -> None:
        """When manual_path is None, search logic runs (likely returns None on CI)."""
        checker = EnvChecker(tmp_path)
        result = checker._find_zotero()
        # On CI there's no Zotero, so result should be None
        # This test just validates the fallthrough doesn't crash
        if result is not None:
            assert result.exists()


class TestFindVault:
    """_find_vault() — standalone vault discovery."""

    def test_finds_vault_with_paperforge_json(self, tmp_path: Path) -> None:
        """Verify _find_vault detects paperforge.json in current or parent dirs."""
        vault = tmp_path / "my_vault"
        vault.mkdir(parents=True)
        (vault / "paperforge.json").write_text("{}", encoding="utf-8")

        with patch("setup_wizard.Path.cwd", return_value=vault):
            result = _find_vault()
            assert result is not None
            assert result == vault

    def test_no_paperforge_json_returns_none(self, tmp_path: Path) -> None:
        """Verify _find_vault returns None when no paperforge.json exists."""
        with patch("setup_wizard.Path.cwd", return_value=tmp_path):
            result = _find_vault()
            assert result is None

    def test_finds_in_parent_directory(self, tmp_path: Path) -> None:
        """Verify _find_vault walks up parent directories."""
        parent = tmp_path / "parent"
        parent.mkdir()
        (parent / "paperforge.json").write_text("{}", encoding="utf-8")
        child = parent / "subdir" / "deeper"
        child.mkdir(parents=True)

        with patch("setup_wizard.Path.cwd", return_value=child):
            result = _find_vault()
            assert result is not None
            # Should resolve to the same path
            assert result.resolve() == parent.resolve()


class TestEnvCheckerCheckJson:
    """EnvChecker.check_json() — JSON export detection."""

    def test_no_exports_dir_fails(self, tmp_path: Path) -> None:
        checker = EnvChecker(tmp_path)
        result = checker.check_json()
        assert result.passed is False

    def test_valid_json_exports_pass(self, tmp_path: Path) -> None:
        imports json
        exports_dir = tmp_path / "99_System" / "PaperForge" / "exports"
        exports_dir.mkdir(parents=True)
        (exports_dir / "test.json").write_text(
            json.dumps({"items": [{"key": "TEST"}]}),
            encoding="utf-8",
        )
        checker = EnvChecker(tmp_path)
        result = checker.check_json()
        assert result.passed is True
        assert "JSON" in result.detail

    def test_invalid_json_fails(self, tmp_path: Path) -> None:
        exports_dir = tmp_path / "99_System" / "PaperForge" / "exports"
        exports_dir.mkdir(parents=True)
        (exports_dir / "bad.json").write_text("{invalid}", encoding="utf-8")
        checker = EnvChecker(tmp_path)
        result = checker.check_json()
        assert result.passed is False

    def test_empty_exports_fails(self, tmp_path: Path) -> None:
        exports_dir = tmp_path / "99_System" / "PaperForge" / "exports"
        exports_dir.mkdir(parents=True)
        checker = EnvChecker(tmp_path)
        result = checker.check_json()
        assert result.passed is False
```

Implementation notes:
- The test file imports from `setup_wizard` directly. Ensure the repo root is importable (same pattern as test_smoke.py lines 26-28).
- Add the `sys.path.insert(0, ...)` at module level in the test file to ensure setup_wizard is importable.
- Use `from setup_wizard import AGENT_CONFIGS, CheckResult, EnvChecker, _find_vault` — these don't depend on Textual.
- `_find_vault` uses `Path.cwd()` — test with `monkeypatch.setattr` or `unittest.mock.patch` to override.
- Do NOT test Textual UI components (SetupWizardApp, StepScreen subclasses) — those require a terminal.
- All tests use `tmp_path` fixture for temporary directories.
- The test for `check_bbt` is omitted since it requires real Zotero profile detection logic that is inherently environment-dependent.

IMPORTANT: Add the `sys.path` insertion at the top of the test file (same pattern as test_smoke.py):
```python
REPO_ROOT = Path(__file__).resolve().parent.parent
if str(REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(REPO_ROOT))
```
  </action>
  <verify>
    <automated>python -m pytest tests/test_setup_wizard.py -x -q</automated>
  </verify>
  <done>All setup wizard tests pass: AGENT_CONFIGS validated, EnvChecker detects correct states, CheckResult tracks state correctly, _find_vault resolves paths correctly</done>
</task>

</tasks>

<verification>
```bash
python -m pytest tests/test_setup_wizard.py -x -q
python -m pytest tests/ -x -q  # Verify 0 regressions
```
</verification>

<success_criteria>
1. `tests/test_setup_wizard.py` exists with 5+ test classes
2. Agent platform configs validated: all 8 agents present, correct skill_dir values
3. EnvChecker validation: check_python, check_vault (happy + sad paths), check_dependencies (with import error simulation), check_json
4. CheckResult correctly tracks passed/detail/action_required
5. _find_vault correctly discovers paperforge.json in current/parent dirs
6. All tests pass; no regressions in existing suite
</success_criteria>

<output>
After completion, create `.planning/phases/19-testing/19-03-SUMMARY.md`
</output>
