feat: clone Opal's rules feature with hard block enforcement

Rebuild the app around recurring screen-time blocking rules modeled on
Opal's My Apps tab:

- Onboarding with FamilyControls Screen Time authorization
- Apps home with Blocked Apps tiles and live rule cards
- New Rule sheet: Schedule/Time Limit/Open Limit types + preset gallery
- Rule editors: time windows (incl. overnight), day picker, app selection
  via FamilyActivityPicker (Block/Allow Only), rename, Hold to Commit
- Hard Mode: active hard rules cannot be edited, disabled, deleted, or
  unblocked until their window ends; soft rules pause until next window
- Shield enforcement through per-rule ManagedSettingsStore + RuleEnforcer
- 73 unit tests (Swift Testing) + 16 UI tests (XCUITest) with launch-arg
  harness for in-memory storage, mocked authorization, seeded scenarios
- docs/RULES_FEATURE_SPEC.md: spec derived from the reference recording

Known gap: background window transitions and time/open-limit thresholds
need a DeviceActivityMonitor extension; shields currently sync while the
app is running.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-12 12:35:52 -04:00
parent 0c03e35ea9
commit e6c87baeba
45 changed files with 3749 additions and 206 deletions

View File

@@ -0,0 +1,36 @@
//
// TestSupport.swift
// SeveredTests
//
import Foundation
@testable import Severed
/// Fixed UTC gregorian calendar so schedule math is deterministic regardless
/// of the machine running the tests.
let utc: Calendar = {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(identifier: "UTC")!
return calendar
}()
/// Builds a date in the UTC test calendar. 2025-01-06 is a Monday; the tests
/// use that week (Jan 612, 2025) as their anchor.
func date(
_ year: Int, _ month: Int, _ day: Int, _ hour: Int = 0, _ minute: Int = 0
) -> Date {
utc.date(
from: DateComponents(year: year, month: month, day: day, hour: hour, minute: minute)
)!
}
/// Monday of the anchor week.
enum AnchorWeek {
static let monday = (year: 2025, month: 1, day: 6)
static let tuesday = (year: 2025, month: 1, day: 7)
static let wednesday = (year: 2025, month: 1, day: 8)
static let saturday = (year: 2025, month: 1, day: 11)
static let sunday = (year: 2025, month: 1, day: 12)
static let nextMonday = (year: 2025, month: 1, day: 13)
}