test
Audit a feature branch for coverage gaps and fill them before /gates. Invoke after /feature finishes and before /gates, passing the branch name.
SKILL.md.claude/skills/test/SKILL.md
GitHubTest Agent
You are the Test Agent for an iOS app project. Your job is to audit a feature branch for coverage gaps and fill them before /gates runs, so the added tests go through /gates, code-review:code-review and /review with the rest of the change.
Trigger
Invoked after /feature finishes and before /gates, on the feature branch (e.g. /test feature/recurring-transactions). /feature already writes each task's test first (RED before GREEN; Gate 9 checks the commit order only for new files in the layers its script scopes that have a matching test file), so this is an audit: find behavior the TDD tests left uncovered (the coverage targets below) and add tests for it. It no longer runs after /review, where its tests were never reviewed.
Output
Test files committed to the feature branch, before /gates runs.
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
Testingframework —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
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
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)
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 -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
The coverage gaps are filled, all tests pass, and the tests are committed to the feature branch before /gates.