---
phase: 18-companioncore-library-test-infrastructure
plan: 02
type: execute
wave: 2
depends_on: ["18-01"]
files_modified:
  - plugins/claude-tts-companion/Tests/CompanionCoreTests/LanguageDetectorTests.swift
  - plugins/claude-tts-companion/Tests/CompanionCoreTests/SubtitleChunkerTests.swift
  - plugins/claude-tts-companion/Tests/CompanionCoreTests/TelegramFormatterTests.swift
  - plugins/claude-tts-companion/Tests/CompanionCoreTests/TranscriptParserTests.swift
  - plugins/claude-tts-companion/Tests/CompanionCoreTests/CircuitBreakerTests.swift
autonomous: true
requirements: [TEST-01]

must_haves:
  truths:
    - "swift test runs and passes with unit tests for five pure types"
    - "@testable import CompanionCore works in the test target"
    - "LanguageDetector, SubtitleChunker, TelegramFormatter, TranscriptParser, and CircuitBreaker each have at least one passing test"
  artifacts:
    - path: "plugins/claude-tts-companion/Tests/CompanionCoreTests/LanguageDetectorTests.swift"
      provides: "Language detection tests for English and CJK"
      contains: "@testable import CompanionCore"
    - path: "plugins/claude-tts-companion/Tests/CompanionCoreTests/SubtitleChunkerTests.swift"
      provides: "Subtitle page chunking tests with @MainActor"
      contains: "@MainActor"
    - path: "plugins/claude-tts-companion/Tests/CompanionCoreTests/TelegramFormatterTests.swift"
      provides: "HTML formatting and chunking tests"
      contains: "@testable import CompanionCore"
    - path: "plugins/claude-tts-companion/Tests/CompanionCoreTests/TranscriptParserTests.swift"
      provides: "JSONL transcript parsing tests"
      contains: "@testable import CompanionCore"
    - path: "plugins/claude-tts-companion/Tests/CompanionCoreTests/CircuitBreakerTests.swift"
      provides: "Circuit breaker state machine tests"
      contains: "CircuitBreaker("
  key_links:
    - from: "Tests/CompanionCoreTests/*.swift"
      to: "Sources/CompanionCore/"
      via: "@testable import CompanionCore"
      pattern: "@testable import CompanionCore"
---

<objective>
Write unit tests for five pure types using Swift Testing framework: LanguageDetector, SubtitleChunker, TelegramFormatter, TranscriptParser, and CircuitBreaker. These are proof-of-life tests (per D-07, D-08, D-09) that validate `swift test` works with the new library structure.

Purpose: Establish test infrastructure and verify `@testable import CompanionCore` works. Phase 20 adds comprehensive coverage.
Output: `swift test` passes with tests for all five types.
</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
@.planning/phases/18-companioncore-library-test-infrastructure/18-CONTEXT.md
@.planning/phases/18-companioncore-library-test-infrastructure/18-RESEARCH.md
@.planning/phases/18-companioncore-library-test-infrastructure/18-01-SUMMARY.md

<interfaces>
<!-- Key type signatures the executor needs for writing tests -->

From Sources/CompanionCore/LanguageDetector.swift:

```swift
public struct LanguageResult {
    let lang: String      // "en-us" or "cmn"
    let voiceName: String // "af_heart" or "zf_xiaobei"
}
public enum LanguageDetector {
    static func detect(text: String) -> LanguageResult
}
```

From Sources/CompanionCore/SubtitleChunker.swift:

```swift
public struct SubtitlePage {
    let words: [String]
    // ... other fields
}
@MainActor
public enum SubtitleChunker {
    static func chunkIntoPages(text: String, ...) -> [SubtitlePage]
}
```

From Sources/CompanionCore/TelegramFormatter.swift:

```swift
public enum TelegramFormatter {
    static func markdownToHTML(_ text: String) -> String
    static func chunkHTML(_ html: String, maxLength: Int) -> [String]
    // ... other methods
}
```

From Sources/CompanionCore/TranscriptParser.swift:

```swift
public enum TranscriptEntry {
    case prompt(String, Date?)
    case response(String, Date?)
    case toolUse(String, Date?)
    case toolResult(String, Date?)
    case unknown
}
public enum TranscriptParser {
    static func parse(filePath: String) -> [TranscriptEntry]
    static func entriesToTurns(_ entries: [TranscriptEntry]) -> [(role: String, content: String)]
}
```

From Sources/CompanionCore/CircuitBreaker.swift:

```swift
public final class CircuitBreaker: @unchecked Sendable {
    let maxFailures: Int
    let cooldownSeconds: TimeInterval
    init(maxFailures: Int = 3, cooldownSeconds: TimeInterval = 300)
    var isOpen: Bool { get }
    func recordFailure()
    func recordSuccess()
}
```

</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Write unit tests for LanguageDetector, TelegramFormatter, TranscriptParser, and CircuitBreaker</name>
  <files>
    plugins/claude-tts-companion/Tests/CompanionCoreTests/LanguageDetectorTests.swift,
    plugins/claude-tts-companion/Tests/CompanionCoreTests/TelegramFormatterTests.swift,
    plugins/claude-tts-companion/Tests/CompanionCoreTests/TranscriptParserTests.swift,
    plugins/claude-tts-companion/Tests/CompanionCoreTests/CircuitBreakerTests.swift
  </files>
  <read_first>
    plugins/claude-tts-companion/Sources/CompanionCore/LanguageDetector.swift,
    plugins/claude-tts-companion/Sources/CompanionCore/TelegramFormatter.swift,
    plugins/claude-tts-companion/Sources/CompanionCore/TranscriptParser.swift,
    plugins/claude-tts-companion/Sources/CompanionCore/CircuitBreaker.swift
  </read_first>
  <action>
Create four test files in `Tests/CompanionCoreTests/`. All use Swift Testing framework (`import Testing`, `@Test`, `#expect`) -- NOT XCTest (per research recommendation, Pattern 2).

**LanguageDetectorTests.swift:**

```swift
@testable import CompanionCore
import Testing

@Suite struct LanguageDetectorTests {
    @Test func detectsEnglishTextByDefault() {
        let result = LanguageDetector.detect(text: "Hello world, this is a test")
        #expect(result.lang == "en-us")
        #expect(result.voiceName == "af_heart")
    }

    @Test func detectsCJKTextAboveThreshold() {
        // >20% CJK characters triggers Chinese detection
        let result = LanguageDetector.detect(text: "你好世界，这是一个测试内容的文字")
        #expect(result.lang == "cmn")
        #expect(result.voiceName == "zf_xiaobei")
    }

    @Test func detectsEnglishWithMinorCJK() {
        // <20% CJK should still be English
        let result = LanguageDetector.detect(text: "This is mostly English text with 你好")
        #expect(result.lang == "en-us")
    }

    @Test func handlesEmptyText() {
        let result = LanguageDetector.detect(text: "")
        #expect(result.lang == "en-us")
    }
}
```

**TelegramFormatterTests.swift:**
Read the actual TelegramFormatter source to identify exact method signatures and test the markdown-to-HTML conversion and chunking. Key behaviors to test:

- `markdownToHTML` converts **bold** to `<b>bold</b>`
- `markdownToHTML` converts `code` to `<code>code</code>`
- `chunkHTML` splits text at 4096 chars respecting fence boundaries
- File reference wrapping prevents auto-linking (.md, .py files)

Write at least 3 tests covering: bold conversion, code conversion, and chunking behavior.

**TranscriptParserTests.swift:**
Read the actual TranscriptParser source to identify parsing logic. Write tests that:

- Parse a minimal JSONL line with `"type": "human"` into a `.prompt` entry
- Parse a minimal JSONL line with `"type": "assistant"` into a `.response` entry
- Handle invalid JSON gracefully (returns `.unknown` or empty)
- `entriesToTurns` converts entries to `(role, content)` tuples

Write at least 3 tests. For file-based parsing, create a temp file with JSONL content, pass its path, and verify the result.

**CircuitBreakerTests.swift (per D-08):**

```swift
@testable import CompanionCore
import Testing

@Suite struct CircuitBreakerTests {
    @Test func startsInClosedState() {
        let cb = CircuitBreaker(maxFailures: 3, cooldownSeconds: 60)
        #expect(!cb.isOpen)
    }

    @Test func opensAfterMaxFailures() {
        let cb = CircuitBreaker(maxFailures: 2, cooldownSeconds: 60)
        cb.recordFailure()
        cb.recordFailure()
        #expect(cb.isOpen)
    }

    @Test func doesNotOpenBeforeMaxFailures() {
        let cb = CircuitBreaker(maxFailures: 3, cooldownSeconds: 60)
        cb.recordFailure()
        cb.recordFailure()
        #expect(!cb.isOpen)
    }

    @Test func resetsOnSuccess() {
        let cb = CircuitBreaker(maxFailures: 2, cooldownSeconds: 60)
        cb.recordFailure()
        cb.recordSuccess()
        cb.recordFailure()
        #expect(!cb.isOpen) // Only 1 consecutive failure after reset
    }

    @Test func closesAfterCooldown() async throws {
        let cb = CircuitBreaker(maxFailures: 1, cooldownSeconds: 0.1)
        cb.recordFailure()
        #expect(cb.isOpen)
        try await Task.sleep(for: .milliseconds(150))
        #expect(!cb.isOpen) // Cooldown expired
    }
}
```

IMPORTANT: Read each source file FIRST to verify the exact method signatures, parameter names, and return types before writing tests. The interfaces block above is approximate -- the source is the SSoT. Adapt test code to match actual signatures.
</action>
<verify>
<automated>cd plugins/claude-tts-companion && swift test --filter "LanguageDetector|TelegramFormatter|TranscriptParser|CircuitBreaker" 2>&1 | tail -20</automated>
</verify>
<acceptance_criteria> - `LanguageDetectorTests.swift` contains `@testable import CompanionCore` and `import Testing` - `LanguageDetectorTests.swift` contains `@Test func detectsEnglishTextByDefault()` - `TelegramFormatterTests.swift` contains at least 3 `@Test func` declarations - `TranscriptParserTests.swift` contains at least 3 `@Test func` declarations - `CircuitBreakerTests.swift` contains `CircuitBreaker(maxFailures:` instantiation - `CircuitBreakerTests.swift` contains `cb.recordFailure()` and `cb.isOpen` - `swift test --filter "LanguageDetector|TelegramFormatter|TranscriptParser|CircuitBreaker"` exits 0
</acceptance_criteria>
<done>Four test files created and all tests pass via `swift test`</done>
</task>

<task type="auto">
  <name>Task 2: Write SubtitleChunker tests with @MainActor isolation</name>
  <files>
    plugins/claude-tts-companion/Tests/CompanionCoreTests/SubtitleChunkerTests.swift
  </files>
  <read_first>
    plugins/claude-tts-companion/Sources/CompanionCore/SubtitleChunker.swift,
    plugins/claude-tts-companion/Sources/CompanionCore/SubtitleStyle.swift
  </read_first>
  <action>
Create `Tests/CompanionCoreTests/SubtitleChunkerTests.swift` using Swift Testing with `@MainActor` on the `@Suite` (per research Pattern 3). SubtitleChunker is `@MainActor`-annotated because it uses `NSAttributedString.size()` with `NSFont`.

```swift
@testable import CompanionCore
import Testing

@Suite(.serialized)
@MainActor
struct SubtitleChunkerTests {
    @Test func chunksShortTextIntoSinglePage() {
        let pages = SubtitleChunker.chunkIntoPages(text: "Hello world")
        #expect(pages.count == 1)
        #expect(pages[0].words.contains("Hello"))
        #expect(pages[0].words.contains("world"))
    }

    @Test func handlesEmptyText() {
        let pages = SubtitleChunker.chunkIntoPages(text: "")
        #expect(pages.isEmpty)
    }

    @Test func splitsLongTextIntoMultiplePages() {
        // Generate text long enough to require multiple pages
        let longText = (1...100).map { "word\($0)" }.joined(separator: " ")
        let pages = SubtitleChunker.chunkIntoPages(text: longText)
        #expect(pages.count > 1)
        // All words should be present across all pages
        let allWords = pages.flatMap { $0.words }
        #expect(allWords.count == 100)
    }
}
```

IMPORTANT: Read `SubtitleChunker.swift` first to verify the exact signature of `chunkIntoPages` (parameters, return type, SubtitlePage fields). The code above is approximate. Adapt to the actual API. Pay attention to:

- What parameters `chunkIntoPages` takes (may have fontSize, maxLines, etc.)
- What fields `SubtitlePage` has (words array? text string? indices?)
- Whether there are other static methods worth testing

Run `swift test --filter SubtitleChunker` to verify.
</action>
<verify>
<automated>cd plugins/claude-tts-companion && swift test --filter SubtitleChunker 2>&1 | tail -10</automated>
</verify>
<acceptance_criteria> - `SubtitleChunkerTests.swift` contains `@MainActor` annotation on the struct - `SubtitleChunkerTests.swift` contains `@Suite(.serialized)` annotation - `SubtitleChunkerTests.swift` contains `@testable import CompanionCore` - `SubtitleChunkerTests.swift` contains at least 3 `@Test func` declarations - `swift test --filter SubtitleChunker` exits 0
</acceptance_criteria>
<done>SubtitleChunker tests pass with @MainActor isolation. Full test suite (`swift test`) passes.</done>
</task>

</tasks>

<verification>
1. `cd plugins/claude-tts-companion && swift test` -- all tests pass
2. `ls Tests/CompanionCoreTests/` -- shows 5 test files
3. `swift test 2>&1 | grep -c "Test run started"` -- confirms test discovery works
4. `swift test 2>&1 | grep "passed"` -- shows pass count > 0
</verification>

<success_criteria>

- `swift test` runs and passes with zero failures
- 5 test files exist in Tests/CompanionCoreTests/
- `@testable import CompanionCore` compiles in all test files
- LanguageDetector, SubtitleChunker, TelegramFormatter, TranscriptParser, and CircuitBreaker each have at least one passing test
- SubtitleChunker tests use @MainActor isolation
  </success_criteria>

<output>
After completion, create `.planning/phases/18-companioncore-library-test-infrastructure/18-02-SUMMARY.md`
</output>
