---
phase: 20-unit-integration-tests
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
  - plugins/claude-tts-companion/Tests/CompanionCoreTests/SubtitleChunkerTests.swift
  - plugins/claude-tts-companion/Tests/CompanionCoreTests/StreamingPipelineTests.swift
autonomous: true
requirements: [TEST-02, TEST-05]

must_haves:
  truths:
    - "SubtitleChunker correctly splits text into 2-line pages by pixel width"
    - "SubtitleChunker handles different font size names"
    - "SubtitleChunker break priority favors clause and phrase boundaries"
    - "Streaming pipeline produces correctly sequenced chunks from sentence-split text"
    - "Integration test verifies SentenceSplitter -> SubtitleChunker -> timing alignment chain"
  artifacts:
    - path: "plugins/claude-tts-companion/Tests/CompanionCoreTests/SubtitleChunkerTests.swift"
      provides: "Expanded unit tests for SubtitleChunker"
      min_lines: 60
    - path: "plugins/claude-tts-companion/Tests/CompanionCoreTests/StreamingPipelineTests.swift"
      provides: "Integration tests for streaming TTS pipeline"
      min_lines: 50
  key_links:
    - from: "SubtitleChunkerTests.swift"
      to: "SubtitleChunker.swift"
      via: "@testable import CompanionCore"
      pattern: "SubtitleChunker\\."
    - from: "StreamingPipelineTests.swift"
      to: "SentenceSplitter.swift, SubtitleChunker.swift, WordTimingAligner.swift"
      via: "@testable import CompanionCore"
      pattern: "SentenceSplitter|SubtitleChunker|WordTimingAligner"
---

<objective>
Expand SubtitleChunker test coverage and add streaming pipeline integration tests.

Purpose: SubtitleChunker already has basic tests from Phase 18 but needs coverage for break priority logic, font size variants, and edge cases (TEST-02). The streaming pipeline integration test verifies that SentenceSplitter -> SubtitleChunker -> WordTimingAligner produce correctly sequenced output end-to-end (TEST-05).

Output: Expanded SubtitleChunkerTests.swift and new StreamingPipelineTests.swift.
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@plugins/claude-tts-companion/Sources/CompanionCore/SubtitleChunker.swift
@plugins/claude-tts-companion/Sources/CompanionCore/SentenceSplitter.swift
@plugins/claude-tts-companion/Sources/CompanionCore/WordTimingAligner.swift
@plugins/claude-tts-companion/Tests/CompanionCoreTests/SubtitleChunkerTests.swift

<interfaces>
<!-- Existing SubtitleChunker API (all @MainActor static methods) -->

```swift
@MainActor
public enum SubtitleChunker {
    static func chunkIntoPages(text: String, fontSizeName: String = "medium") -> [SubtitlePage]
    static func availableLineWidth() -> CGFloat
    static func measureWidth(_ text: String, fontSizeName: String = "medium") -> CGFloat
    static func breakPriority(_ word: String) -> Int
}

public struct SubtitlePage {
    let words: [String]
    let startWordIndex: Int
    var wordCount: Int { words.count }
}
```

<!-- SentenceSplitter API -->

```swift
public struct SentenceSplitter: Sendable {
    public static func splitIntoSentences(_ text: String) -> [String]
}
```

<!-- WordTimingAligner relevant API -->

```swift
public struct WordTimingAligner: Sendable {
    static func extractWordTimings(text: String, audioDuration: TimeInterval) -> [TimeInterval]
}
```

</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Expand SubtitleChunker tests</name>
  <files>
    plugins/claude-tts-companion/Tests/CompanionCoreTests/SubtitleChunkerTests.swift
  </files>
  <read_first>
    plugins/claude-tts-companion/Tests/CompanionCoreTests/SubtitleChunkerTests.swift
    plugins/claude-tts-companion/Sources/CompanionCore/SubtitleChunker.swift
    plugins/claude-tts-companion/Sources/CompanionCore/SubtitleStyle.swift
  </read_first>
  <behavior>
    SubtitleChunker.breakPriority:
    - Returns 3 for words ending in comma, semicolon, colon, or em-dash
    - Returns 2 for conjunctions (and, or, but, for, nor, yet, so) and prepositions (in, on, at, to, of, by, with, from)
    - Returns 1 for all other words

    SubtitleChunker.chunkIntoPages with font sizes:
    - "small" font produces fewer pages than "large" for the same text (smaller font = more words per line)
    - "large" font produces more pages than "medium"
    - All font sizes produce valid pages (no empty pages, all words accounted for)

    SubtitleChunker page integrity:
    - Pages cover all input words with no gaps and no overlaps
    - startWordIndex of page N+1 equals startWordIndex of page N + page N wordCount
    - Single very long word (no spaces) produces a single page with that word

    SubtitleChunker.measureWidth:
    - Longer text measures wider than shorter text
    - "large" font size measures wider than "small" for the same text

  </behavior>
  <action>
    Read the existing SubtitleChunkerTests.swift (5 tests from Phase 18). ADD new tests to the existing file -- do NOT delete the existing tests.

    Add these new test methods to the existing `@Suite(.serialized) @MainActor struct SubtitleChunkerTests`:

    1. breakPriority tests:
       - `breakPriorityClauseBoundary`: "hello," -> 3, "world;" -> 3, "done:" -> 3
       - `breakPriorityPhraseWord`: "and" -> 2, "with" -> 2, "from" -> 2
       - `breakPriorityRegularWord`: "hello" -> 1, "Swift" -> 1

    2. Font size variant tests:
       - `fontSizeAffectsPageCount`: Generate 50-word text, chunk with "small" and "large", verify small produces fewer or equal pages
       - `allFontSizesProduceValidPages`: Chunk same text with "small", "medium", "large" -- all produce non-empty pages covering all words

    3. Page integrity tests:
       - `pagesCoverAllWordsContiguously`: For multi-page text, verify sum of all page wordCounts equals total word count, and startWordIndex values are contiguous
       - `singleLongWordProducesOnePage`: A 200-character word with no spaces -> 1 page, 1 word

    4. measureWidth test:
       - `measureWidthIncreasesWithLength`: "Hi" < "Hello world" < "Hello world this is a longer string"
       - `measureWidthIncreasesWithFontSize`: measureWidth("Hello", fontSizeName: "small") < measureWidth("Hello", fontSizeName: "large")

    Per TEST-02: verify page splitting, line breaks, and font size variants produce correct output.

  </action>
  <verify>
    <automated>cd plugins/claude-tts-companion && swift test --filter "SubtitleChunkerTests" 2>&1 | tail -20</automated>
  </verify>
  <acceptance_criteria>
    - SubtitleChunkerTests.swift has at least 12 test methods (5 existing + 7+ new)
    - `swift test --filter SubtitleChunkerTests` shows all tests passed
    - grep confirms tests for: "breakPriority", "fontSize", "measureWidth", "Contiguously"
  </acceptance_criteria>
  <done>SubtitleChunker tests cover break priority logic, font size variants, page integrity, and width measurement in addition to existing basic tests.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Streaming pipeline integration test</name>
  <files>
    plugins/claude-tts-companion/Tests/CompanionCoreTests/StreamingPipelineTests.swift
  </files>
  <read_first>
    plugins/claude-tts-companion/Sources/CompanionCore/SentenceSplitter.swift
    plugins/claude-tts-companion/Sources/CompanionCore/SubtitleChunker.swift
    plugins/claude-tts-companion/Sources/CompanionCore/WordTimingAligner.swift
  </read_first>
  <behavior>
    End-to-end pipeline (SentenceSplitter -> SubtitleChunker -> WordTimingAligner):
    - Multi-sentence text splits into sentences, each sentence chunks into pages, each page gets word timings
    - Total word count across all chunks equals total word count of original text
    - Word timings sum to the mock audio duration for each chunk
    - Chunks arrive in sentence order (first sentence's pages come before second sentence's pages)
    - Empty text produces no chunks
    - Single sentence produces at least one chunk with correct word count

    Sequencing verification:
    - Given "First sentence here. Second sentence there. Third one now."
    - SentenceSplitter produces 3 sentences
    - Each sentence chunked into pages
    - Each page's words get character-weighted timings from WordTimingAligner
    - All words from original text appear exactly once across all chunks in order

  </behavior>
  <action>
    Create StreamingPipelineTests.swift. This is an integration test that exercises the real pipeline
    components together (no mocks for the pipeline itself -- only mock the synthesis step by providing
    fake audio durations).

    Use `@Suite(.serialized) @MainActor struct StreamingPipelineTests` because SubtitleChunker requires @MainActor.

    The pipeline under test simulates what TTSEngine.synthesizeStreaming does:
    1. SentenceSplitter.splitIntoSentences(text) -> [String]
    2. For each sentence: SubtitleChunker.chunkIntoPages(text: sentence) -> [SubtitlePage]
    3. For each sentence: WordTimingAligner.extractWordTimings(text: sentence, audioDuration: mockDuration) -> [TimeInterval]

    Test cases:

    1. `multiSentenceProducesSequencedChunks`:
       Input: "The quick brown fox jumped over the lazy dog. It was a sunny day in the park. Birds were singing loudly."
       Mock audio duration: 2.0s per sentence
       Verify: 3 sentences produced, each has pages, all words present in order, timings sum to 2.0s each

    2. `singleSentenceProducesOneChunk`:
       Input: "Hello world this is a test"
       Mock duration: 1.5s
       Verify: 1 sentence, pages cover all 6 words, timings sum to 1.5s

    3. `emptyTextProducesNoChunks`:
       Input: ""
       Verify: SentenceSplitter returns [], no chunks

    4. `wordOrderPreservedAcrossChunks`:
       Input: "First part of text. Second part of text. Third part of text."
       Collect all words from all pages across all sentences in order
       Verify the concatenated words match the original text split by whitespace

    5. `timingsSumToAudioDuration`:
       Input: "Testing that durations are correct."
       Mock duration: 3.0s
       timings = WordTimingAligner.extractWordTimings(text:, audioDuration: 3.0)
       Verify: abs(timings.reduce(0, +) - 3.0) < 0.001

    Per TEST-05: integration test verifies a mock synthesis produces correctly sequenced chunks through the streaming pipeline.

  </action>
  <verify>
    <automated>cd plugins/claude-tts-companion && swift test --filter "StreamingPipelineTests" 2>&1 | tail -20</automated>
  </verify>
  <acceptance_criteria>
    - StreamingPipelineTests.swift exists with at least 5 test methods
    - `swift test --filter StreamingPipelineTests` shows all tests passed
    - grep confirms: "SentenceSplitter" and "SubtitleChunker" and "WordTimingAligner" all referenced in the test file
    - grep confirms: "multiSentence" test exists
  </acceptance_criteria>
  <done>Streaming pipeline integration test verifies end-to-end chunk sequencing from sentence splitting through page chunking and timing alignment.</done>
</task>

</tasks>

<verification>
```bash
cd plugins/claude-tts-companion && swift test 2>&1 | tail -30
```
All tests pass (existing Phase 18 tests + new Phase 20 tests). No regressions.
</verification>

<success_criteria>

1. SubtitleChunkerTests expanded with break priority, font size, page integrity, and width tests (TEST-02)
2. StreamingPipelineTests verifies SentenceSplitter -> SubtitleChunker -> WordTimingAligner chain (TEST-05)
3. `swift test` passes with all new and existing tests
4. Total word coverage is preserved across chunk boundaries (no lost or duplicated words)
   </success_criteria>

<output>
After completion, create `.planning/phases/20-unit-integration-tests/20-02-SUMMARY.md`
</output>
