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

GitHub

Test 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 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

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.