Skip to content

Extract domain, clients and UI into separate modules - #8

Open
ARamy23 wants to merge 3 commits into
Abdo-codes:mainfrom
ARamy23:refactor/extract-packages
Open

Extract domain, clients and UI into separate modules#8
ARamy23 wants to merge 3 commits into
Abdo-codes:mainfrom
ARamy23:refactor/extract-packages

Conversation

@ARamy23

@ARamy23 ARamy23 commented Jul 28, 2026

Copy link
Copy Markdown

Stacked on #7 — review that first.

Splits the single app target into three modules with compiler-enforced boundaries:

Module Contains May import
NightcapDomain WatchedApp, LaunchAtLoginStatus, AppFeature, client interfaces nothing platform-specific
NightcapClients live DependencyKey conformances AppKit, IOKit, ServiceManagement, StoreKit
NightcapUI the SwiftUI menu views SwiftUI

Client interfaces are split from implementations using the standard swift-dependencies pattern: the @DependencyClient struct + TestDependencyKey in the domain, liveValue in NightcapClients.

AppFeature.swift imported 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 — NightcapKit never appeared in SourcePackages/workspace-state.json, and all three products reported Missing 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-patching package = into the product dependencies, and clean DerivedData + reopen each time. The manifests build fine under swift build and 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.swift manifests 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 unchanged
  • com.apple.security.app-sandbox and files.user-selected.read-only intact
  • no Nightcap frameworks embedded (statically linked in)

All 20 scenarios pass.

One coverage number moved, and it's honest

Domain coverage 96.04% → 93.11%. LaunchAtLoginStatus went 100% → 30.77% — but that 100% was never earned by a test. It came from the test host app launching and running the live SMAppService path. That code now lives in NightcapClients, so the figure reflects what tests actually cover. Still comfortably over the 80% gate.

Note for contributors

project.yml pins DEVELOPMENT_TEAM, so anyone outside that team can't build. project.local.yml (gitignored) overrides it with ad-hoc signing:

xcodegen --spec project.local.yml

https://claude.ai/code/session_01WGsVLm7Vs83QN4o1tCaCLw

ARamy23 added 3 commits July 29, 2026 00:12
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant