refactor: model rule options as a per-kind sum type
Replace the wide BlockingRule/RuleDraft "god struct" — where every option existed for every kind — with a RuleConfiguration sum type that carries only the options each kind actually has: .schedule(ScheduleConfig: window + selectionMode + blockAdultContent) .timeLimit(TimeLimitConfig: dailyLimitMinutes) .openLimit(OpenLimitConfig: maxOpens) Name, days, Hard Mode, app list, and pause stay common to all kinds. This makes illegal states unrepresentable: Block / Allow Only and Block Adult Content are now structurally Schedule-only. User-visible behavior change: the Time Limit and Open Limit editors no longer offer a Block Adult Content toggle, and their detail sheets drop the "Adult websites" row — those never made sense for a usage budget. Limit rules are always Block and never engage the web-content filter. BlockingRule keeps flat columns as raw persistence behind a computed `configuration` bridge (lowest SwiftData risk; the cross-process RuleSnapshot wire format and the Screen Time extensions are untouched). Logic, the editors, and the detail sheet all switch on the sum type. Spec (§1, §3.5/§3.6, §5.2) updated first; tests reworked to the new API with new structural-guarantee unit tests and a UI test asserting the Time Limit editor/detail omit adult content. Full suite green (180 tests).
This commit is contained in:
@@ -63,7 +63,7 @@ struct RuleStatusTests {
|
||||
|
||||
@Test("Time-limit rules are never schedule-active")
|
||||
func timeLimitNeverActive() {
|
||||
let rule = BlockingRule(name: "Time Keeper", kind: .timeLimit)
|
||||
let rule = BlockingRule(name: "Time Keeper", configuration: .timeLimit(TimeLimitConfig()))
|
||||
let status = rule.status(at: date(2025, 1, 6, 10, 0), calendar: utc)
|
||||
#expect(!status.isActive)
|
||||
if case .upcoming = status {} else {
|
||||
@@ -116,7 +116,8 @@ struct RuleStatusTests {
|
||||
/// daily budget — never the vestigial 09:00 start as "Starts in 22h".
|
||||
@Test("Idle time-limit rule shows its daily budget, not a clock countdown")
|
||||
func timeLimitDisplayLabel() {
|
||||
let rule = BlockingRule(name: "Time Keeper", kind: .timeLimit, dailyLimitMinutes: 15)
|
||||
let rule = BlockingRule(
|
||||
name: "Time Keeper", configuration: .timeLimit(TimeLimitConfig(dailyLimitMinutes: 15)))
|
||||
let now = date(2025, 1, 6, 11, 38) // past the vestigial 09:00 window start
|
||||
let status = rule.status(at: now, calendar: utc)
|
||||
#expect(rule.statusLabel(for: status, relativeTo: now) == "15m / day")
|
||||
@@ -124,7 +125,8 @@ struct RuleStatusTests {
|
||||
|
||||
@Test("Idle open-limit rule shows its daily opens budget")
|
||||
func openLimitDisplayLabel() {
|
||||
let rule = BlockingRule(name: "Gate Keeper", kind: .openLimit, maxOpens: 5)
|
||||
let rule = BlockingRule(
|
||||
name: "Gate Keeper", configuration: .openLimit(OpenLimitConfig(maxOpens: 5)))
|
||||
let now = date(2025, 1, 6, 11, 38)
|
||||
let status = rule.status(at: now, calendar: utc)
|
||||
#expect(rule.statusLabel(for: status, relativeTo: now) == "5 opens / day")
|
||||
@@ -140,7 +142,8 @@ struct RuleStatusTests {
|
||||
|
||||
@Test("A spent time-limit budget still shows the blocked countdown")
|
||||
func timeLimitBlockingDisplayLabel() {
|
||||
let rule = BlockingRule(name: "Time Keeper", kind: .timeLimit, dailyLimitMinutes: 15)
|
||||
let rule = BlockingRule(
|
||||
name: "Time Keeper", configuration: .timeLimit(TimeLimitConfig(dailyLimitMinutes: 15)))
|
||||
let now = date(2025, 1, 6, 11, 38)
|
||||
let status = rule.status(at: now, calendar: utc, usage: RuleUsage(minutesUsed: 15))
|
||||
#expect(status.isActive)
|
||||
|
||||
Reference in New Issue
Block a user