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:
81
SeveredTests/RulePolicyTests.swift
Normal file
81
SeveredTests/RulePolicyTests.swift
Normal file
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// RulePolicyTests.swift
|
||||
// SeveredTests
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@testable import Severed
|
||||
|
||||
@MainActor
|
||||
@Suite("Hard Mode policy")
|
||||
struct RulePolicyTests {
|
||||
let mondayDuringWork = date(2025, 1, 6, 10, 0)
|
||||
let mondayEvening = date(2025, 1, 6, 19, 0)
|
||||
|
||||
func rule(hardMode: Bool) -> BlockingRule {
|
||||
BlockingRule(name: "Work Time", hardMode: hardMode)
|
||||
}
|
||||
|
||||
@Test("An active Hard Mode rule is locked")
|
||||
func hardLockedWhileActive() {
|
||||
let rule = rule(hardMode: true)
|
||||
#expect(RulePolicy.isHardLocked(rule, at: mondayDuringWork, calendar: utc))
|
||||
#expect(!RulePolicy.canEdit(rule, at: mondayDuringWork, calendar: utc))
|
||||
#expect(!RulePolicy.canDisable(rule, at: mondayDuringWork, calendar: utc))
|
||||
#expect(!RulePolicy.canDelete(rule, at: mondayDuringWork, calendar: utc))
|
||||
#expect(!RulePolicy.canUnblock(rule, at: mondayDuringWork, calendar: utc))
|
||||
#expect(!RulePolicy.canTurnOffHardMode(rule, at: mondayDuringWork, calendar: utc))
|
||||
}
|
||||
|
||||
@Test("A Hard Mode rule unlocks once its window ends")
|
||||
func unlockedOutsideWindow() {
|
||||
let rule = rule(hardMode: true)
|
||||
#expect(!RulePolicy.isHardLocked(rule, at: mondayEvening, calendar: utc))
|
||||
#expect(RulePolicy.canEdit(rule, at: mondayEvening, calendar: utc))
|
||||
#expect(RulePolicy.canDisable(rule, at: mondayEvening, calendar: utc))
|
||||
#expect(RulePolicy.canDelete(rule, at: mondayEvening, calendar: utc))
|
||||
#expect(RulePolicy.canTurnOffHardMode(rule, at: mondayEvening, calendar: utc))
|
||||
}
|
||||
|
||||
@Test("A disabled Hard Mode rule is not locked")
|
||||
func disabledRuleNotLocked() {
|
||||
let rule = rule(hardMode: true)
|
||||
rule.isEnabled = false
|
||||
#expect(!RulePolicy.isHardLocked(rule, at: mondayDuringWork, calendar: utc))
|
||||
#expect(RulePolicy.canEdit(rule, at: mondayDuringWork, calendar: utc))
|
||||
}
|
||||
|
||||
@Test("Active non-Hard-Mode rules may be unblocked")
|
||||
func softRuleUnblockable() {
|
||||
let rule = rule(hardMode: false)
|
||||
#expect(RulePolicy.canUnblock(rule, at: mondayDuringWork, calendar: utc))
|
||||
}
|
||||
|
||||
@Test("Unblocking pauses the rule until its window ends")
|
||||
func unblockPausesUntilWindowEnd() {
|
||||
let rule = rule(hardMode: false)
|
||||
let didUnblock = RulePolicy.unblock(rule, at: mondayDuringWork, calendar: utc)
|
||||
#expect(didUnblock)
|
||||
#expect(rule.pausedUntil == date(2025, 1, 6, 17, 0))
|
||||
#expect(rule.status(at: mondayDuringWork, calendar: utc)
|
||||
== .paused(until: date(2025, 1, 6, 17, 0)))
|
||||
}
|
||||
|
||||
@Test("Unblocking a Hard Mode rule is refused and changes nothing")
|
||||
func hardModeUnblockRefused() {
|
||||
let rule = rule(hardMode: true)
|
||||
let didUnblock = RulePolicy.unblock(rule, at: mondayDuringWork, calendar: utc)
|
||||
#expect(!didUnblock)
|
||||
#expect(rule.pausedUntil == nil)
|
||||
#expect(rule.status(at: mondayDuringWork, calendar: utc).isActive)
|
||||
}
|
||||
|
||||
@Test("Unblocking an inactive rule is refused")
|
||||
func inactiveUnblockRefused() {
|
||||
let rule = rule(hardMode: false)
|
||||
#expect(!RulePolicy.unblock(rule, at: mondayEvening, calendar: utc))
|
||||
#expect(rule.pausedUntil == nil)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user