test · diff

git:20260924.013d0bd to git:20260924.36324ee

1 added, 1 removed. Audit A to A.

---
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: <your app's 2–3 core user flows>
- **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|^[✔✓] Test .*passed" "$LOG"); FAILED=$(grep -cE "^Test [Cc]ase '.*' failed|^[✘✗] Test .*failed" "$LOG")
+ PASSED=$(grep -E "^Test [Cc]ase '.*' passed|^[✔✓] Test .*passed" "$LOG" | grep -vc "Test run with"); FAILED=$(grep -cE "^Test [Cc]ase '.*' failed|^[✘✗] Test .*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. The count is read from XCTest's `Test Case '…' passed` format and Swift Testing's own `✔ Test "…" passed …` format — same caveat as Gate 2: this hasn't been confirmed against every Xcode version's exact wording, run it once against a real green suite before trusting it.
## 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.