---
phase: 18-cli-chunk-display
plan: "01"
type: tdd
wave: 1
depends_on: []
files_modified:
  - tests/search/test_formatter.py
autonomous: true
requirements:
  - CHUNK-02

must_haves:
  truths:
    - "format_result() returns a primary line containing path:start-end when start_line > 0"
    - "format_result() omits the line range when both start_line and end_line are 0 (legacy rows)"
    - "format_result() returns None for the preview when chunk_text is empty"
    - "format_result() truncates long chunk_text to 200 chars + '...' (203 total chars of content plus 4 indent spaces)"
    - "format_result() takes only the first line of chunk_text containing newlines"
    - "format_result() omits brackets entirely when construct_type is empty or None"
    - "format_result() produces relative paths using the provided cwd; falls back to absolute for out-of-tree paths"
  artifacts:
    - path: "tests/search/test_formatter.py"
      provides: "Failing RED tests for format_result()"
      contains: "format_result"
  key_links:
    - from: "tests/search/test_formatter.py"
      to: "src/corpus_analyzer/search/formatter.py"
      via: "from corpus_analyzer.search.formatter import format_result"
      pattern: "from corpus_analyzer.search.formatter import format_result"
---

<objective>
Write failing RED tests for the `format_result()` function that will be implemented in Plan 02.

Purpose: Establish the exact contract for grep/compiler-error style output before writing any implementation. Tests define the spec — format, edge cases, and fallback behaviour — so the GREEN phase has unambiguous targets.
Output: New test functions in `tests/search/test_formatter.py`; all new tests MUST fail (ImportError or AssertionError) before Plan 02 ships.
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/milestones/v2-ROADMAP.md
@.planning/STATE.md
@.planning/phases/18-cli-chunk-display/18-CONTEXT.md
@.planning/phases/18-cli-chunk-display/18-RESEARCH.md
@src/corpus_analyzer/search/formatter.py
@tests/search/test_formatter.py
</context>

<feature>
  <name>format_result() RED test suite</name>
  <files>
    tests/search/test_formatter.py
    src/corpus_analyzer/search/formatter.py  (NOT modified in this plan — only imported)
  </files>
  <behavior>
    The function under test: `format_result(result: dict[str, Any], cwd: Path) -> tuple[str, str | None]`

    Cases (input -> expected output):

    1. Full result with line range:
       input: file_path="/cwd/skills/foo.md", start_line=42, end_line=67, construct_type="skill", _relevance_score=0.021, chunk_text="Some chunk content"
       -> primary contains "skills/foo.md:42-67", "score:0.021", "[skill]"
       -> preview == "    Some chunk content"

    2. Legacy row (zero line range):
       input: start_line=0, end_line=0, chunk_text=""
       -> primary does NOT contain ":0-0"
       -> preview is None

    3. Empty construct type:
       input: construct_type=None (or "")
       -> primary does NOT contain "[]" or "[None]"

    4. Newlines in chunk_text:
       input: chunk_text="first line\nsecond line\nthird line"
       -> preview == "    first line"

    5. Truncation at 200 chars:
       input: chunk_text="x" * 250
       -> preview ends with "..."
       -> len(preview) == 207  (4 spaces + 200 chars + 3 dots)

    6. Path outside CWD (relative_to raises ValueError):
       input: file_path="/other/project/file.md", cwd=Path("/home/user/project")
       -> primary contains "/other/project/file.md" (absolute fallback)

    7. Score formatting — always 3 decimal places:
       input: _relevance_score=0.1
       -> primary contains "score:0.100"

    8. Rich markup escape — path containing brackets:
       input: file_path="/cwd/[deprecated]/foo.md"
       -> primary does NOT raise MarkupError; path is escaped

    9. chunk_text exactly 200 chars — no ellipsis:
       input: chunk_text="y" * 200
       -> preview == "    " + "y" * 200  (no trailing "...")

    10. start_line > 0 but end_line == 0 (edge case — treat as having a range):
        input: start_line=5, end_line=0
        -> primary contains ":5-0"  (not a legacy row — only BOTH zero triggers omission)
  </behavior>
  <implementation>
    Add test functions to `tests/search/test_formatter.py` alongside existing `extract_snippet` tests.
    Import: `from corpus_analyzer.search.formatter import format_result`
    All tests MUST fail at RED phase — `format_result` does not exist yet.
    Use `pytest.raises(ImportError)` is NOT the pattern — simply import at module level and let each test body assert the expected output. The ImportError at collection time is the RED signal.

    Test function names (use these exactly to match roadmap intent):
    - test_format_result_with_line_range
    - test_format_result_legacy_row_no_line_range
    - test_format_result_empty_construct_omits_brackets
    - test_format_result_chunk_text_newline_takes_first_line
    - test_format_result_truncates_at_200_chars
    - test_format_result_no_ellipsis_at_exactly_200_chars
    - test_format_result_path_outside_cwd_uses_absolute
    - test_format_result_score_always_3_decimal_places
    - test_format_result_rich_markup_escape_in_path
    - test_format_result_start_line_nonzero_end_line_zero_shows_range

    Do NOT remove or alter the existing `extract_snippet` tests — `format_result` is a new export alongside `extract_snippet`, not a replacement in the test file.
  </implementation>
</feature>

<tasks>

<task type="tdd">
  <name>Task 1: RED — Write failing tests for format_result()</name>
  <files>tests/search/test_formatter.py</files>
  <action>
    Add the import `from corpus_analyzer.search.formatter import format_result` at the top of `tests/search/test_formatter.py`, below the existing imports.

    Then add 10 test functions covering the cases described in the feature behaviour block above. Each test must:
    - Pass a `result: dict[str, Any]` with all required keys: `file_path`, `start_line`, `end_line`, `construct_type`, `_relevance_score`, `chunk_text`
    - Call `format_result(result, cwd=Path("/cwd"))` or an appropriate mock cwd
    - Assert the exact expected behaviour

    Key assertions to include per test:
    1. `test_format_result_with_line_range`: assert "skills/foo.md:42-67" in primary; assert "score:0.021" in primary; assert "[skill]" in primary; assert preview == "    Some chunk content"
    2. `test_format_result_legacy_row_no_line_range`: assert ":0-0" not in primary; assert preview is None
    3. `test_format_result_empty_construct_omits_brackets`: assert "[]" not in primary; assert "[None]" not in primary
    4. `test_format_result_chunk_text_newline_takes_first_line`: assert preview == "    first line"
    5. `test_format_result_truncates_at_200_chars`: assert preview is not None; assert preview.endswith("..."); assert len(preview) == 207
    6. `test_format_result_no_ellipsis_at_exactly_200_chars`: assert not preview.endswith("..."); assert len(preview) == 204  (4 indent + 200 chars)
    7. `test_format_result_path_outside_cwd_uses_absolute`: cwd=Path("/home/user/project"), file_path="/other/project/file.md" -> assert "/other/project/file.md" in primary
    8. `test_format_result_score_always_3_decimal_places`: _relevance_score=0.1 -> assert "score:0.100" in primary
    9. `test_format_result_rich_markup_escape_in_path`: file_path="/cwd/[deprecated]/foo.md" -> call format_result, assert no exception and "[deprecated]" in primary (escaped form)
    10. `test_format_result_start_line_nonzero_end_line_zero_shows_range`: start_line=5, end_line=0 -> assert ":5-0" in primary

    After writing, run: `uv run pytest tests/search/test_formatter.py -v`
    Expected RED outcome: collection error or ImportError on `format_result` (the function does not exist yet).
    If the tests unexpectedly PASS, something is wrong — raise an error in the summary.
  </action>
  <verify>
    <automated>cd /home/ollie/Development/Tools/vibe_coding/corpus-analyzer && uv run pytest tests/search/test_formatter.py -v 2>&1 | tail -20</automated>
    <manual>Confirm that test collection fails with ImportError or similar — format_result does not exist yet. Existing extract_snippet tests still collected correctly.</manual>
    <sampling_rate>run immediately after writing the tests</sampling_rate>
  </verify>
  <done>10 new test functions exist in tests/search/test_formatter.py; running the test file shows the new tests fail (ImportError or collection error on format_result); existing extract_snippet tests are unaffected</done>
</task>

</tasks>

<verification>
Run: `uv run pytest tests/search/test_formatter.py -v`
Expected: 10 new test functions collected and failing (ImportError on format_result import); existing tests unaffected.
</verification>

<success_criteria>
- `tests/search/test_formatter.py` contains 10 new test functions named `test_format_result_*`
- `from corpus_analyzer.search.formatter import format_result` is in the import block
- Running `uv run pytest tests/search/test_formatter.py` shows failures only for the new tests (not the pre-existing ones)
- The RED signal is confirmed — no implementation exists yet
</success_criteria>

<output>
After completion, create `.planning/phases/18-cli-chunk-display/18-01-SUMMARY.md`
</output>
