Phase 2: Constraint Validation - 2 plans in 1 wave (parallel) - Both plans autonomous (no checkpoints) Plan 02-01: Migrate 13 XCTest tests to Swift Testing Plan 02-02: Add edge case tests and document constraint API Note: ItineraryConstraints is already fully implemented. This phase verifies and standardizes tests. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
14 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, user_setup, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | user_setup | must_haves | |||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-constraint-validation | 02 | execute | 1 |
|
true |
|
Purpose: Ensure constraint system handles boundary conditions and provide clear reference for drag-drop implementation. Output: Enhanced test suite with edge cases, API documentation for Phase 4.
<execution_context>
@/.claude/get-shit-done/workflows/execute-plan.md
@/.claude/get-shit-done/templates/summary.md
</execution_context>
Implementation being tested
@SportsTime/Core/Models/Domain/ItineraryConstraints.swift
Test file (will be enhanced)
@SportsTimeTests/Domain/ItineraryConstraintsTests.swift
Task 1: Add edge case tests SportsTimeTests/Domain/ItineraryConstraintsTests.swift Add the following edge case tests to the `// MARK: - Edge Cases` section:// MARK: - Edge Cases
@Test("edge: single-day trip accepts valid positions")
func edge_singleDayTrip_acceptsValidPositions() {
let constraints = makeConstraints(tripDays: 1, gameDays: [])
let custom = makeCustomItem(day: 1, sortOrder: 50)
#expect(constraints.isValidPosition(for: custom, day: 1, sortOrder: 50))
#expect(!constraints.isValidPosition(for: custom, day: 0, sortOrder: 50))
#expect(!constraints.isValidPosition(for: custom, day: 2, sortOrder: 50))
}
@Test("edge: day 0 is always invalid")
func edge_day0_isAlwaysInvalid() {
let constraints = makeConstraints(tripDays: 5, gameDays: [])
let custom = makeCustomItem(day: 1, sortOrder: 50)
#expect(!constraints.isValidPosition(for: custom, day: 0, sortOrder: 50))
}
@Test("edge: day beyond trip is invalid")
func edge_dayBeyondTrip_isInvalid() {
let constraints = makeConstraints(tripDays: 3, gameDays: [])
let custom = makeCustomItem(day: 1, sortOrder: 50)
#expect(!constraints.isValidPosition(for: custom, day: 4, sortOrder: 50))
#expect(!constraints.isValidPosition(for: custom, day: 100, sortOrder: 50))
}
@Test("edge: travel at exact game sortOrder boundary is invalid")
func edge_travelAtExactGameSortOrder_isInvalid() {
// Game at sortOrder 100
let constraints = makeConstraints(
tripDays: 3,
games: [makeGameItem(city: "Chicago", day: 1, sortOrder: 100)]
)
let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 1, sortOrder: 100)
// Exactly AT game sortOrder should be invalid (must be AFTER)
#expect(!constraints.isValidPosition(for: travel, day: 1, sortOrder: 100))
// Just after should be valid
#expect(constraints.isValidPosition(for: travel, day: 1, sortOrder: 100.001))
}
@Test("edge: travel with no games in either city has full range")
func edge_travelNoGamesInEitherCity_hasFullRange() {
let constraints = makeConstraints(tripDays: 5, games: [])
let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 1, sortOrder: 50)
// Valid on any day
for day in 1...5 {
#expect(constraints.isValidPosition(for: travel, day: day, sortOrder: 50))
}
// Full range
#expect(constraints.validDayRange(for: travel) == 1...5)
}
@Test("edge: negative sortOrder is valid for custom items")
func edge_negativeSortOrder_validForCustomItems() {
let constraints = makeConstraints(tripDays: 3, gameDays: [])
let custom = makeCustomItem(day: 2, sortOrder: -100)
#expect(constraints.isValidPosition(for: custom, day: 2, sortOrder: -100))
}
@Test("edge: very large sortOrder is valid for custom items")
func edge_veryLargeSortOrder_validForCustomItems() {
let constraints = makeConstraints(tripDays: 3, gameDays: [])
let custom = makeCustomItem(day: 2, sortOrder: 10000)
#expect(constraints.isValidPosition(for: custom, day: 2, sortOrder: 10000))
}
Also add a test verifying the roadmap success criteria are testable:
// MARK: - Success Criteria Verification
@Test("success: game row shows no drag interaction (game not draggable)")
func success_gameNotDraggable() {
// Games return false for ANY position, making them non-draggable
let game = makeGameItem(city: "Chicago", day: 2, sortOrder: 100)
let constraints = makeConstraints(tripDays: 5, games: [game])
// Same position
#expect(!constraints.isValidPosition(for: game, day: 2, sortOrder: 100))
// Different day
#expect(!constraints.isValidPosition(for: game, day: 1, sortOrder: 100))
// Different sortOrder
#expect(!constraints.isValidPosition(for: game, day: 2, sortOrder: 50))
}
@Test("success: custom note can be placed anywhere")
func success_customNotePlacedAnywhere() {
// Custom items can be placed before, between, or after games on any day
let constraints = makeConstraints(
tripDays: 3,
games: [
makeGameItem(city: "Chicago", day: 1, sortOrder: 100),
makeGameItem(city: "Chicago", day: 1, sortOrder: 200),
makeGameItem(city: "Detroit", day: 3, sortOrder: 100)
]
)
let custom = makeCustomItem(day: 1, sortOrder: 50)
// Before games on day 1
#expect(constraints.isValidPosition(for: custom, day: 1, sortOrder: 50))
// Between games on day 1
#expect(constraints.isValidPosition(for: custom, day: 1, sortOrder: 150))
// After games on day 1
#expect(constraints.isValidPosition(for: custom, day: 1, sortOrder: 250))
// On rest day (day 2)
#expect(constraints.isValidPosition(for: custom, day: 2, sortOrder: 50))
// On day 3 with different city games
#expect(constraints.isValidPosition(for: custom, day: 3, sortOrder: 50))
}
@Test("success: invalid position returns false (rejection)")
func success_invalidPositionReturnsRejection() {
// Travel segment cannot be placed before departure game
let constraints = makeConstraints(
tripDays: 3,
games: [makeGameItem(city: "Chicago", day: 2, sortOrder: 100)]
)
let travel = makeTravelItem(from: "Chicago", to: "Detroit", day: 1, sortOrder: 50)
// Day 1 is before Chicago game on Day 2, so invalid
#expect(!constraints.isValidPosition(for: travel, day: 1, sortOrder: 50))
}
# ItineraryConstraints API
**Location:** `SportsTime/Core/Models/Domain/ItineraryConstraints.swift`
**Verified by:** 23 tests in `SportsTimeTests/Domain/ItineraryConstraintsTests.swift`
## Overview
`ItineraryConstraints` validates item positions during drag-drop operations. It enforces:
- **Games cannot move** (CONS-01)
- **Travel segments have day range limits** (CONS-02)
- **Travel segments must respect game sortOrder on same day** (CONS-03)
- **Custom items have no constraints** (CONS-04)
## Construction
```swift
let constraints = ItineraryConstraints(
tripDayCount: days.count,
items: allItineraryItems // All items including games
)
Parameters:
tripDayCount: Total days in trip (1-indexed, so a 5-day trip has days 1-5)items: All itinerary items (games, travel, custom). Games are used to calculate constraints for travel items.
Public API
isValidPosition(for:day:sortOrder:) -> Bool
Check if a specific position is valid for an item.
func isValidPosition(for item: ItineraryItem, day: Int, sortOrder: Double) -> Bool
Usage during drag:
// On each drag position update
let dropPosition = calculateDropPosition(at: touchLocation)
let isValid = constraints.isValidPosition(
for: draggedItem,
day: dropPosition.day,
sortOrder: dropPosition.sortOrder
)
if isValid {
showValidDropIndicator()
} else {
showInvalidDropIndicator()
}
Returns:
true: Position is valid, allow dropfalse: Position is invalid, reject drop (snap back)
Rules by item type:
| Item Type | Day Constraint | SortOrder Constraint |
|---|---|---|
.game |
Always false |
Always false |
.travel |
Within valid day range | After departure games, before arrival games |
.custom |
Any day 1...tripDayCount | Any sortOrder |
validDayRange(for:) -> ClosedRange<Int>?
Get the valid day range for a travel item (for visual feedback).
func validDayRange(for item: ItineraryItem) -> ClosedRange<Int>?
Usage at drag start:
// When drag begins, precompute valid range
guard case .travel = draggedItem.kind,
let validRange = constraints.validDayRange(for: draggedItem) else {
// Not a travel item or impossible constraints
return
}
// Use range to dim invalid days
for day in 1...tripDayCount {
if !validRange.contains(day) {
dimDay(day)
}
}
Returns:
ClosedRange<Int>: Valid day range (e.g.,2...4)nil: Constraints are impossible (e.g., departure game after arrival game)
barrierGames(for:) -> [ItineraryItem]
Get games that constrain a travel item (for visual highlighting).
func barrierGames(for item: ItineraryItem) -> [ItineraryItem]
Usage for visual feedback:
// Highlight barrier games during drag
let barriers = constraints.barrierGames(for: travelItem)
for barrier in barriers {
highlightAsBarrier(barrier) // e.g., gold border
}
Returns:
- Array of game items: Last departure city game + first arrival city game
- Empty array: Not a travel item or no constraining games
Integration Points
ItineraryTableViewController (existing)
// In reloadData()
self.constraints = ItineraryConstraints(tripDayCount: tripDayCount, items: itineraryItems)
// In drag handling
if constraints.isValidPosition(for: draggedItem, day: targetDay, sortOrder: targetSortOrder) {
// Allow drop
} else {
// Reject drop, snap back
}
Phase 4 Implementation Notes
-
Drag Start:
- Check
item.isReorderable(games returnfalse) - Call
validDayRange(for:)to precompute valid days - Call
barrierGames(for:)to identify visual barriers
- Check
-
Drag Move:
- Calculate target (day, sortOrder) from touch position
- Call
isValidPosition(for:day:sortOrder:)for real-time feedback - Update insertion line (valid) or red indicator (invalid)
-
Drag End:
- Final
isValidPosition(for:day:sortOrder:)check - Valid: Update item's day/sortOrder, animate settle
- Invalid: Animate snap back, haptic feedback
- Final
Test Coverage
| Requirement | Tests | Verified |
|---|---|---|
| CONS-01 (games cannot move) | 2 | Yes |
| CONS-02 (travel day range) | 5 | Yes |
| CONS-03 (travel sortOrder) | 5 | Yes |
| CONS-04 (custom flexibility) | 4 | Yes |
| Edge cases | 7 | Yes |
| Total | 23 | 100% |
API documented: Phase 02 Ready for: Phase 04 (Drag Interaction)
</action>
<verify>File exists and contains all three public methods with usage examples</verify>
<done>CONSTRAINT-API.md created with complete API documentation</done>
</task>
<task type="auto">
<name>Task 3: Run full test suite and commit</name>
<files>None (verification only)</files>
<action>
1. Run full test suite to verify no regressions:
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test 2>&1 | grep -E "(Test Suite|Executed|passed|failed)"
2. Commit the edge case tests:
git add SportsTimeTests/Domain/ItineraryConstraintsTests.swift git commit -m "test(02-02): add edge case tests for constraint validation
Add 10 edge case tests:
- Single-day trip boundaries
- Day 0 and beyond-trip validation
- Exact sortOrder boundary behavior
- Travel with no games in cities
- Negative and large sortOrders
- Success criteria verification tests
Total: 23 constraint tests
Co-Authored-By: Claude Opus 4.5 noreply@anthropic.com"
3. Commit the API documentation:
git add .planning/phases/02-constraint-validation/CONSTRAINT-API.md git commit -m "docs(02-02): document ItineraryConstraints API for Phase 4
Document public API for drag-drop integration:
- isValidPosition() for position validation
- validDayRange() for precomputing valid days
- barrierGames() for visual highlighting
- Integration patterns for ItineraryTableViewController
Co-Authored-By: Claude Opus 4.5 noreply@anthropic.com"
</action>
<verify>Full test suite passes with no regressions</verify>
<done>Edge case tests and API documentation committed</done>
</task>
</tasks>
<verification>
After all tasks:
1. 23 total constraint tests pass (13 migrated + 10 edge cases)
2. Full test suite passes (no regressions)
3. CONSTRAINT-API.md exists with complete documentation
4. All commits follow project conventions
</verification>
<success_criteria>
- Edge cases tested: single-day, day boundaries, sortOrder boundaries, no-games scenarios
- Roadmap success criteria are verifiable by tests
- API documentation complete for Phase 4 integration
- All tests pass
</success_criteria>
<output>
After completion, create `.planning/phases/02-constraint-validation/02-02-SUMMARY.md`
</output>