From 6dbd6d1770eb331647e7e33ec2f0e9ab1c9ef2a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 13:59:03 +0000 Subject: [PATCH 1/2] fix(ci): start unit tests with an empty host app The menu bar scene kept the test host from finishing its launch on CI, so the test runner never connected. A separate entry point now starts an empty app under XCTest; the tests build what they need themselves. Refs #62 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TmmqmKmX8VDSfoPbzTS1ew --- App/AppLauncher.swift | 27 +++++++++++++++++++++++++++ App/StudyAppApp.swift | 1 - 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 App/AppLauncher.swift diff --git a/App/AppLauncher.swift b/App/AppLauncher.swift new file mode 100644 index 0000000..78bc748 --- /dev/null +++ b/App/AppLauncher.swift @@ -0,0 +1,27 @@ +import SwiftUI + +/// Entry point: unit tests get an empty host app, so launch work (database, sync, menu bar) never runs under XCTest. +@main +enum AppLauncher { + static func main() { + if isRunningUnitTests { + UnitTestHostApp.main() + } else { + StudyAppApp.main() + } + } + + private static var isRunningUnitTests: Bool { + let environment = ProcessInfo.processInfo.environment + return environment["XCTestConfigurationFilePath"] != nil + || environment["XCTestBundlePath"] != nil + || environment["XCTestSessionIdentifier"] != nil + } +} + +/// Host app for the unit tests; the tests build what they need themselves. +private struct UnitTestHostApp: App { + var body: some Scene { + WindowGroup { EmptyView() } + } +} diff --git a/App/StudyAppApp.swift b/App/StudyAppApp.swift index 5f1d012..9083e3b 100644 --- a/App/StudyAppApp.swift +++ b/App/StudyAppApp.swift @@ -3,7 +3,6 @@ import AppIntents import CoreSpotlight import SwiftUI -@main struct StudyAppApp: App { private let container: AppContainer private let autopilot: LectureAutopilot From 494012eb9810c342738610b31bc05c5032c4f0e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 13:59:03 +0000 Subject: [PATCH 2/2] docs(architecture): describe the app launcher Closes #62 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TmmqmKmX8VDSfoPbzTS1ew --- docs/ARCHITECTURE.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3255200..d087e42 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -6,7 +6,7 @@ StudyApp follows a pragmatic Clean Architecture (ports and adapters). The goal i ```text ┌──────────────────────────────────────────────────────────────┐ -│ App StudyAppApp (@main) · AppContainer · AppUpdater │ wires everything together +│ App AppLauncher (@main) · StudyAppApp · AppContainer │ wires everything together ├──────────────────────────────────────────────────────────────┤ │ Presentation SwiftUI views · @Observable view models │ ├──────────────────────────────────────────────────────────────┤ @@ -24,7 +24,7 @@ Dependencies point **inward**: Presentation and Data both depend on Domain, Doma | Domain | `Domain/` | Foundation | `Entities/` (value types such as `StudyModule`, `Semester`, `CourseFile`), `Repositories/` (protocols = ports), `UseCases/` (one application action each), `Support/` | | Data | `Data/` | Domain, SwiftData, PDFKit, Security, URLSession | `SwiftData/` (`@Model` records + mapping), `Repositories/` (SwiftData implementations), `Calendar/`, `StudIP/`, `Files/`, `PDF/`, `Preferences/` | | Presentation | `Presentation/` | Domain, SwiftUI, AppKit, WebKit | One folder per feature with views and view models; `DesignSystem/` for shared components and `Theme` | -| App | `App/` | everything | `AppContainer` (composition root), `StudyAppApp`, `AppUpdater` | +| App | `App/` | everything | `AppContainer` (composition root), `AppLauncher`, `StudyAppApp`, `AppUpdater` | ## Composition root @@ -32,6 +32,8 @@ Dependencies point **inward**: Presentation and Data both depend on Domain, Doma `AppContainer(inMemory: true)` swaps in in-memory persistence and preferences for previews and tests. +`AppLauncher` is the entry point: under XCTest it starts an empty host app instead of `StudyAppApp`, so launch work (database, sync, menu bar item) never runs during unit tests. On CI runners the menu bar scene kept the test host from finishing its launch. + ## Data flow example: calendar subscription → module 1. `StudyCalendarViewModel.subscribe(to:)` normalises the address (`CalendarFeedURL`) and calls `SyncCalendarSubscriptionUseCase`.