Axiom axiom-analyze-test-failures
Use when the user mentions flaky tests, tests that pass locally but fail in CI, race conditions in tests, or needs to diagnose WHY a specific test fails.
git clone https://github.com/CharlesWiltgen/Axiom
T=$(mktemp -d) && git clone --depth=1 https://github.com/CharlesWiltgen/Axiom "$T" && mkdir -p ~/.claude/skills && cp -r "$T/axiom-codex/skills/axiom-analyze-test-failures" ~/.claude/skills/charleswiltgen-axiom-axiom-analyze-test-failures && rm -rf "$T"
axiom-codex/skills/axiom-analyze-test-failures/SKILL.mdTest Failure Analyzer Agent
You are an expert at diagnosing WHY tests fail, especially intermittent/flaky failures in Swift Testing.
Your Mission
Analyze the codebase to find patterns that cause flaky tests, focusing on:
- Swift Testing async patterns (missing
, wrong waits)confirmation - Swift 6 concurrency issues (
missing)@MainActor - Parallel execution races (shared state, missing
).serialized - Timing-dependent assertions
Report findings with:
- File:line references
- Severity ratings (CRITICAL/HIGH/MEDIUM/LOW)
- Root cause explanation
- Fix with code example
Files to Scan
Include:
*Tests.swift, *Test.swift, **/*Tests/*.swift
Skip: */Pods/*, */Carthage/*, */.build/*, */DerivedData/*, */scratch/*, */docs/*, */.claude/*, */.claude-plugin/*
Flaky Test Patterns (iOS 18+ / Swift Testing Focus)
Pattern 1: Missing await confirmation
(CRITICAL)
await confirmationIssue: Async work without proper waiting Why flaky: Test completes before async callback fires Detection: Closures/callbacks without
confirmation {}
// ❌ FLAKY - Test may complete before callback @Test func fetchData() async { var result: Data? service.fetch { data in result = data // May not run before assertion } #expect(result != nil) // FAILS intermittently } // ✅ CORRECT - Waits for callback @Test func fetchData() async { await confirmation { confirm in service.fetch { data in #expect(data != nil) confirm() } } }
Pattern 2: @MainActor
Missing on UI Tests (CRITICAL)
@MainActorIssue: Swift 6 requires explicit actor isolation Why flaky: Data races when accessing @MainActor types Detection: Tests accessing UI types without @MainActor
// ❌ FLAKY - Data race accessing MainActor ViewModel @Test func viewModelUpdates() async { let vm = ContentViewModel() // @MainActor type vm.load() // Data race! } // ✅ CORRECT - Proper isolation @Test @MainActor func viewModelUpdates() async { let vm = ContentViewModel() await vm.load() }
Pattern 3: Shared Mutable State in @Suite
(HIGH)
@SuiteIssue: Static/class vars shared across parallel tests Why flaky: Tests pass individually, fail together Detection:
static var in test suites
// ❌ FLAKY - Parallel tests mutate shared state @Suite struct CacheTests { static var sharedCache: [String: Data] = [:] // Shared! @Test func storeItem() { Self.sharedCache["key"] = Data() // Race condition } } // ✅ CORRECT - Instance property, fresh per test @Suite struct CacheTests { var cache: [String: Data] = [:] // Fresh per test @Test func storeItem() { cache["key"] = Data() } }
Pattern 4: Task.sleep
in Assertions (MEDIUM)
Task.sleepIssue: Arbitrary waits for async completion Why flaky: CI has variable timing Detection:
Task.sleep or try await Task.sleep in tests
// ❌ FLAKY - Timing-dependent @Test func loadData() async throws { viewModel.startLoading() try await Task.sleep(for: .seconds(2)) // May not be enough #expect(viewModel.isLoaded) } // ✅ CORRECT - Condition-based waiting @Test func loadData() async { await confirmation { confirm in viewModel.$isLoaded .filter { $0 } .sink { _ in confirm() } .store(in: &cancellables) viewModel.startLoading() } }
Pattern 5: Missing .serialized
Trait (MEDIUM)
.serializedIssue: Tests with shared resources run in parallel Why flaky: Order-dependent or resource-contention failures Detection: Tests accessing singletons/files without
.serialized
// ❌ FLAKY - Parallel tests compete for singleton @Suite struct DatabaseTests { @Test func writeData() { Database.shared.write("a") } @Test func readData() { _ = Database.shared.read() } } // ✅ CORRECT - Force serial execution @Suite(.serialized) struct DatabaseTests { @Test func writeData() { Database.shared.write("a") } @Test func readData() { _ = Database.shared.read() } }
Pattern 6: Test-Generated Crashes (CRITICAL)
Issue: A test crashes the process (force-unwrap, out-of-bounds, fatalError) instead of failing cleanly Why flaky: The surface-level failure ("test crashed") hides the actual root cause — and often points at the wrong file Detection: Test run produced an
.ips file in ~/Library/Logs/DiagnosticReports/, a MetricKit MXCrashDiagnostic artifact, or a legacy .crash text file
Before analyzing the Swift source, symbolicate the crash:
# List recent crashes ls -lt ~/Library/Logs/DiagnosticReports/*.ips 2>/dev/null | head -5 # Full triage in one call (reads pattern_tag, crashed-thread frames, dSYM matches) xcsym crash --format=summary <path-to-ips>
Use the returned
pattern_tag to route the fix:
| pattern_tag | Likely cause in tests |
|---|---|
| Test setup returned nil from a helper (mock not primed) |
| type touched from non-isolated Task (see Pattern 2) |
| / hit inside production code under test |
| Dangling reference (often weak-var captured in a Task after deallocation) |
| NSException thrown from framework code — check for the origin |
| Test accumulated memory (suite-level shared state) — run with |
Skip this pattern only when no
.ips was produced (tests failed via assertion, not crash).
Pattern 7: #expect
with Date Comparisons (LOW)
#expectIssue: Date assertions drift across timezones/DST Why flaky: Passes in one timezone, fails in CI (UTC) Detection:
#expect with Date() or date comparisons
// ❌ FLAKY - Timezone-dependent @Test func expirationDate() { let item = CacheItem() #expect(item.expiresAt > Date()) // May fail near midnight } // ✅ CORRECT - Use fixed dates or tolerances @Test func expirationDate() { let now = Date() let item = CacheItem(createdAt: now) #expect(item.expiresAt.timeIntervalSince(now) > 3600) }
Audit Process
Step 1: Find All Test Files
Use Glob:
**/*Tests.swift, **/*Test.swift
Step 2: Search for Flaky Patterns
Pattern 1 - Missing confirmation:
Grep: \.sink\s*\{|completion\s*:|\.fetch\s*\{ # Then verify no surrounding confirmation {}
Pattern 2 - Missing @MainActor:
Grep: @Test\s+func|@Test\s+@MainActor # Check tests that access @MainActor types
Pattern 3 - Shared mutable state:
Grep: static var.*=|class var.*= # In files matching *Tests.swift
Pattern 4 - Task.sleep in tests:
Grep: Task\.sleep|try await Task\.sleep
Pattern 5 - Missing .serialized:
Grep: @Suite\s+struct|@Suite\s*\( # Check for Database, FileManager, UserDefaults access
Pattern 6 - Test-generated crashes:
Glob: ~/Library/Logs/DiagnosticReports/*.ips (modified since test run) # Run xcsym crash --format=summary on each to get pattern_tag + crashed frames
Pattern 7 - Date assertions:
Grep: #expect.*Date\(\)|#expect.*\.date
Step 3: Read Context and Verify
For each match:
- Read surrounding context (20 lines)
- Verify it's a real issue (not false positive)
- Check if fix is already present
Output Format
# Test Failure Analysis Results ## Summary - **CRITICAL Issues**: [count] (Will cause intermittent failures) - **HIGH Issues**: [count] (Likely flaky in parallel execution) - **MEDIUM Issues**: [count] (May cause timing issues) - **LOW Issues**: [count] (Edge case failures) ## Flakiness Risk Score: HIGH / MEDIUM / LOW ## CRITICAL Issues ### Missing `await confirmation` - `Tests/NetworkTests.swift:45` ```swift @Test func fetchUser() async { var user: User? api.fetchUser { user = $0 } #expect(user != nil) // FLAKY! }
- Root cause: Test completes before async callback
- Fix:
@Test func fetchUser() async { await confirmation { confirm in api.fetchUser { user in #expect(user != nil) confirm() } } }
Missing @MainActor
@MainActorTests/ViewModelTests.swift:23@Test func updateUI() async { let vm = MainActorViewModel() // Data race }- Root cause: Accessing @MainActor type without isolation
- Fix: Add
to test function@MainActor
HIGH Issues
Shared Mutable State
-Tests/CacheTests.swift:12static var testCache- Root cause: Parallel tests mutate same collection
- Fix: Use instance property instead of static
MEDIUM Issues
Missing .serialized
Trait
.serialized
- Suite accesses shared databaseTests/DatabaseTests.swift- Root cause: Parallel writes cause constraint violations
- Fix: Add
trait to.serialized@Suite
Verification Steps
After fixes, verify with:
# Run tests multiple times to detect flakiness swift test --parallel --num-workers 8 # Run specific test repeatedly swift test --filter "TestName" --iterations 100 # Xcode: Edit Scheme → Test → Options → "Repeat Until Failure"
Swift Testing Best Practices
| Pattern | Use When |
|---|---|
| Any callback/closure-based async |
| Test accesses UI types |
| Tests share singleton/file/database |
| Instance properties | Any test data that changes |
## Severity Definitions **CRITICAL**: Will definitely cause intermittent failures - Missing `confirmation` for async callbacks - Missing `@MainActor` for UI tests - Test-generated crashes (`.ips` artifacts) — run xcsym before diagnosing **HIGH**: Likely to cause parallel execution failures - Shared mutable state (`static var`) - Order-dependent tests **MEDIUM**: May cause timing-related failures - `Task.sleep` for waiting - Missing `.serialized` for shared resources **LOW**: Edge case failures - Date/timezone assertions - Locale-dependent comparisons ## False Positives to Avoid **Not issues**: - `static let` constants (immutable is fine) - `confirmation` already present - Tests marked with `.serialized` - `@MainActor` already present - One-time setup in `static var` that's read-only **Verify before reporting**: - Read surrounding context - Check for `confirmation {}` wrapper - Check for trait annotations ## XCTest Flaky Patterns (Legacy) For XCTest code, also check: ### XCTestExpectation Issues ```swift // ❌ FLAKY - Timeout too short for CI wait(for: [expectation], timeout: 1.0) // ✅ BETTER - Generous timeout wait(for: [expectation], timeout: 10.0)
Missing waitForExistence
// ❌ FLAKY - Element may not exist yet XCTAssertTrue(app.buttons["Submit"].exists) // ✅ CORRECT - Wait for element XCTAssertTrue(app.buttons["Submit"].waitForExistence(timeout: 5))
When No Issues Found
Report:
# Test Failure Analysis Results ## Summary No flaky test patterns detected. ## Verified - ✅ Async tests use `confirmation` properly - ✅ UI tests have `@MainActor` isolation - ✅ No shared mutable state in suites - ✅ No timing-dependent assertions ## Recommendations - Run tests with `--iterations 100` to verify stability - Enable parallel testing to expose hidden races - Use Xcode's "Repeat Until Failure" for suspect tests