fixes for testing and debug

Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
Matt Bruce 2026-02-09 09:14:08 -06:00
parent c1625e4c54
commit ce27a4473e
7 changed files with 143 additions and 68 deletions

View File

@ -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,
@ -24,43 +29,14 @@ struct AndromidaApp: App {
)
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
}
}
}

View File

@ -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)")
}
}
}

View File

@ -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
)
}
}

View File

@ -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
}

View File

@ -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())
}
}

View File

@ -47,7 +47,8 @@ struct AndromidaTests {
let store = RitualStore(
modelContext: container.mainContext,
seedService: EmptySeedService(),
settingsStore: TestFeedbackSettings()
settingsStore: TestFeedbackSettings(),
isRunningTests: true
)
store.createQuickRitual()

View File

@ -433,6 +433,7 @@ private func makeStore(now: @escaping () -> Date = Date.init) -> RitualStore {
modelContext: container.mainContext,
seedService: EmptySeedService(),
settingsStore: TestFeedbackSettings(),
isRunningTests: true,
now: now
)
}