From ce27a4473edebb333a49129d85a7511089670dc4 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 9 Feb 2026 09:14:08 -0600 Subject: [PATCH] fixes for testing and debug Signed-off-by: Matt Bruce --- Andromida/AndromidaApp.swift | 73 +++--------- .../App/Services/ReminderScheduler.swift | 15 +-- Andromida/App/State/RitualStore+Preview.swift | 7 +- Andromida/App/State/RitualStore.swift | 7 +- Andromida/App/Support/AppLaunchContext.swift | 105 ++++++++++++++++++ AndromidaTests/AndromidaTests.swift | 3 +- AndromidaTests/RitualStoreTests.swift | 1 + 7 files changed, 143 insertions(+), 68 deletions(-) create mode 100644 Andromida/App/Support/AppLaunchContext.swift diff --git a/Andromida/AndromidaApp.swift b/Andromida/AndromidaApp.swift index ea6c1c7..435f03c 100644 --- a/Andromida/AndromidaApp.swift +++ b/Andromida/AndromidaApp.swift @@ -5,6 +5,7 @@ import Bedrock @main struct AndromidaApp: App { private let modelContainer: ModelContainer + private let launchContext: AppLaunchContext @State private var store: RitualStore @State private var settingsStore: SettingsStore @State private var categoryStore: CategoryStore @@ -15,6 +16,10 @@ struct AndromidaApp: App { @State private var isTransitioningToRoot = false init() { + #if !DEBUG + Design.showDebugLogs = false + #endif + // Register app's color theme for Bedrock components Theme.register( text: AppTextColors.self, @@ -23,44 +28,15 @@ struct AndromidaApp: App { status: AppStatus.self ) Theme.register(border: AppBorder.self) - - let environment = ProcessInfo.processInfo.environment - let isRunningTests = environment["XCTestConfigurationFilePath"] != nil - let isUITesting = environment["UITEST_MODE"] == "1" - if isUITesting { - if environment["UITEST_RESET_USER_DEFAULTS"] == "1", - let bundleIdentifier = Bundle.main.bundleIdentifier { - UserDefaults.standard.removePersistentDomain(forName: bundleIdentifier) - } - if let completedValue = environment["UITEST_HAS_COMPLETED_SETUP_WIZARD"] { - let hasCompleted = completedValue == "1" || completedValue.lowercased() == "true" - UserDefaults.standard.set(hasCompleted, forKey: "hasCompletedSetupWizard") - } - } + let launchContext = AppLaunchContext() + self.launchContext = launchContext + launchContext.applyUserDefaultsOverrides(bundleIdentifier: Bundle.main.bundleIdentifier) // Include all models in schema - Ritual, RitualArc, and ArcHabit let schema = Schema([Ritual.self, RitualArc.self, ArcHabit.self]) - let configuration: ModelConfiguration - if isUITesting { - // UI tests should always run with isolated in-memory persistence. - configuration = ModelConfiguration( - schema: schema, - isStoredInMemoryOnly: true, - cloudKitDatabase: .none - ) - } else { - // Use App Group for shared container between app and widget. - // Disable CloudKit mirroring under XCTest to keep simulator tests deterministic. - let storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: AppIdentifiers.appGroupIdentifier)? - .appendingPathComponent("Andromida.sqlite") ?? URL.documentsDirectory.appendingPathComponent("Andromida.sqlite") - configuration = ModelConfiguration( - schema: schema, - url: storeURL, - cloudKitDatabase: isRunningTests ? .none : .private(AppIdentifiers.cloudKitContainerIdentifier) - ) - } + let configuration = launchContext.modelConfiguration(for: schema) let container: ModelContainer do { @@ -73,15 +49,13 @@ struct AndromidaApp: App { _settingsStore = State(initialValue: settings) _categoryStore = State(initialValue: CategoryStore()) - let ritualStore = RitualStore(modelContext: container.mainContext, seedService: RitualSeedService(), settingsStore: settings) - if isUITesting, environment["UITEST_SEED_THREE_PRESETS"] == "1" { - ritualStore.createRitualFromPreset(RitualPresetLibrary.healthPresets[0]) // morning - ritualStore.createRitualFromPreset(RitualPresetLibrary.healthPresets[1]) // midday - ritualStore.createRitualFromPreset(RitualPresetLibrary.healthPresets[3]) // evening - } - if isUITesting, environment["UITEST_PRELOAD_DEMO_DATA"] == "1" { - ritualStore.preloadDemoData() - } + let ritualStore = RitualStore( + modelContext: container.mainContext, + seedService: RitualSeedService(), + settingsStore: settings, + isRunningTests: launchContext.isRunningTests + ) + launchContext.applyUITestSeeding(to: ritualStore) _store = State(initialValue: ritualStore) } @@ -92,13 +66,12 @@ struct AndromidaApp: App { .ignoresSafeArea() if hasCompletedSetupWizard { - let uiTestInitialTab = uiTestRequestedInitialTab() // Main app - start on Rituals tab if just completed wizard RootView( store: store, settingsStore: settingsStore, categoryStore: categoryStore, - initialTab: uiTestInitialTab ?? (justCompletedWizard ? .rituals : .today) + initialTab: launchContext.initialTabOverride ?? (justCompletedWizard ? .rituals : .today) ) .transition(.opacity) } else { @@ -126,16 +99,4 @@ struct AndromidaApp: App { .preferredColorScheme(settingsStore.theme.colorScheme) } } - - private func uiTestRequestedInitialTab() -> RootView.RootTab? { - guard ProcessInfo.processInfo.environment["UITEST_MODE"] == "1" else { return nil } - switch ProcessInfo.processInfo.environment["UITEST_INITIAL_TAB"]?.lowercased() { - case "today": return .today - case "rituals": return .rituals - case "insights": return .insights - case "history": return .history - case "settings": return .settings - default: return nil - } - } } diff --git a/Andromida/App/Services/ReminderScheduler.swift b/Andromida/App/Services/ReminderScheduler.swift index 3294c27..7d0e24e 100644 --- a/Andromida/App/Services/ReminderScheduler.swift +++ b/Andromida/App/Services/ReminderScheduler.swift @@ -1,6 +1,7 @@ import Foundation import UserNotifications import Observation +import Bedrock /// Reminder time slots based on ritual TimeOfDay values. /// Groups similar times to avoid excessive notifications. @@ -85,7 +86,7 @@ final class ReminderScheduler: NSObject, UNUserNotificationCenterDelegate { willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { - print("🔔 Notification will present in foreground: \(notification.request.identifier)") + Design.debugLog("🔔 Notification will present in foreground: \(notification.request.identifier)") // Show the notification even when the app is in the foreground completionHandler([.banner, .list, .sound, .badge]) } @@ -95,7 +96,7 @@ final class ReminderScheduler: NSObject, UNUserNotificationCenterDelegate { didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { - print("🔔 Notification received/tapped: \(response.notification.request.identifier)") + Design.debugLog("🔔 Notification received/tapped: \(response.notification.request.identifier)") // Clear badge when user interacts with notification clearBadge() shouldNavigateToToday = true @@ -127,7 +128,7 @@ final class ReminderScheduler: NSObject, UNUserNotificationCenterDelegate { await refreshAuthorizationStatus() return granted } catch { - print("Notification authorization error: \(error)") + Design.debugLog("Notification authorization error: \(error)") return false } } @@ -147,7 +148,7 @@ final class ReminderScheduler: NSObject, UNUserNotificationCenterDelegate { /// Schedules a test notification to appear in 5 seconds. func scheduleTestNotification() { - print("🔔 Attempting to schedule test notification...") + Design.debugLog("🔔 Attempting to schedule test notification...") let content = UNMutableNotificationContent() content.title = String(localized: "Test Notification") @@ -164,9 +165,9 @@ final class ReminderScheduler: NSObject, UNUserNotificationCenterDelegate { UNUserNotificationCenter.current().add(request) { error in if let error = error { - print("❌ Failed to schedule test notification: \(error)") + Design.debugLog("❌ Failed to schedule test notification: \(error)") } else { - print("✅ Test notification scheduled successfully! It should appear in 5 seconds.") + Design.debugLog("✅ Test notification scheduled successfully! It should appear in 5 seconds.") } } } @@ -257,7 +258,7 @@ final class ReminderScheduler: NSObject, UNUserNotificationCenterDelegate { do { try await UNUserNotificationCenter.current().add(request) } catch { - print("Failed to schedule \(slot.rawValue) reminder: \(error)") + Design.debugLog("Failed to schedule \(slot.rawValue) reminder: \(error)") } } } diff --git a/Andromida/App/State/RitualStore+Preview.swift b/Andromida/App/State/RitualStore+Preview.swift index 73b90cc..7495cbb 100644 --- a/Andromida/App/State/RitualStore+Preview.swift +++ b/Andromida/App/State/RitualStore+Preview.swift @@ -11,6 +11,11 @@ extension RitualStore { } catch { fatalError("Preview container failed: \(error)") } - return RitualStore(modelContext: container.mainContext, seedService: RitualSeedService(), settingsStore: SettingsStore()) + return RitualStore( + modelContext: container.mainContext, + seedService: RitualSeedService(), + settingsStore: SettingsStore(), + isRunningTests: false + ) } } diff --git a/Andromida/App/State/RitualStore.swift b/Andromida/App/State/RitualStore.swift index 6ed042e..907f4da 100644 --- a/Andromida/App/State/RitualStore.swift +++ b/Andromida/App/State/RitualStore.swift @@ -59,6 +59,7 @@ final class RitualStore: RitualStoreProviding { modelContext: ModelContext, seedService: RitualSeedProviding, settingsStore: any RitualFeedbackSettingsProviding, + isRunningTests: Bool, calendar: Calendar = .current, now: @escaping () -> Date = Date.init ) { @@ -67,7 +68,7 @@ final class RitualStore: RitualStoreProviding { self.settingsStore = settingsStore self.calendar = calendar self.nowProvider = now - self.isRunningTests = ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil + self.isRunningTests = isRunningTests self.dayFormatter = DateFormatter() self.displayFormatter = DateFormatter() dayFormatter.calendar = calendar @@ -1556,7 +1557,7 @@ final class RitualStore: RitualStoreProviding { // Find the first ritual with an active arc guard let ritual = currentRituals.first, let arc = ritual.activeArc(on: now()) else { - print("No active arcs to complete") + Design.debugLog("No active arcs to complete") return } @@ -1573,7 +1574,7 @@ final class RitualStore: RitualStoreProviding { // Trigger the completion check - this will set ritualNeedingRenewal checkForCompletedArcs() - print("Arc '\(ritual.title)' marked as completed. Navigate to Today tab to see renewal prompt.") + Design.debugLog("Arc '\(ritual.title)' marked as completed. Navigate to Today tab to see renewal prompt.") } #endif } diff --git a/Andromida/App/Support/AppLaunchContext.swift b/Andromida/App/Support/AppLaunchContext.swift new file mode 100644 index 0000000..8598b8f --- /dev/null +++ b/Andromida/App/Support/AppLaunchContext.swift @@ -0,0 +1,105 @@ +import Foundation +import SwiftData + +struct AppLaunchContext { + enum LaunchTab: String { + case today + case rituals + case insights + case history + case settings + } + + let isRunningTests: Bool + let isUITesting: Bool + let shouldResetUserDefaults: Bool + let hasCompletedSetupWizardOverride: Bool? + let shouldSeedThreePresets: Bool + let shouldPreloadDemoData: Bool + let requestedInitialTab: LaunchTab? + + init(environment: [String: String] = ProcessInfo.processInfo.environment) { + isRunningTests = environment["XCTestConfigurationFilePath"] != nil + isUITesting = Self.isEnabled(environment["UITEST_MODE"]) + shouldResetUserDefaults = Self.isEnabled(environment["UITEST_RESET_USER_DEFAULTS"]) + hasCompletedSetupWizardOverride = Self.optionalBool(environment["UITEST_HAS_COMPLETED_SETUP_WIZARD"]) + shouldSeedThreePresets = Self.isEnabled(environment["UITEST_SEED_THREE_PRESETS"]) + shouldPreloadDemoData = Self.isEnabled(environment["UITEST_PRELOAD_DEMO_DATA"]) + requestedInitialTab = Self.parseTab(environment["UITEST_INITIAL_TAB"]) + } + + func applyUserDefaultsOverrides(bundleIdentifier: String?) { + guard isUITesting else { return } + + if shouldResetUserDefaults, let bundleIdentifier { + UserDefaults.standard.removePersistentDomain(forName: bundleIdentifier) + } + + if let hasCompletedSetupWizardOverride { + UserDefaults.standard.set(hasCompletedSetupWizardOverride, forKey: "hasCompletedSetupWizard") + } + } + + func modelConfiguration(for schema: Schema) -> ModelConfiguration { + if isUITesting { + // UI tests should always run with isolated in-memory persistence. + return ModelConfiguration( + schema: schema, + isStoredInMemoryOnly: true, + cloudKitDatabase: .none + ) + } + + // Use App Group for shared container between app and widget. + // Disable CloudKit mirroring under XCTest to keep simulator tests deterministic. + let storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: AppIdentifiers.appGroupIdentifier)? + .appendingPathComponent("Andromida.sqlite") ?? URL.documentsDirectory.appendingPathComponent("Andromida.sqlite") + + return ModelConfiguration( + schema: schema, + url: storeURL, + cloudKitDatabase: isRunningTests ? .none : .private(AppIdentifiers.cloudKitContainerIdentifier) + ) + } + + func applyUITestSeeding(to ritualStore: RitualStore) { + guard isUITesting else { return } + + if shouldSeedThreePresets { + ritualStore.createRitualFromPreset(RitualPresetLibrary.healthPresets[0]) // morning + ritualStore.createRitualFromPreset(RitualPresetLibrary.healthPresets[1]) // midday + ritualStore.createRitualFromPreset(RitualPresetLibrary.healthPresets[3]) // evening + } + + #if DEBUG + if shouldPreloadDemoData { + ritualStore.preloadDemoData() + } + #endif + } + + var initialTabOverride: RootView.RootTab? { + guard let requestedInitialTab else { return nil } + switch requestedInitialTab { + case .today: return .today + case .rituals: return .rituals + case .insights: return .insights + case .history: return .history + case .settings: return .settings + } + } + + private static func isEnabled(_ value: String?) -> Bool { + optionalBool(value) ?? false + } + + private static func optionalBool(_ value: String?) -> Bool? { + guard let value else { return nil } + return value == "1" || value.lowercased() == "true" + } + + private static func parseTab(_ value: String?) -> LaunchTab? { + guard let value else { return nil } + return LaunchTab(rawValue: value.lowercased()) + } +} diff --git a/AndromidaTests/AndromidaTests.swift b/AndromidaTests/AndromidaTests.swift index 870f5b7..6aba468 100644 --- a/AndromidaTests/AndromidaTests.swift +++ b/AndromidaTests/AndromidaTests.swift @@ -47,7 +47,8 @@ struct AndromidaTests { let store = RitualStore( modelContext: container.mainContext, seedService: EmptySeedService(), - settingsStore: TestFeedbackSettings() + settingsStore: TestFeedbackSettings(), + isRunningTests: true ) store.createQuickRitual() diff --git a/AndromidaTests/RitualStoreTests.swift b/AndromidaTests/RitualStoreTests.swift index 81c96b8..db849e6 100644 --- a/AndromidaTests/RitualStoreTests.swift +++ b/AndromidaTests/RitualStoreTests.swift @@ -433,6 +433,7 @@ private func makeStore(now: @escaping () -> Date = Date.init) -> RitualStore { modelContext: container.mainContext, seedService: EmptySeedService(), settingsStore: TestFeedbackSettings(), + isRunningTests: true, now: now ) }