Release v1.18.0: Add iOS-APP-developer and promptfoo-evaluation skills
### Added - **New Skill**: iOS-APP-developer v1.1.0 - iOS development with XcodeGen, SwiftUI, and SPM - XcodeGen project.yml configuration - SPM dependency resolution - Device deployment and code signing - Camera/AVFoundation debugging - iOS version compatibility handling - Library not loaded @rpath framework error fixes - State machine testing patterns for @MainActor classes - Bundled references: xcodegen-full.md, camera-avfoundation.md, swiftui-compatibility.md, testing-mainactor.md - **New Skill**: promptfoo-evaluation v1.0.0 - LLM evaluation framework using Promptfoo - Promptfoo configuration (promptfooconfig.yaml) - Python custom assertions - llm-rubric for LLM-as-judge evaluations - Few-shot example management - Model comparison and prompt testing - Bundled reference: promptfoo_api.md ### Changed - Updated marketplace version from 1.16.0 to 1.18.0 - Updated marketplace skills count from 23 to 25 - Updated skill-creator to v1.2.2: - Fixed best practices documentation URL (platform.claude.com) - Enhanced quick_validate.py to exclude file:// prefixed paths from validation - Updated marketplace.json metadata description to include new skills 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
146
iOS-APP-developer/references/testing-mainactor.md
Normal file
146
iOS-APP-developer/references/testing-mainactor.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# Testing @MainActor Classes
|
||||
|
||||
## The Problem
|
||||
|
||||
Testing `@MainActor` classes like `ObservableObject` controllers in Swift 6 is challenging because:
|
||||
|
||||
1. `setUp()` and `tearDown()` are nonisolated
|
||||
2. Properties with `private(set)` can't be set from tests
|
||||
3. Direct property access from tests triggers concurrency errors
|
||||
|
||||
## Solution: setStateForTesting Pattern
|
||||
|
||||
Add a DEBUG-only method to allow tests to set internal state:
|
||||
|
||||
```swift
|
||||
@MainActor
|
||||
final class TrainingSessionController: ObservableObject {
|
||||
@Published private(set) var state: TrainingState = .idle
|
||||
|
||||
// ... rest of controller ...
|
||||
|
||||
// MARK: - Testing Support
|
||||
|
||||
#if DEBUG
|
||||
/// Set state directly for testing purposes only
|
||||
func setStateForTesting(_ newState: TrainingState) {
|
||||
state = newState
|
||||
}
|
||||
#endif
|
||||
}
|
||||
```
|
||||
|
||||
## Test Class Structure
|
||||
|
||||
```swift
|
||||
import XCTest
|
||||
@testable import YourApp
|
||||
|
||||
@MainActor
|
||||
final class TrainingSessionControllerTests: XCTestCase {
|
||||
|
||||
var controller: TrainingSessionController!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
controller = TrainingSessionController()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
controller = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testConfigureFromFailedStateAutoResets() {
|
||||
// Arrange: Set to failed state
|
||||
controller.setStateForTesting(.failed("Recording too short"))
|
||||
|
||||
// Act: Configure should recover
|
||||
controller.configure(with: PhaseConfig.default)
|
||||
|
||||
// Assert: Should be back in idle
|
||||
XCTAssertEqual(controller.state, .idle)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## State Machine Testing Patterns
|
||||
|
||||
### Testing State Transitions
|
||||
|
||||
```swift
|
||||
func testStateTransitions() {
|
||||
// Test each state's behavior
|
||||
let states: [TrainingState] = [.idle, .completed, .failed("error")]
|
||||
|
||||
for state in states {
|
||||
controller.setStateForTesting(state)
|
||||
controller.configure(with: PhaseConfig.default)
|
||||
|
||||
// Verify expected outcome
|
||||
XCTAssertTrue(controller.canStart, "\(state) should allow starting")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Regression Tests
|
||||
|
||||
For bugs that have been fixed, add specific regression tests:
|
||||
|
||||
```swift
|
||||
/// Regression test for: State machine dead-lock after recording failure
|
||||
/// Bug: After error, controller stayed in failed state forever
|
||||
func testRegressionFailedStateDeadLock() {
|
||||
// Simulate the bug scenario
|
||||
controller.configure(with: PhaseConfig.default)
|
||||
controller.setStateForTesting(.failed("录音太短"))
|
||||
|
||||
// The fix: configure() should auto-reset from failed state
|
||||
controller.configure(with: PhaseConfig.default)
|
||||
|
||||
XCTAssertEqual(controller.state, .idle,
|
||||
"REGRESSION: Failed state should not block configure()")
|
||||
}
|
||||
```
|
||||
|
||||
### State Machine Invariants
|
||||
|
||||
Test invariants that should always hold:
|
||||
|
||||
```swift
|
||||
/// No terminal state should become a "dead end"
|
||||
func testAllTerminalStatesAreRecoverable() {
|
||||
let terminalStates: [TrainingState] = [
|
||||
.idle,
|
||||
.completed,
|
||||
.failed("test error")
|
||||
]
|
||||
|
||||
for state in terminalStates {
|
||||
controller.setStateForTesting(state)
|
||||
// Action that should recover
|
||||
controller.configure(with: PhaseConfig.default)
|
||||
|
||||
// Verify recovery
|
||||
XCTAssertTrue(canConfigure(),
|
||||
"\(state) should be recoverable via configure()")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Why This Pattern Works
|
||||
|
||||
1. **`#if DEBUG`**: Method only exists in test builds, zero production overhead
|
||||
2. **Explicit method**: Makes test-only state manipulation obvious and searchable
|
||||
3. **MainActor compatible**: Method is part of the @MainActor class
|
||||
4. **Swift 6 safe**: Avoids concurrency errors by staying on the main actor
|
||||
|
||||
## Alternative: Internal Setter
|
||||
|
||||
If you prefer, you can use `internal(set)` instead of `private(set)`:
|
||||
|
||||
```swift
|
||||
@Published internal(set) var state: TrainingState = .idle
|
||||
```
|
||||
|
||||
However, this is redundant since properties are already internal by default. The `setStateForTesting()` pattern is more explicit about test-only intent.
|
||||
Reference in New Issue
Block a user