---
phase: 01-core-hardening
plan: 04
type: execute
wave: 2
depends_on: [01, 02, 03]
files_modified:
  - pyproject.toml
  - tests/plugins/test_event_bus.py
  - tests/plugins/test_git_plugin.py
  - tests/services/test_check.py
  - tests/services/test_reweave.py
  - tests/services/test_session.py
  - tests/mcp/test_tools_impl.py
autonomous: true
requirements: [HARD-05]

must_haves:
  truths:
    - "plugins/* is removed from coverage omit in pyproject.toml"
    - "services/session.py is removed from coverage omit in pyproject.toml"
    - "services/reweave.py is removed from coverage omit in pyproject.toml"
    - "services/check.py is removed from coverage omit in pyproject.toml"
    - "mcp/* is removed from coverage omit in pyproject.toml"
    - "EventBus dead_letter state machine path is tested"
    - "Full test suite passes with no coverage exclusions (except __main__.py)"
  artifacts:
    - path: "pyproject.toml"
      provides: "Coverage omit list reduced to only __main__.py"
      contains: "__main__.py"
    - path: "tests/plugins/test_event_bus.py"
      provides: "EventBus state machine tests including dead_letter path"
      contains: "dead_letter"
    - path: "tests/mcp/test_tools_impl.py"
      provides: "MCP _impl function tests"
      contains: "_impl"
  key_links:
    - from: "pyproject.toml"
      to: "src/ztlctl/services/session.py"
      via: "coverage omit removal"
      pattern: "omit"
---

<objective>
Close all test coverage gaps by expanding tests for excluded services (session, reweave, check), plugins (EventBus, GitPlugin), and MCP _impl functions. Remove all entries from the pyproject.toml coverage omit list except __main__.py.

Purpose: HARD-05 requires that previously excluded services have test coverage and no longer appear in coverage exclusion config.
Output: Expanded test files, new MCP _impl tests, coverage omit list reduced. Full suite passes with overall coverage >= 80%.
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-core-hardening/01-RESEARCH.md
@.planning/codebase/CONCERNS.md

<interfaces>
<!-- Key types and contracts the executor needs. -->

From pyproject.toml coverage config:
```toml
[tool.coverage.run]
source = ["ztlctl"]
branch = true
omit = [
    "src/ztlctl/__main__.py",
    "src/ztlctl/mcp/*",
    "src/ztlctl/services/session.py",
    "src/ztlctl/services/reweave.py",
    "src/ztlctl/services/check.py",
    "src/ztlctl/plugins/*",
]

[tool.coverage.report]
fail_under = 80
```

Target: remove all omit entries EXCEPT __main__.py.

From src/ztlctl/plugins/event_bus.py:
```python
class EventBus:
    # WAL-backed async dispatch
    # States: pending -> completed | failed -> dead_letter (after max retries)
    def dispatch(self, event_type: str, payload: dict) -> None: ...
    def drain(self, timeout: float = 30.0) -> None: ...
    def shutdown(self) -> None: ...
```

From src/ztlctl/mcp/tools.py:
```python
# _impl functions are designed to be testable without mcp package
def create_note_impl(vault, title, ...) -> dict: ...
def search_impl(vault, query, ...) -> dict: ...
# etc.
```

PITFALL (from RESEARCH.md): Before removing a file from omit, run
`uv run pytest --cov=ztlctl.services.session --cov-report=term-missing`
to see actual coverage for that file alone. Only remove when >= 80%.
Strategy: Add tests FIRST for each file, verify coverage >= 80%, THEN remove from omit.
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Expand plugin and MCP tests, lift plugins/* and mcp/* from coverage omit</name>
  <files>tests/plugins/test_event_bus.py, tests/plugins/test_git_plugin.py, tests/mcp/test_tools_impl.py, pyproject.toml</files>
  <read_first>
    - src/ztlctl/plugins/event_bus.py (full file — understand state machine: pending, completed, failed, dead_letter, drain, shutdown)
    - src/ztlctl/plugins/builtins/git.py (batch mode vs immediate commit mode)
    - tests/plugins/test_event_bus.py (existing tests — identify gaps)
    - tests/plugins/test_git_plugin.py (existing tests — identify gaps)
    - src/ztlctl/mcp/tools.py (find all _impl functions, their signatures and return types)
    - tests/mcp/ (list existing test files, if any)
    - pyproject.toml (current coverage omit list, lines 122-129)
  </read_first>
  <action>
    **1. Expand tests/plugins/test_event_bus.py:**

    Add tests for the following untested paths:
    - `test_event_transitions_to_failed_on_handler_error`: Dispatch an event with a handler that raises. Verify the event WAL record transitions to `failed` state.
    - `test_event_transitions_to_dead_letter_after_max_retries`: Dispatch an event with a handler that always raises. Drain multiple times until retry count is exhausted. Verify final state is `dead_letter`.
    - `test_drain_timeout_handles_slow_handler`: Register a handler that sleeps longer than the drain timeout. Verify drain completes without hanging, event stays in `failed` state.
    - `test_sync_mode_dispatch`: Create EventBus in sync mode. Dispatch event. Verify handler is called synchronously (no ThreadPoolExecutor).
    - `test_shutdown_after_drain`: Verify `shutdown()` can be called after `drain()` without error.

    **2. Expand tests/plugins/test_git_plugin.py:**

    Add tests for:
    - `test_git_plugin_batch_mode`: Verify batch mode defers commits (does not call git commit per-event).
    - `test_git_plugin_immediate_mode`: Verify immediate mode calls git commit per-event.
    - (The sanitization test was already added in Plan 02 Task 2.)

    **3. Create tests/mcp/test_tools_impl.py:**

    Create a new test file that tests the most critical `_impl` functions WITHOUT importing the `mcp` package. The `_impl` functions accept a `Vault` directly. Use the existing test vault fixture from conftest.py.

    Test at minimum:
    - `test_create_note_impl`: Call `create_note_impl()` with valid args, verify it returns a dict with expected keys.
    - `test_search_impl`: Call `search_impl()` with a query, verify it returns results dict.
    - `test_get_note_impl`: Create a note, then call `get_note_impl()` by ID, verify returned data matches.

    Import pattern: `from ztlctl.mcp.tools import create_note_impl, search_impl, get_note_impl`
    If the mcp package is not installed, these imports should still work because `_impl` functions are designed to be independent.

    **4. Verify coverage and remove from omit:**

    Run per-file coverage checks:
    ```bash
    uv run pytest --cov=ztlctl.plugins --cov-report=term-missing tests/plugins/ -q
    uv run pytest --cov=ztlctl.mcp --cov-report=term-missing tests/mcp/ -q
    ```

    If both report >= 80% coverage, remove from pyproject.toml:
    - Remove `"src/ztlctl/mcp/*",` from omit list
    - Remove `"src/ztlctl/plugins/*",` from omit list

    If any file is below 80%, add more targeted tests for uncovered lines before removing.
  </action>
  <verify>
    <automated>uv run pytest tests/plugins/ tests/mcp/ -x -v && uv run pytest --cov=ztlctl.plugins --cov-report=term-missing --cov-fail-under=80 tests/plugins/ -q</automated>
  </verify>
  <acceptance_criteria>
    - tests/plugins/test_event_bus.py contains `def test_event_transitions_to_dead_letter`
    - tests/plugins/test_event_bus.py contains `dead_letter`
    - tests/mcp/test_tools_impl.py exists and contains `def test_create_note_impl`
    - pyproject.toml coverage omit list does NOT contain `"src/ztlctl/plugins/*"`
    - pyproject.toml coverage omit list does NOT contain `"src/ztlctl/mcp/*"`
    - `uv run pytest tests/plugins/ -x -v` exits 0
    - `uv run pytest tests/mcp/ -x -v` exits 0
  </acceptance_criteria>
  <done>Plugin tests expanded (EventBus dead_letter path, GitPlugin modes). MCP _impl tests created. plugins/* and mcp/* removed from coverage omit. Coverage >= 80% for both.</done>
</task>

<task type="auto">
  <name>Task 2: Expand service tests, lift session/reweave/check from coverage omit</name>
  <files>tests/services/test_check.py, tests/services/test_reweave.py, tests/services/test_session.py, pyproject.toml</files>
  <read_first>
    - src/ztlctl/services/session.py (full file — start, close, reopen, enrichment pipeline)
    - src/ztlctl/services/reweave.py (full file — DISCOVER, SCORE, FILTER, CONNECT stages)
    - src/ztlctl/services/check.py (full file — 4-category scan, backup/restore, rebuild, fix)
    - tests/services/test_session.py (existing tests — identify coverage gaps)
    - tests/services/test_reweave.py (existing tests — identify coverage gaps)
    - tests/services/test_check.py (existing tests — identify coverage gaps)
    - tests/conftest.py (available fixtures — vault, populated vault, etc.)
  </read_first>
  <action>
    **Strategy: Incremental per-file lift.** For each of the 3 services:
    1. Run `uv run pytest --cov=ztlctl.services.{service} --cov-report=term-missing tests/services/test_{service}.py -q` to see current coverage.
    2. Identify uncovered branches/lines.
    3. Add tests targeting those specific paths.
    4. Re-run coverage check. When >= 80%, remove from omit.

    **For tests/services/test_check.py, expand with:**

    - `test_check_scan_detects_orphan_db_rows`: Create a vault with DB rows that have no corresponding file. Run `check()`. Verify the result contains a warning about orphan rows.
    - `test_check_fix_removes_orphan_db_rows`: Run `fix(level="safe")` on a vault with orphan DB rows. Verify they are removed.
    - `test_backup_retention_days_prunes_old_backups`: Create backups with old timestamps. Run `_prune_backups()`. Verify only backups within retention window remain. (Tests the HARD-01 fix from Plan 02.)
    - `test_rebuild_completes_with_content`: Create several content items. Run `rebuild()`. Verify FTS5 and node table are consistent.
    - `test_rollback_restores_previous_state`: Create content, take backup, create more content, rollback. Verify state matches backup.
    - `test_fts_divergence_detection`: After rollback, run check scan. Verify it detects FTS5 vs nodes count mismatch (if that check was added).

    **For tests/services/test_reweave.py, expand with:**

    - `test_reweave_discovers_candidates`: Create notes with related content. Run reweave. Verify candidates are discovered.
    - `test_reweave_scores_candidates_batch`: Verify the batch FTS5 scoring path works (tests the HARD-06 fix from Plan 03).
    - `test_reweave_filters_already_linked`: Create a note already linked to a candidate. Run reweave. Verify the already-linked note is filtered out.
    - `test_reweave_connects_high_score_candidates`: Run reweave on notes with high similarity. Verify new links are created.
    - `test_reweave_prune_removes_weak_links`: Create links with weak signals. Run prune. Verify weak links are removed.
    - `test_reweave_undo_restores_previous_links`: Run reweave, then undo. Verify links are restored.

    **For tests/services/test_session.py, expand with:**

    - `test_session_start_creates_log_entry`: Start a session. Verify a session log is created in the DB.
    - `test_session_close_runs_enrichment`: Start and close a session. Verify enrichment pipeline ran (reweave + orphan sweep + integrity check).
    - `test_session_reopen_existing`: Start, close, then reopen. Verify session is active again.
    - `test_session_close_with_no_active_session`: Call close with no active session. Verify returns ServiceResult with ok=False.
    - `test_session_start_while_active`: Start a session, then try to start another. Verify appropriate error.

    **After all test expansions, update pyproject.toml:**

    Remove from omit list:
    - `"src/ztlctl/services/session.py",`
    - `"src/ztlctl/services/reweave.py",`
    - `"src/ztlctl/services/check.py",`

    Final pyproject.toml omit should be:
    ```toml
    omit = [
        "src/ztlctl/__main__.py",
    ]
    ```

    Run full suite: `uv run pytest` to verify overall coverage >= 80%.
  </action>
  <verify>
    <automated>uv run pytest tests/services/test_check.py tests/services/test_reweave.py tests/services/test_session.py -x -v && uv run pytest --cov-fail-under=80 -q</automated>
  </verify>
  <acceptance_criteria>
    - tests/services/test_check.py contains `def test_backup_retention_days_prunes_old_backups`
    - tests/services/test_check.py contains `def test_rebuild_completes_with_content`
    - tests/services/test_reweave.py contains `def test_reweave_discovers_candidates`
    - tests/services/test_session.py contains `def test_session_start_creates_log_entry`
    - tests/services/test_session.py contains `def test_session_close_runs_enrichment`
    - pyproject.toml coverage omit list does NOT contain `session.py`
    - pyproject.toml coverage omit list does NOT contain `reweave.py`
    - pyproject.toml coverage omit list does NOT contain `check.py`
    - pyproject.toml coverage omit contains ONLY `"src/ztlctl/__main__.py"`
    - `uv run pytest --cov-fail-under=80 -q` exits 0 (overall coverage >= 80%)
  </acceptance_criteria>
  <done>Session, reweave, and check service tests expanded. All coverage exclusions removed except __main__.py. Overall test coverage >= 80%. Full suite passes.</done>
</task>

</tasks>

<verification>
- `uv run pytest -x -q` — full suite passes
- `uv run pytest --cov-fail-under=80 -q` — overall coverage >= 80%
- pyproject.toml omit list contains only `__main__.py`
- `uv run ruff check .` — no lint issues
- `uv run mypy src/ --strict` — no type errors
</verification>

<success_criteria>
- All 5 coverage exclusions removed from pyproject.toml (session.py, reweave.py, check.py, plugins/*, mcp/*)
- Only __main__.py remains in omit
- Overall test coverage >= 80% (fail_under threshold)
- New tests cover: EventBus dead_letter path, GitPlugin modes, MCP _impl functions, session lifecycle, reweave pipeline stages, check scan/fix/rebuild/rollback
</success_criteria>

<output>
After completion, create `.planning/phases/01-core-hardening/01-04-SUMMARY.md`
</output>
