---
phase: 20-unit-integration-tests
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - plugins/claude-tts-companion/Tests/CompanionCoreTests/WordTimingAlignerTests.swift
  - plugins/claude-tts-companion/Tests/CompanionCoreTests/PronunciationProcessorTests.swift
  - plugins/claude-tts-companion/Tests/CompanionCoreTests/SentenceSplitterTests.swift
autonomous: true
requirements: [TEST-03, TEST-04]

must_haves:
  truths:
    - "WordTimingAligner correctly extracts timings from MToken arrays filtering punctuation"
    - "WordTimingAligner aligns MToken onsets to subtitle words even when counts differ"
    - "WordTimingAligner handles hyphenated words in character-offset alignment"
    - "PronunciationProcessor replaces 'plugin' variants with hyphenated forms"
    - "PronunciationProcessor respects word boundaries (no partial matches)"
    - "SentenceSplitter splits on sentence boundaries while preserving abbreviations and decimals"
  artifacts:
    - path: "plugins/claude-tts-companion/Tests/CompanionCoreTests/WordTimingAlignerTests.swift"
      provides: "Unit tests for WordTimingAligner"
      min_lines: 80
    - path: "plugins/claude-tts-companion/Tests/CompanionCoreTests/PronunciationProcessorTests.swift"
      provides: "Unit tests for PronunciationProcessor"
      min_lines: 30
    - path: "plugins/claude-tts-companion/Tests/CompanionCoreTests/SentenceSplitterTests.swift"
      provides: "Unit tests for SentenceSplitter"
      min_lines: 40
  key_links:
    - from: "WordTimingAlignerTests.swift"
      to: "WordTimingAligner.swift"
      via: "@testable import CompanionCore"
      pattern: "WordTimingAligner\\."
    - from: "PronunciationProcessorTests.swift"
      to: "PronunciationProcessor.swift"
      via: "@testable import CompanionCore"
      pattern: "PronunciationProcessor\\."
---

<objective>
Unit tests for the pure-struct components extracted in Phase 19: WordTimingAligner, PronunciationProcessor, and SentenceSplitter.

Purpose: These are the most testable components (pure functions, no side effects, no @MainActor). Testing them validates the core TTS text processing pipeline independently of audio hardware or UI.

Output: Three new test files covering TEST-03 (WordTimingAligner) and TEST-04 (PronunciationProcessor), plus SentenceSplitter coverage.
</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/WordTimingAligner.swift
@plugins/claude-tts-companion/Sources/CompanionCore/PronunciationProcessor.swift
@plugins/claude-tts-companion/Sources/CompanionCore/SentenceSplitter.swift
@plugins/claude-tts-companion/Tests/CompanionCoreTests/LanguageDetectorTests.swift

<interfaces>
<!-- Key types the executor needs for constructing test data -->

From MLXUtilsLibrary (MToken class, used by WordTimingAligner):

```swift
public class MToken {
    public var text: String
    public var tokenRange: Range<String.Index>
    public var tag: NLTag?
    public var whitespace: String
    public var phonemes: String?
    public var start_ts: Double?
    public var end_ts: Double?
    public var `_`: Underscore

    public init(
        text: String,
        tokenRange: Range<String.Index>,
        tag: NLTag? = nil,
        whitespace: String,
        phonemes: String? = nil,
        start_ts: Double? = nil,
        end_ts: Double? = nil,
        underscore: Underscore = Underscore()
    )
}
```

From WordTimingAligner (internal types accessed via @testable):

```swift
struct NativeTimings {
    let durations: [TimeInterval]
    let onsets: [TimeInterval]
    let texts: [String]
}

struct ResolvedTimings {
    let durations: [TimeInterval]
    let onsets: [TimeInterval]?
}
```

</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: WordTimingAligner + PronunciationProcessor unit tests</name>
  <files>
    plugins/claude-tts-companion/Tests/CompanionCoreTests/WordTimingAlignerTests.swift
    plugins/claude-tts-companion/Tests/CompanionCoreTests/PronunciationProcessorTests.swift
  </files>
  <read_first>
    plugins/claude-tts-companion/Sources/CompanionCore/WordTimingAligner.swift
    plugins/claude-tts-companion/Sources/CompanionCore/PronunciationProcessor.swift
    plugins/claude-tts-companion/Tests/CompanionCoreTests/LanguageDetectorTests.swift
  </read_first>
  <behavior>
    WordTimingAligner.extractTimingsFromTokens:
    - Returns nil for nil input
    - Returns nil for empty token array
    - Filters out punctuation-only tokens (".", ",", "!", "?", ";", ":", "-")
    - Filters out tokens with zero or negative duration
    - Returns correct durations (end_ts - start_ts) and onsets (start_ts) for valid tokens
    - Skips tokens missing start_ts or end_ts

    WordTimingAligner.alignOnsetsToWords:
    - Fast path: when native.texts.count == subtitleWords.count, returns native values directly
    - Handles fewer MToken words than subtitle words (extrapolation)
    - Handles more MToken words than subtitle words (compression)
    - Handles hyphenated subtitle words ("mid-decay") aligned to split MTokens ("mid", "decay")
    - Returns nil when alignment produces wrong count

    WordTimingAligner.stripPunctuation:
    - Removes leading/trailing punctuation
    - Removes internal hyphens and dashes (en-dash, em-dash)
    - Preserves letters and digits

    WordTimingAligner.extractWordTimings (character-weighted fallback):
    - Distributes duration proportional to character count
    - Sum of all timings equals audioDuration exactly
    - Returns empty array for empty text
    - Handles single-word text

    PronunciationProcessor.preprocessText:
    - Replaces "plugin" with "plug-in" (lowercase)
    - Replaces "plugins" with "plug-ins"
    - Replaces "Plugin" with "Plug-in" (capitalized)
    - Replaces "Plugins" with "Plug-ins"
    - Does NOT replace "unplugin" or "pluginfo" (word boundary \b)
    - Leaves text without overrides unchanged

  </behavior>
  <action>
    Create WordTimingAlignerTests.swift with `@Suite struct WordTimingAlignerTests` (no @MainActor needed -- pure struct).
    Import `@testable import CompanionCore`, `import Testing`, and `import MLXUtilsLibrary` (for MToken).

    To construct MToken test fixtures, create a helper function:
    ```swift
    func makeToken(_ text: String, start: Double, end: Double) -> MToken {
        let dummyRange = "dummy".startIndex..<"dummy".endIndex
        return MToken(text: text, tokenRange: dummyRange, whitespace: " ", start_ts: start, end_ts: end)
    }
    ```

    Test extractTimingsFromTokens with:
    - nil input -> nil
    - empty array -> nil
    - tokens: [("Hello", 0.0, 0.5), ("world", 0.6, 1.0)] -> durations [0.5, 0.4], onsets [0.0, 0.6], texts ["Hello", "world"]
    - mixed with punctuation: [("Hello", 0.0, 0.5), (",", 0.5, 0.55), ("world", 0.6, 1.0)] -> filters comma
    - tokens with nil start_ts -> skipped

    Test alignOnsetsToWords with:
    - Equal count fast path: native 3 words, subtitle 3 words -> returns native arrays
    - Hyphenated: native texts ["mid", "decay"], subtitle words ["mid-decay"] -> 1 onset, correct duration
    - More subtitle words than tokens: extrapolation works without crash

    Test stripPunctuation: "...hello!" -> "hello", "mid-decay" -> "middecay", "\u{2014}test\u{2013}" -> "test"

    Test extractWordTimings: "Hello world" with 2.0s -> [~0.667, ~1.333] (proportional to 5:10 chars)

    Create PronunciationProcessorTests.swift with `@Suite struct PronunciationProcessorTests`.
    Test all override variants and word boundary enforcement.

    Per TEST-03: cover MToken-to-word alignment, onset resolution, and hyphenated word handling.
    Per TEST-04: cover override dictionary matching and regex boundary behavior.

  </action>
  <verify>
    <automated>cd plugins/claude-tts-companion && swift test --filter "WordTimingAlignerTests|PronunciationProcessorTests" 2>&1 | tail -20</automated>
  </verify>
  <acceptance_criteria>
    - WordTimingAlignerTests.swift exists with at least 10 test methods
    - PronunciationProcessorTests.swift exists with at least 5 test methods
    - `swift test --filter WordTimingAlignerTests` shows all tests passed
    - `swift test --filter PronunciationProcessorTests` shows all tests passed
    - grep confirms: "extractTimingsFromTokens" tested, "alignOnsetsToWords" tested, "stripPunctuation" tested, "preprocessText" tested
  </acceptance_criteria>
  <done>WordTimingAligner has tests for token extraction, onset alignment, hyphen handling, and character-weighted fallback. PronunciationProcessor has tests for all override patterns and word boundary enforcement.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: SentenceSplitter unit tests</name>
  <files>
    plugins/claude-tts-companion/Tests/CompanionCoreTests/SentenceSplitterTests.swift
  </files>
  <read_first>
    plugins/claude-tts-companion/Sources/CompanionCore/SentenceSplitter.swift
  </read_first>
  <behavior>
    SentenceSplitter.splitIntoSentences:
    - Splits "Hello world. Goodbye world." into ["Hello world.", "Goodbye world."]
    - Handles "!" and "?" as sentence boundaries
    - Preserves abbreviations: "Mr. Smith went home." -> single sentence (no split after "Mr.")
    - Preserves decimals: "The value is 3.14 today." -> single sentence
    - Empty string returns empty array
    - Whitespace-only string returns empty array
    - Single sentence without terminal punctuation returns that sentence
    - Trailing fragment without punctuation merges into last sentence
    - Multiple sentence types: "Hello! How are you? Fine." -> 3 sentences
  </behavior>
  <action>
    Create SentenceSplitterTests.swift with `@Suite struct SentenceSplitterTests` (no @MainActor needed -- pure struct).
    Import `@testable import CompanionCore` and `import Testing`.

    Test cases:
    - Basic two-sentence split on period
    - Exclamation and question mark boundaries
    - Abbreviation preservation: "Dr. Smith said hello. Then left." -> ["Dr. Smith said hello.", "Then left."]
    - Decimal preservation: "Pi is 3.14 roughly. That is all." -> ["Pi is 3.14 roughly.", "That is all."]
    - Single capital letter abbreviation: "Call U. N. headquarters." -> single sentence (U. and N. are abbreviations)
    - Empty and whitespace-only input
    - No terminal punctuation: "Hello world" -> ["Hello world"]
    - Trailing fragment merge: "Hello. World" -> ["Hello.", "Hello. World"] -- actually the implementation merges trailing fragment with last sentence
    - Mixed punctuation: "Wow! Really? Yes." -> ["Wow!", "Really?", "Yes."]

  </action>
  <verify>
    <automated>cd plugins/claude-tts-companion && swift test --filter "SentenceSplitterTests" 2>&1 | tail -20</automated>
  </verify>
  <acceptance_criteria>
    - SentenceSplitterTests.swift exists with at least 7 test methods
    - `swift test --filter SentenceSplitterTests` shows all tests passed
    - grep confirms tests cover: empty input, period split, abbreviation, decimal, mixed punctuation
  </acceptance_criteria>
  <done>SentenceSplitter has tests for all boundary types, abbreviation/decimal preservation, empty input, and trailing fragment handling.</done>
</task>

</tasks>

<verification>
```bash
cd plugins/claude-tts-companion && swift test --filter "WordTimingAlignerTests|PronunciationProcessorTests|SentenceSplitterTests" 2>&1 | tail -30
```
All three new test suites pass. No existing tests broken.
</verification>

<success_criteria>

1. Three new test files created in Tests/CompanionCoreTests/
2. WordTimingAligner tests cover MToken extraction, onset alignment, hyphen handling (TEST-03)
3. PronunciationProcessor tests cover override matching and regex boundaries (TEST-04)
4. SentenceSplitter tests cover sentence boundary detection with edge cases
5. `swift test` passes with all new and existing tests
   </success_criteria>

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