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). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -79,13 +79,17 @@ struct UsageStatusTests {
|
||||
let mondayMorning = date(2025, 1, 6, 10, 0)
|
||||
|
||||
private func timeLimitRule(limit: Int = 45) -> BlockingRule {
|
||||
BlockingRule(name: "Time Keeper", kind: .timeLimit, days: Weekday.everyDay,
|
||||
dailyLimitMinutes: limit)
|
||||
BlockingRule(
|
||||
name: "Time Keeper",
|
||||
configuration: .timeLimit(TimeLimitConfig(dailyLimitMinutes: limit)),
|
||||
days: Weekday.everyDay)
|
||||
}
|
||||
|
||||
private func openLimitRule(maxOpens: Int = 5) -> BlockingRule {
|
||||
BlockingRule(name: "Gate Keeper", kind: .openLimit, days: Weekday.everyDay,
|
||||
maxOpens: maxOpens)
|
||||
BlockingRule(
|
||||
name: "Gate Keeper",
|
||||
configuration: .openLimit(OpenLimitConfig(maxOpens: maxOpens)),
|
||||
days: Weekday.everyDay)
|
||||
}
|
||||
|
||||
@Test("A time-limit rule with budget left is not active")
|
||||
@@ -153,13 +157,17 @@ struct UsageEnforcementTests {
|
||||
let ledger = MockUsageLedger()
|
||||
let enforcer = RuleEnforcer(shields: shields, usage: ledger)
|
||||
let rule = BlockingRule(
|
||||
name: "Time Keeper", kind: .timeLimit, days: Weekday.everyDay, dailyLimitMinutes: 45)
|
||||
name: "Time Keeper",
|
||||
configuration: .timeLimit(TimeLimitConfig(dailyLimitMinutes: 45)),
|
||||
days: Weekday.everyDay)
|
||||
ledger.usageByRule[rule.id] = RuleUsage(minutesUsed: 45)
|
||||
|
||||
enforcer.refresh(rules: [rule], at: mondayMorning, calendar: utc)
|
||||
|
||||
#expect(shields.shieldedRuleIDs == [rule.id])
|
||||
#expect(shields.appliedModes[rule.id] == .block)
|
||||
// Limit rules never engage the adult-content filter (Schedule-only).
|
||||
#expect(shields.appliedAdultContentFlags[rule.id] == false)
|
||||
}
|
||||
|
||||
@Test("A limit rule with budget left is not shielded")
|
||||
@@ -168,7 +176,9 @@ struct UsageEnforcementTests {
|
||||
let ledger = MockUsageLedger()
|
||||
let enforcer = RuleEnforcer(shields: shields, usage: ledger)
|
||||
let rule = BlockingRule(
|
||||
name: "Gate Keeper", kind: .openLimit, days: Weekday.everyDay, maxOpens: 5)
|
||||
name: "Gate Keeper",
|
||||
configuration: .openLimit(OpenLimitConfig(maxOpens: 5)),
|
||||
days: Weekday.everyDay)
|
||||
ledger.usageByRule[rule.id] = RuleUsage(opensUsed: 2)
|
||||
|
||||
enforcer.refresh(rules: [rule], at: mondayMorning, calendar: utc)
|
||||
@@ -181,9 +191,13 @@ struct UsageEnforcementTests {
|
||||
@Suite("Usage display strings")
|
||||
struct UsageDisplayTests {
|
||||
let timeRule = BlockingRule(
|
||||
name: "Time Keeper", kind: .timeLimit, days: Weekday.everyDay, dailyLimitMinutes: 45)
|
||||
name: "Time Keeper",
|
||||
configuration: .timeLimit(TimeLimitConfig(dailyLimitMinutes: 45)),
|
||||
days: Weekday.everyDay)
|
||||
let openRule = BlockingRule(
|
||||
name: "Gate Keeper", kind: .openLimit, days: Weekday.everyDay, maxOpens: 5)
|
||||
name: "Gate Keeper",
|
||||
configuration: .openLimit(OpenLimitConfig(maxOpens: 5)),
|
||||
days: Weekday.everyDay)
|
||||
|
||||
@Test("Time-limit rows show minutes used and remaining")
|
||||
func timeLimitStrings() {
|
||||
|
||||
Reference in New Issue
Block a user