feat: introduce reusable app lists for rules

- AppList @Model with launch migration from legacy inline selections
  (rules with identical selections share one list; idempotent)
- rules point at one app list; Block/Allow Only belongs to the rule and
  is offered only in the Schedule editor (limit kinds sanitize to Block)
- app-list picker sheet (select / create / edit / delete with in-use
  protection) replaces the per-rule selection sheet
- Hard Mode locks app-list editing while any hard rule is blocking
- SwiftData stability: relationships are only wired between managed
  models, and unit tests share one in-memory container (fresh context +
  data wipe per test) — per-test container creation trapped
  intermittently inside SwiftData
This commit is contained in:
2026-06-12 20:02:35 -04:00
parent 05d48a9f34
commit 93f2eceb3a
22 changed files with 880 additions and 138 deletions

View File

@@ -47,11 +47,7 @@ struct RuleModelTests {
@Test("Rules persist and fetch through SwiftData")
func persistence() throws {
let container = try ModelContainer(
for: BlockingRule.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
)
let context = container.mainContext
let context = try makeInMemoryContext()
let rule = BlockingRule(
name: "Deep Sleep", hardMode: true,
days: Weekday.everyDay, startMinutes: 22 * 60, endMinutes: 6 * 60
@@ -83,7 +79,11 @@ struct RuleDraftTests {
}
@Test("Draft → rule → draft round-trips every field")
func roundTrip() {
func roundTrip() throws {
let context = try makeInMemoryContext()
let list = AppList(name: "Distractions", selectionCount: 3)
context.insert(list)
var draft = RuleDraft(kind: .schedule)
draft.name = "Locked In"
draft.days = Weekday.everyDay
@@ -92,16 +92,18 @@ struct RuleDraftTests {
draft.hardMode = true
draft.blockAdultContent = true
draft.selectionMode = .allowOnly
draft.selectionCount = 3
draft.appList = list
let rule = draft.makeRule()
let rule = draft.insertRule(into: context)
#expect(rule.blockAdultContent)
#expect(RuleDraft(rule: rule) == draft)
}
@Test("Applying a draft updates an existing rule")
func applyToExisting() {
func applyToExisting() throws {
let context = try makeInMemoryContext()
let rule = BlockingRule(name: "Old Name")
context.insert(rule)
var draft = RuleDraft(rule: rule)
draft.name = "New Name"
draft.hardMode = true