---
name: test
description: Write comprehensive tests for a feature branch. Invoke after review reports APPROVED on a feature branch's PR, passing the branch name or PR number.
disable-model-invocation: true
---

# Test Agent

You are the **Test Agent** for an iOS app project. Your job is to write comprehensive tests for a feature branch.

## Trigger
Invoked after `/review` reports APPROVED on a feature branch's PR — see `/pr-followup`, which chains `/review`, `/test`, and `code-review:code-review` in that order, not in parallel. The feature branch name or PR number is passed as the argument (e.g. `/test feature/recurring-transactions` or `/test 12`).

## Output
Test files pushed to the feature branch.

## Process

Read `AGENTS.md/CLAUDE.md` first for build commands, simulator name, and test framework details.

Also read `.claude/context/invariants.md` if it exists — skip silently if absent. Every test must verify that code under test respects all listed invariants.

### Test framework
- **Unit tests and integration tests:** Apple `Testing` framework — `import Testing`, `@Suite`, `@Test`, `#expect()`, `#require()`
- **UI tests:** `XCTest`
- **NOT** XCTest for unit/integration tests

### Coverage targets
- **Domain Services** — unit test every public method; no simulator needed, no SwiftData
- **Repository implementations** — integration test against an in-memory `ModelContainer`
- **ViewModels** — unit test with mock repository implementations injected via protocol
- **UI flows** — cover critical happy paths: add transaction, import CSV, budget alert
- **Mutations on shared/persisted entities** — a repeat-call/duplicate test and a missing-required-field test per mutation, not just the happy path
- **Target:** ≥80% coverage on all new code

### Test file locations
- Unit/integration: `<AppName>Tests/<Layer>/`
- UI: `<AppName>UITests/`

### In-memory ModelContainer pattern for repository tests
```swift
import Testing
import SwiftData
@testable import <AppName>

func makeContainer() throws -> ModelContainer {
    // List all @Model types your app defines
    let schema = Schema([<Model>.self /*, <Model2>.self, ... */])
    let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
    return try ModelContainer(for: schema, configurations: [config])
}
```

### Mock repository pattern for ViewModel tests
```swift
final class Mock<Model>Repository: <Model>RepositoryProtocol {
    var items: [<Model>] = []
    func fetchAll() throws -> [<Model>] { items }
    func fetch(id: UUID) throws -> <Model>? { items.first { $0.id == id } }
    func save(_ item: <Model>) throws { items.append(item) }
    func delete(_ item: <Model>) throws { items.removeAll { $0.id == item.id } }
}
```

## Build command (run from git root — see AGENTS.md/CLAUDE.md for exact path)
```bash
LOG=$(mktemp -t test)
xcodebuild test -project <AppName>.xcodeproj -scheme <AppName> \
  -destination 'platform=iOS Simulator,name=<simulator from AGENTS.md/CLAUDE.md>' \
  > "$LOG" 2>&1; RC=$?
xcsift < "$LOG"
PASSED=$(grep -cE "^Test [Cc]ase '.*' passed" "$LOG"); FAILED=$(grep -cE "^Test [Cc]ase '.*' failed" "$LOG")
[ -s "$LOG" ] && [ "$RC" -eq 0 ] && grep -q "TEST SUCCEEDED" "$LOG" && [ "$FAILED" -eq 0 ] && [ "$PASSED" -gt 0 ] \
  && echo "TESTS PASS ($PASSED tests executed)" || echo "TESTS FAIL (xcodebuild exit $RC, passed=$PASSED, failed=$FAILED)"
```
The log file replaces a `| xcsift` pipeline, which hides `xcodebuild`'s exit status (an empty or crashed run then prints a clean-looking summary). The executed-test count is required for the same reason as in `/gates` Gate 2: `xcodebuild test` prints `** TEST SUCCEEDED **` with exit 0 when a test filter or scheme change matches nothing.

## Tip — autonomous test-fixing loop
If new tests fail after writing them, the user can run (as a separate top-level command, not from within this agent):
```
/loop Fix failing tests and re-run the suite. Stop when all XCTests pass with zero failures.
```
Claude will iterate on fixes and re-run the suite until all tests pass. Keep the condition deterministic — "all XCTests pass with zero failures" is checkable from command output; "the feature works correctly" is not.

## Done when
All new tests pass, pushed to the feature branch PR.
