- Add Block Adult Content toggle to all three rule editors, persisted as BlockingRule.blockAdultContent (inline default for clean migration of existing stores) and surfaced in the detail sheet as an 'Adult websites: Blocked/Allowed' row - Engage Screen Time's adult-website filter (webContent.blockedByFilter = .auto()) alongside the rule's shield and clear it when the shield clears - Replace the New Rule sheet's view-swap with a NavigationStack push (navigationDestination(item:)); the editor uses native chrome there (system back button, inline title, toolbar rename), enabling the push animation and edge-swipe back - Tests: 93 passing (+2 unit: draft round-trip and enforcer forwarding; +3 UI: toggle-to-detail flow, default-allowed row, swipe-back as a behavioral proof of native navigation) - Spec updated accordingly (editor sections, behavior, data model, navigation note) Co-Authored-By: Claude <noreply@anthropic.com>
127 lines
4.4 KiB
Swift
127 lines
4.4 KiB
Swift
//
|
|
// RuleEnforcerTests.swift
|
|
// SeveredTests
|
|
//
|
|
|
|
import Foundation
|
|
import Testing
|
|
|
|
@testable import Severed
|
|
|
|
@MainActor
|
|
@Suite("Rule enforcement → shields")
|
|
struct RuleEnforcerTests {
|
|
let mondayDuringWork = date(2025, 1, 6, 10, 0)
|
|
let mondayEvening = date(2025, 1, 6, 19, 0)
|
|
|
|
@Test("Active schedule rules are shielded; inactive ones are not")
|
|
func shieldsActiveRules() {
|
|
let shields = MockShieldController()
|
|
let enforcer = RuleEnforcer(shields: shields)
|
|
let active = BlockingRule(name: "Work Time")
|
|
let weekendOnly = BlockingRule(name: "Weekend Zen", days: Weekday.weekends)
|
|
|
|
enforcer.refresh(rules: [active, weekendOnly], at: mondayDuringWork, calendar: utc)
|
|
|
|
#expect(shields.shieldedRuleIDs == [active.id])
|
|
#expect(enforcer.blockingRuleIDs == [active.id])
|
|
}
|
|
|
|
@Test("Disabled rules are never shielded")
|
|
func skipsDisabledRules() {
|
|
let shields = MockShieldController()
|
|
let enforcer = RuleEnforcer(shields: shields)
|
|
let rule = BlockingRule(name: "Work Time", isEnabled: false)
|
|
|
|
enforcer.refresh(rules: [rule], at: mondayDuringWork, calendar: utc)
|
|
|
|
#expect(shields.shieldedRuleIDs.isEmpty)
|
|
}
|
|
|
|
@Test("Paused rules are not shielded")
|
|
func skipsPausedRules() {
|
|
let shields = MockShieldController()
|
|
let enforcer = RuleEnforcer(shields: shields)
|
|
let rule = BlockingRule(name: "Work Time")
|
|
RulePolicy.unblock(rule, at: mondayDuringWork, calendar: utc)
|
|
|
|
enforcer.refresh(rules: [rule], at: mondayDuringWork, calendar: utc)
|
|
|
|
#expect(shields.shieldedRuleIDs.isEmpty)
|
|
}
|
|
|
|
@Test("Time-limit rules are not schedule-shielded")
|
|
func skipsTimeLimitRules() {
|
|
let shields = MockShieldController()
|
|
let enforcer = RuleEnforcer(shields: shields)
|
|
let rule = BlockingRule(name: "Time Keeper", kind: .timeLimit)
|
|
|
|
enforcer.refresh(rules: [rule], at: mondayDuringWork, calendar: utc)
|
|
|
|
#expect(shields.shieldedRuleIDs.isEmpty)
|
|
}
|
|
|
|
@Test("Shields are cleared when a window ends")
|
|
func clearsShieldAfterWindow() {
|
|
let shields = MockShieldController()
|
|
let enforcer = RuleEnforcer(shields: shields)
|
|
let rule = BlockingRule(name: "Work Time")
|
|
|
|
enforcer.refresh(rules: [rule], at: mondayDuringWork, calendar: utc)
|
|
#expect(shields.shieldedRuleIDs == [rule.id])
|
|
|
|
enforcer.refresh(rules: [rule], at: mondayEvening, calendar: utc)
|
|
#expect(shields.shieldedRuleIDs.isEmpty)
|
|
#expect(enforcer.blockingRuleIDs.isEmpty)
|
|
}
|
|
|
|
@Test("Shields are cleared when a rule is deleted")
|
|
func clearsShieldAfterDeletion() {
|
|
let shields = MockShieldController()
|
|
let enforcer = RuleEnforcer(shields: shields)
|
|
let rule = BlockingRule(name: "Work Time")
|
|
|
|
enforcer.refresh(rules: [rule], at: mondayDuringWork, calendar: utc)
|
|
enforcer.refresh(rules: [], at: mondayDuringWork, calendar: utc)
|
|
|
|
#expect(shields.shieldedRuleIDs.isEmpty)
|
|
}
|
|
|
|
@Test("Expired pauses are cleaned up during refresh")
|
|
func clearsExpiredPause() {
|
|
let shields = MockShieldController()
|
|
let enforcer = RuleEnforcer(shields: shields)
|
|
let rule = BlockingRule(name: "Work Time")
|
|
rule.pausedUntil = date(2025, 1, 6, 9, 30)
|
|
|
|
enforcer.refresh(rules: [rule], at: mondayDuringWork, calendar: utc)
|
|
|
|
#expect(rule.pausedUntil == nil)
|
|
#expect(shields.shieldedRuleIDs == [rule.id])
|
|
}
|
|
|
|
@Test("The selection mode is forwarded to the shield layer")
|
|
func forwardsSelectionMode() {
|
|
let shields = MockShieldController()
|
|
let enforcer = RuleEnforcer(shields: shields)
|
|
let rule = BlockingRule(name: "Focus", selectionMode: .allowOnly)
|
|
|
|
enforcer.refresh(rules: [rule], at: mondayDuringWork, calendar: utc)
|
|
|
|
#expect(shields.appliedModes[rule.id] == .allowOnly)
|
|
}
|
|
|
|
@Test("The adult-content flag is forwarded to the shield layer")
|
|
func forwardsAdultContentFlag() {
|
|
let shields = MockShieldController()
|
|
let enforcer = RuleEnforcer(shields: shields)
|
|
let filtered = BlockingRule(name: "Clean Mode", blockAdultContent: true)
|
|
let unfiltered = BlockingRule(name: "Plain")
|
|
|
|
enforcer.refresh(rules: [filtered, unfiltered], at: mondayDuringWork, calendar: utc)
|
|
|
|
#expect(shields.appliedAdultContentFlags[filtered.id] == true)
|
|
#expect(shields.appliedAdultContentFlags[unfiltered.id] == false)
|
|
}
|
|
}
|