---
phase: 53-plugin-e2e
plan: 002
type: execute
wave: 1
depends_on: ["52-001", "52-002"]
files_modified:
  - tests/e2e/conftest.py
  - tests/e2e/test_sync_pipeline.py
  - tests/e2e/test_ocr_e2e.py
  - tests/e2e/test_status_doctor_repair.py
  - tests/e2e/test_multi_domain_sync.py
autonomous: true
requirements: [E2E-01, E2E-02, E2E-03, E2E-04, E2E-05]

must_haves:
  truths:
    - "Temp vault fixture produces a disposable Vault with config (paperforge.json), directories, mock Zotero data, and mock OCR state — usable by all E2E tests without external dependencies"
    - "Full sync workflow test: BBT JSON -> formal notes -> canonical index -> Base views completes in temp vault without external Zotero or OCR API calls"
    - "Multi-domain sync test: papers from orthopedic and sports_medicine collections produce domain-separated formal notes and index entries — each domain gets its own subdirectory under Literature/"
    - "OCR E2E test: mock PaddleOCR backend via `responses` HTTP interception processes a do_ocr: true paper through pending -> processing -> done states in temp vault"
    - "status/doctor/repair CLI commands run correctly in temp vault with known state and produce expected structured output"
  artifacts:
    - path: "tests/e2e/conftest.py"
      provides: "Temp vault fixture (session-scoped golden vault + per-test fast clone), mock OCR backend fixture, CLI invoker"
      min_lines: 80
    - path: "tests/e2e/test_sync_pipeline.py"
      provides: "Full pipeline test: BBT JSON -> formal notes -> canonical index -> Base views in temp vault"
      min_lines: 80
    - path: "tests/e2e/test_ocr_e2e.py"
      provides: "OCR E2E test: mock PaddleOCR intercepts HTTP, processes do_ocr:true through pending->processing->done"
      min_lines: 60
    - path: "tests/e2e/test_status_doctor_repair.py"
      provides: "E2E tests for status/doctor/repair CLI commands in known-state temp vault"
      min_lines: 60
    - path: "tests/e2e/test_multi_domain_sync.py"
      provides: "Multi-domain sync test: multiple Zotero collections sync to separate domain directories"
      min_lines: 50
  key_links:
    - from: "tests/e2e/conftest.py"
      to: "fixtures/vault_builder.py"
      via: "import VaultBuilder, build temp vaults at minimal/standard/full levels"
      pattern: "from fixtures.vault_builder import"
    - from: "tests/e2e/conftest.py"
      to: "fixtures/ocr/mock_ocr_backend.py"
      via: "import mock helpers for OCR E2E tests"
      pattern: "mock_ocr_backend"
    - from: "tests/e2e/test_sync_pipeline.py"
      to: "fixtures/zotero/orthopedic.json"
      via: "VaultBuilder copies exports -> CLI sync processes -> formal notes appear in Literature/orthopedic/"
      pattern: "orthopedic"
    - from: "tests/e2e/test_ocr_e2e.py"
      to: "responses library"
      via: "mock_ocr_backend fixture uses responses.RequestsMock to intercept PaddleOCR HTTP calls"
      pattern: "responses"

---

<objective>
Build temp vault end-to-end tests (L4) covering sync, OCR, status, doctor, and repair workflows with mock PaddleOCR backend, using the Phase 52 golden datasets and vault builder.

Purpose: Full pipeline workflows must be validated in disposable temp vaults before release. These E2E tests run without any external dependencies (no Zotero, no real PaddleOCR API, no Obsidian) — using fixtures, mocks, and subprocess isolation.

Output:
- tests/e2e/conftest.py (temp vault fixture, mock OCR fixture, CLI invoker)
- tests/e2e/test_sync_pipeline.py (full sync pipeline verification)
- tests/e2e/test_ocr_e2e.py (OCR E2E with mock backend)
- tests/e2e/test_status_doctor_repair.py (status/doctor/repair in temp vault)
- tests/e2e/test_multi_domain_sync.py (multi-domain sync verification)
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/53-doctor-verdict-surface/53-CONTEXT.md
@.planning/phases/53-doctor-verdict-surface/53-001-PLAN.md

<interfaces>
**Reusable imports from Phase 52:**

fixtures/vault_builder.py:
```python
class VaultBuilder:
    def build(self, level: str = "minimal") -> Path
    # level "minimal": config + dirs + .env
    # level "standard": minimal + BBT exports + mock PDFs + Zotero storage
    # level "full": standard + OCR fixtures + formal notes + canonical index
```

fixtures/ocr/mock_ocr_backend.py:
```python
def mock_ocr_success() -> responses.RequestsMock  # submit -> poll -> result
def mock_ocr_pending() -> responses.RequestsMock   # submit -> poll processing
def mock_ocr_error(status=401) -> responses.RequestsMock
def mock_ocr_timeout() -> responses.RequestsMock    # queued forever
```

tests/cli/conftest.py (existing pattern to follow):
```python
@pytest.fixture
def cli_invoker(vault_builder):
    # Returns function: cli_invoker(args, vault_level, input_text, env)
    # Creates temp vault, runs `python -m paperforge --vault <vault> <args>`
    # Returns subprocess.CompletedProcess
```

**Available Zotero JSON fixtures:**
- orthopedic.json — single paper, domain=orthopedic, storage: path
- sports_medicine.json — second domain (CJK name), storage: path
- cjk_content.json — full CJK title/authors
- storage_prefix.json — storage: prefix path format

**Available OCR fixtures:**
- paddleocr_submit.json — 202 with mock-job-001
- paddleocr_poll_pending.json — processing at 45%
- paddleocr_poll_done.json — completed with result_url
- paddleocr_result.json — 2 pages markdown + 1 figure + 1 table
- paddleocr_error.json — 401 authentication_failed
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create E2E test infrastructure — temp vault conftest with fixtures</name>
  <files>
    tests/e2e/conftest.py (NEW)
  </files>
  <action>
    Create `tests/e2e/conftest.py` with three fixtures:

    1. **`vault_builder` fixture** — session-scoped VaultBuilder instance:
       ```python
       @pytest.fixture(scope="session")
       def vault_builder():
           from fixtures.vault_builder import VaultBuilder
           return VaultBuilder()
       ```

    2. **`temp_vault` fixture** — function-scoped temp vault at "standard" level:
       - Uses `tmp_path` from pytest to create a unique per-test temp directory
       - Calls `VaultBuilder().build("standard")` to populate with:
         - paperforge.json (clean directory config)
         - All required subdirectories (System/PaperForge/exports/ etc.)
         - .env with mock PADDLEOCR_API_TOKEN
         - Zotero JSON exports (orthopedic.json, sports_medicine.json, cjk_content.json, storage_prefix.json)
         - Mock PDFs in Zotero storage directories
       - Yields the vault Path
       - Teardown: retry-safe shutil.rmtree with os.chmod to handle Windows file locking:
         - `_force_rmtree(vault)` that walks tree, chmods all files/dirs to S_IWRITE, then rmtree

    3. **`e2e_cli_invoker` fixture** — function-scoped CLI invoker for E2E tests:
       - Builds command: `[sys.executable, "-m", "paperforge", "--vault", str(vault)] + args`
       - Runs with `subprocess.run(..., capture_output=True, text=True, timeout=120)`
       - Returns `subprocess.CompletedProcess`
       - Accepts optional env overrides

    4. **`mock_ocr_backend` fixture** — function-scoped:
       - Accepts optional `state` parameter: "success", "pending", "error_401", "timeout"
       - Returns a factory function producing `responses.RequestsMock` context managers
       - Delegates to fixtures.ocr.mock_ocr_backend module

    **IMPORTANT Windows safety:** Inline _force_rmtree helper in conftest:
    ```python
    import stat, shutil, os
    def _force_rmtree(root):
        for dirpath, dirnames, filenames in os.walk(root):
            for f in filenames:
                try: os.chmod(os.path.join(dirpath, f), stat.S_IWRITE)
                except: pass
            for d in dirnames:
                try: os.chmod(os.path.join(dirpath, d), stat.S_IWRITE)
                except: pass
        shutil.rmtree(root, ignore_errors=True)
    ```
  </action>
  <verify>
    <automated>python -c "import importlib.util; import sys; sys.path.insert(0,'.'); spec=importlib.util.spec_from_file_location('x','tests/e2e/conftest.py'); assert spec is not None; print('conftest.py exists')"</automated>
  </verify>
  <done>
    - `tests/e2e/conftest.py` exists with vault_builder, temp_vault, e2e_cli_invoker, mock_ocr_backend fixtures
    - Python can import it without syntax errors
    - _force_rmtree helper handles Windows file locking
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Full sync pipeline E2E test + multi-domain sync test</name>
  <files>
    tests/e2e/test_sync_pipeline.py (NEW)
    tests/e2e/test_multi_domain_sync.py (NEW)
  </files>
  <behavior>
    - test_sync_pipeline: running `paperforge sync` in a temp vault with orthopedic.json creates a formal note at Literature/orthopedic/<key> - <Title>.md, generates canonical index at System/PaperForge/indexes/formal-library.json, and renders .base files in Bases/
    - test_multi_domain_sync: separate orthopedic and sports_medicine collections sync to separate domain subdirectories under Literature/; both appear in the canonical index with correct domain values; index contains 2+ entries
  </behavior>
  <action>
    1. Create `tests/e2e/test_sync_pipeline.py`:
       - Fixtures: `temp_vault`, `e2e_cli_invoker`
       - Test `test_full_sync_pipeline`:
         a. Invoke: `invoke(["sync"])` in temp vault with orthopedic.json export
         b. Assert returncode == 0
         c. Assert vault structure: `Path(vault) / "Resources/Literature/orthopedic"` exists
         d. Assert formal note file exists with glob: `*FIXT* - *.md` in the orthopedic dir
         e. Assert canonical index at `System/PaperForge/indexes/formal-library.json` exists
         f. Parse JSON index, assert:
            - index.items has at least 1 entry
            - entry.zotero_key matches fixture key
            - entry.domain == "orthopedic"
            - entry.note_path is non-empty and points to Literature/orthopedic/
         g. Assert .base files exist in Bases/ directory

    2. Create `tests/e2e/test_multi_domain_sync.py`:
       - Fixtures: `temp_vault` (with orthopedic AND sports_medicine exports), `e2e_cli_invoker`
       - Test `test_multi_domain_sync`:
         a. Invoke: `invoke(["sync"])`
         b. Assert returncode == 0
         c. Assert both domain dirs exist: `Literature/orthopedic/`, `Literature/sports_medicine/`
         d. Assert formal notes in both directories
         e. Parse index JSON, assert:
            - index.items has >= 2 entries
            - One entry has domain=="orthopedic"
            - One entry has domain=="sports_medicine"
            - Both have non-empty note_path fields pointing to correct domain dirs
         f. Verify sync is idempotent: run `invoke(["sync"])` again, assert returncode == 0, count items stays the same

    Both tests must use `pytest.mark.e2e` marker.
    Both use `subprocess.run` via the e2e_cli_invoker — no direct function calls.
  </action>
  <verify>
    <automated>python -m pytest tests/e2e/test_sync_pipeline.py tests/e2e/test_multi_domain_sync.py -v --timeout=120 2>&1</automated>
  </verify>
  <done>
    - `test_full_sync_pipeline` passes: BBT JSON -> formal notes -> canonical index -> Base views verified
    - `test_multi_domain_sync` passes: orthopedic + sports_medicine produce domain-separated notes and index entries
    - Sync is idempotent (second run produces same count)
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 3: OCR E2E test + status/doctor/repair E2E tests</name>
  <files>
    tests/e2e/test_ocr_e2e.py (NEW)
    tests/e2e/test_status_doctor_repair.py (NEW)
  </files>
  <behavior>
    - test_ocr_e2e: with mock PaddleOCR backend (responses library), a do_ocr: true paper transitions through pending -> processing -> done states. OCR output files appear in System/PaperForge/ocr/<key>/
    - test_status_doctor_repair: `paperforge status --json`, `paperforge doctor --json`, and `paperforge repair` commands produce correct structured output in a known-state temp vault
  </behavior>
  <action>
    1. Create `tests/e2e/test_ocr_e2e.py`:
       - Fixtures: `temp_vault` (using VaultBuilder "full" level with OCR fixtures), `e2e_cli_invoker`, `mock_ocr_backend`
       - Test `test_ocr_full_workflow`:
         a. Before sync: verify temp vault has a formal note with do_ocr: true already set (from "full" level vault)
         b. Run sync first: `invoke(["sync"])` to build index
         c. With `mock_ocr_backend("success")` as rsps:
            - Run `invoke(["ocr"])`
            - Assert returncode == 0
         d. After OCR: assert vault structure:
            - `System/PaperForge/ocr/<key>/meta.json` exists with `ocr_status: "done"`
            - `System/PaperForge/ocr/<key>/fulltext.md` exists and is non-empty
         d. Verify index was updated: parse formal-library.json, assert entry.ocr_status == "done"
       - Test `test_ocr_pending_state`:
         a. With `mock_ocr_backend("pending")`:
            - Run `invoke(["ocr", "--diagnose"])`
            - Assert returncode == 0 (diagnose should not fail)
            - Output shows pending/processing state info

    2. Create `tests/e2e/test_status_doctor_repair.py`:
       - Fixtures: `temp_vault` (standard level), `e2e_cli_invoker`
       - Test `test_status_json`:
         a. Run `invoke(["status", "--json"])`
         b. Assert returncode == 0
         c. Parse JSON stdout: assert "formal_notes" key exists, "version" key exists
         d. Assert JSON is valid and contains expected shape keys
       - Test `test_doctor_json`:
         a. Run `invoke(["doctor", "--json"])`
         b. Assert returncode == 0 or 1 (doctor can indicate issues)
         c. Parse JSON stdout: assert "ok" key exists (boolean), "checks" array exists
       - Test `test_repair_dry_run`:
         a. Run `invoke(["repair"])` without --fix (dry run)
         b. Assert returncode == 0
         c. Output contains repair findings or "no issues" summary

    All tests must use `pytest.mark.e2e` marker.
    All tests use `subprocess.run` via e2e_cli_invoker.
  </action>
  <verify>
    <automated>python -m pytest tests/e2e/test_ocr_e2e.py tests/e2e/test_status_doctor_repair.py -v --timeout=120 2>&1</automated>
  </verify>
  <done>
    - `test_ocr_full_workflow` passes: mock OCR completes, meta.json shows ocr_status: done, index updated
    - `test_ocr_pending_state` passes: diagnose command with mock pending returns correct state
    - `test_status_json` passes: returns valid JSON with formal_notes and version keys
    - `test_doctor_json` passes: returns valid JSON with ok and checks keys
    - `test_repair_dry_run` passes: dry run completes with findings summary
  </done>
</task>

</tasks>

<verification>
1. All 4 test files exist and are importable: `python -m pytest tests/e2e/ --collect-only -q`
2. All E2E tests pass: `python -m pytest tests/e2e/ -v --timeout=120 -m e2e`
3. No external network calls — verify with RESPONSES isolation (mock OCR backend must intercept all HTTP)
4. All temp vaults are cleaned up after tests (no orphan temp directories)
</verification>

<success_criteria>
1. E2E temp vault fixture produces disposable, repeatable test vaults — verified by 5 test files consuming it
2. Full sync pipeline (BBT JSON -> formal notes -> canonical index -> Base views) passes in temp vault
3. Multi-domain sync correctly separates orthopedic and sports_medicine collections
4. OCR pipeline with mock backend processes do_ocr:true paper through all states
5. Status/doctor/repair commands produce correct structured output in known vault state
6. All tests are marked `@pytest.mark.e2e` and run with `--timeout=120`
</success_criteria>

<output>
After completion, create `.planning/phases/53-doctor-verdict-surface/53-002-SUMMARY.md`
</output>
