Extract domain, clients and UI into separate modules - #8
Open
ARamy23 wants to merge 3 commits into
Open
Conversation
Rewrites NightcapAppTests from XCTest to Swift Testing, organised as one @suite per feature with numbered Given/When/Then scenarios. Behaviour is characterised rather than changed: no production code is touched. Notable additions beyond a straight translation: - removeAppRequested had no coverage at all. Three scenarios now cover removing a running app, removing an idle one, and removing one of two running apps (the assertion must survive for the other). - The assertion reason string is asserted when a second watched app launches, so the pmset-visible reason stays truthful. - A scenario covers IOKit refusing the assertion, where the app must not claim the Mac is being kept awake. - test_terminate_event_keeps_assertion_when_another_instance_still_running previously asserted nothing. It now checks that release() is not called. Tests are given an in-memory file storage dependency. Swift Testing runs suites in parallel, and @shared(.fileStorage) would otherwise be shared mutable state across scenarios. scripts/check-domain-coverage.sh enforces a floor on pure-domain coverage from an .xcresult bundle. Live adapters (NSWorkspace, IOKit, SMAppService, StoreKit) and SwiftUI views are excluded by design; covering those means integration tests, not characterisation. 20 scenarios, all passing. Domain coverage 96.04% (291/303), up from 93.70% (284/303). Claude-Session: https://claude.ai/code/session_01WGsVLm7Vs83QN4o1tCaCLw
Splits the app into three modules inside one local SwiftPM package: - NightcapDomain: WatchedApp, LaunchAtLoginStatus, AppFeature, and all dependency-client interfaces. No AppKit, IOKit, ServiceManagement or StoreKit, so it can build for iOS and watchOS. - NightcapClients: the live DependencyKey conformances. The only module that touches platform frameworks. - NightcapUI: the SwiftUI menu views. AppFeature.swift previously imported AppKit without using a single AppKit symbol; the reducer was already pure and that import is now gone. The package builds standalone (`swift build` in Packages/NightcapKit succeeds). The generated Xcode project does NOT yet consume it: Xcode never registers the XCLocalSwiftPackageReference, and NightcapKit is absent from SourcePackages/workspace-state.json, so all three products report as "Missing package product". Committed as WIP so the extraction is not lost while that is resolved. Claude-Session: https://claude.ai/code/session_01WGsVLm7Vs83QN4o1tCaCLw
Splits the single app target into three modules with compiler-enforced boundaries: - NightcapDomain: WatchedApp, LaunchAtLoginStatus, AppFeature, and the dependency-client interfaces. Imports no platform frameworks, so it can be reused by iOS and watchOS targets later. - NightcapClients: the live DependencyKey conformances. The only module that touches AppKit, IOKit, ServiceManagement and StoreKit. - NightcapUI: the SwiftUI menu views. Client interfaces are separated from implementations using the standard swift-dependencies split: the @DependencyClient struct and its TestDependencyKey live in the domain, the liveValue conformance lives in NightcapClients. AppFeature.swift imported AppKit without using a single AppKit symbol. The reducer was already pure, so the extraction started by deleting that import. Modules are built as static framework targets rather than local SwiftPM packages. Xcode would not register a local package reference for this project: NightcapKit never appeared in SourcePackages/workspace-state.json and all products reported "Missing package product", despite a manifest that builds fine under `swift build` and matches a working setup elsewhere. Static linking also avoids embedding and signing three extra dynamic frameworks. Package.swift manifests are kept alongside so the modules remain consumable by SwiftPM directly. Shipping parity verified on the built app: LSUIElement true, category unchanged, app-sandbox and files.user-selected.read-only intact, and no Nightcap frameworks embedded (statically linked). All 20 scenarios still pass. Domain coverage is 93.11%, down from 96.04%, entirely because LaunchAtLoginStatus.init(SMAppService.Status) was previously counted as covered by the test host app exercising the live code path at launch, not by any test. That code now lives in NightcapClients, so the number reflects real test coverage. Claude-Session: https://claude.ai/code/session_01WGsVLm7Vs83QN4o1tCaCLw
This was referenced Jul 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #7 — review that first.
Splits the single app target into three modules with compiler-enforced boundaries:
NightcapDomainWatchedApp,LaunchAtLoginStatus,AppFeature, client interfacesNightcapClientsDependencyKeyconformancesNightcapUIClient interfaces are split from implementations using the standard swift-dependencies pattern: the
@DependencyClientstruct +TestDependencyKeyin the domain,liveValueinNightcapClients.AppFeature.swiftimported AppKit without using a single AppKit symbol — the reducer was already pure, so the extraction opened by deleting that one line.Why static framework targets, not local SwiftPM packages
I tried local packages first. Xcode would not register the local package reference for this project —
NightcapKitnever appeared inSourcePackages/workspace-state.json, and all three products reportedMissing package product, while all 14 remote packages resolved fine. That reproduced across: one package vs three, package-name-matching-product-name, a workspace wrapper,fileGroups, hand-patchingpackage =into the product dependencies, and clean DerivedData + reopen each time. The manifests build fine underswift buildand match the layout of another xcodegen project on this machine that works.Static framework targets sidestep it and additionally avoid embedding and signing three dynamic frameworks. The
Package.swiftmanifests are kept, so the modules stay consumable by SwiftPM directly — which is what the iOS and watchOS targets will use.Verification
Shipping parity checked on the built app:
LSUIElement= true, category unchangedcom.apple.security.app-sandboxandfiles.user-selected.read-onlyintactAll 20 scenarios pass.
One coverage number moved, and it's honest
Domain coverage 96.04% → 93.11%.
LaunchAtLoginStatuswent 100% → 30.77% — but that 100% was never earned by a test. It came from the test host app launching and running the liveSMAppServicepath. That code now lives inNightcapClients, so the figure reflects what tests actually cover. Still comfortably over the 80% gate.Note for contributors
project.ymlpinsDEVELOPMENT_TEAM, so anyone outside that team can't build.project.local.yml(gitignored) overrides it with ad-hoc signing:https://claude.ai/code/session_01WGsVLm7Vs83QN4o1tCaCLw