From e236a83d795d25f7f550d97cc073e148a270292c Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Sat, 31 Oct 2026 15:13:02 -0400 Subject: [PATCH 01/10] test(async): await work completion and isolate drive fixtures --- Sources/DilatedUI/Automation/Downloads.swift | 2 + Sources/DilatedUI/Automation/WiFi.swift | 6 +- .../Feedback/FeedbackController.swift | 2 + .../DilatedCoreTests/PackageHelperTests.swift | 15 ++- Tests/DilatedCoreTests/TriggerTests.swift | 4 +- Tests/DilatedUITests/DownloadTests.swift | 14 +- Tests/DilatedUITests/DriveAliveTests.swift | 127 +++++++++--------- Tests/DilatedUITests/FeedbackTests.swift | 12 +- Tests/DilatedUITests/WiFiTests.swift | 15 ++- 9 files changed, 123 insertions(+), 74 deletions(-) diff --git a/Sources/DilatedUI/Automation/Downloads.swift b/Sources/DilatedUI/Automation/Downloads.swift index 645bc9c..21792f5 100644 --- a/Sources/DilatedUI/Automation/Downloads.swift +++ b/Sources/DilatedUI/Automation/Downloads.swift @@ -190,6 +190,8 @@ private final class DownloadEventTarget: Sendable { requestRefresh() } + var pendingReadForTesting: Task? { readTask } + func tick() { guard active, let quietDeadline, clock() >= quietDeadline, readTask == nil else { return } requestRefresh() diff --git a/Sources/DilatedUI/Automation/WiFi.swift b/Sources/DilatedUI/Automation/WiFi.swift index 73c908e..98e23c6 100644 --- a/Sources/DilatedUI/Automation/WiFi.swift +++ b/Sources/DilatedUI/Automation/WiFi.swift @@ -133,6 +133,8 @@ private final class NativeWiFiAuthorization: NSObject, WiFiAuthorizationSource, } @MainActor final class WiFiObserver { + typealias Schedule = @MainActor (@escaping @MainActor @Sendable () -> Void) -> Void + private let schedule: Schedule private let suppliedAccess: WiFiAccess? private let readSSIDs: () -> Set? private let changed: () -> Void @@ -144,8 +146,10 @@ private final class NativeWiFiAuthorization: NSObject, WiFiAuthorizationSource, init( access: WiFiAccess? = nil, readSSIDs: (() -> Set?)? = nil, + schedule: @escaping Schedule = { action in Task { @MainActor in action() } }, changed: @escaping () -> Void ) { + self.schedule = schedule suppliedAccess = access self.readSSIDs = readSSIDs ?? { WiFiCatalog.read() } self.changed = changed @@ -187,7 +191,7 @@ private final class NativeWiFiAuthorization: NSObject, WiFiAuthorizationSource, guard !notificationPending else { return } notificationPending = true let generation = generation - Task { @MainActor [weak self] in + schedule { [weak self] in guard let self, self.access != nil, self.generation == generation else { return } self.notificationPending = false self.changed() diff --git a/Sources/DilatedUI/Feedback/FeedbackController.swift b/Sources/DilatedUI/Feedback/FeedbackController.swift index 382a8e3..c3c40e0 100644 --- a/Sources/DilatedUI/Feedback/FeedbackController.swift +++ b/Sources/DilatedUI/Feedback/FeedbackController.swift @@ -54,6 +54,8 @@ struct FeedbackNotice: Sendable { @ObservationIgnored private var requests: [FeedbackNotice.Kind: UUID] = [:] @ObservationIgnored private var tasks: [FeedbackNotice.Kind: Task] = [:] + var pendingDeliveriesForTesting: [Task] { Array(tasks.values) } + init(service: any FeedbackService, sounds: any FeedbackSoundPlaying) { self.service = service self.sounds = sounds diff --git a/Tests/DilatedCoreTests/PackageHelperTests.swift b/Tests/DilatedCoreTests/PackageHelperTests.swift index b63c1ab..25be382 100644 --- a/Tests/DilatedCoreTests/PackageHelperTests.swift +++ b/Tests/DilatedCoreTests/PackageHelperTests.swift @@ -19,7 +19,20 @@ struct PackageHelperTests { process.waitUntilExit() } } - try await Task.sleep(for: .milliseconds(50)) + let deadline = ContinuousClock.now.advanced(by: .seconds(5)) + let pattern = "^" + NSRegularExpression.escapedPattern(for: executable.path) + "($|[[:space:]])" + var ready = false + while ContinuousClock.now < deadline { + let found = try await BoundedProcess.run( + executable: URL(fileURLWithPath: "/usr/bin/pgrep"), + arguments: ["-f", pattern], timeout: 0.5) + if !found.timedOut, found.status == 0 { + ready = true + break + } + try await Task.sleep(for: .milliseconds(20)) + } + try #require(ready, "The child did not adopt the expected app identity.") await #expect(throws: (any Error).self) { try await PackageHelperCheck.verifyAppAbsent(executable: executable) } diff --git a/Tests/DilatedCoreTests/TriggerTests.swift b/Tests/DilatedCoreTests/TriggerTests.swift index 44b33db..de06a93 100644 --- a/Tests/DilatedCoreTests/TriggerTests.swift +++ b/Tests/DilatedCoreTests/TriggerTests.swift @@ -21,7 +21,9 @@ struct TriggerTests { var input = TriggerInputs() input.processNames = ["make"] evaluator.reconcile(rules: [rule], inputs: input, engine: &engine, now: start) - #expect(evaluator.activeHoldIDs[rule.id] == engine.holds.first?.id) + try #require(engine.holds.count == 1) + let hold = try #require(engine.holds.first) + #expect(evaluator.activeHoldIDs[rule.id] == hold.id) #expect(!engine.desiredState.closedLid) evaluator.reconcile(rules: [], inputs: input, engine: &engine, now: start) #expect(evaluator.activeHoldIDs.isEmpty && engine.holds.isEmpty) diff --git a/Tests/DilatedUITests/DownloadTests.swift b/Tests/DilatedUITests/DownloadTests.swift index 3b4189d..cc87769 100644 --- a/Tests/DilatedUITests/DownloadTests.swift +++ b/Tests/DilatedUITests/DownloadTests.swift @@ -192,7 +192,7 @@ import Testing if failure { fixture.readFails = true } else { fixture.snapshot.files = [:] } fixture.event?() await fixture.drain() - #expect(fixture.ends.count == 1) + try #require(fixture.ends.count == 1) #expect((fixture.ends[0] != nil) == failure) #expect(fixture.cancellations == 1) } @@ -312,8 +312,9 @@ import Testing power.lowPowerMode = false model.refreshPower() _ = try model.startDownload(fixture.target, quietSeconds: 30, makeMonitor: fixture.monitor) + let pending = try #require(fixture.pendingRead) model.shutdown() - await fixture.drain() + await pending.value #expect(model.engine.holds.isEmpty && fixture.cancellations == 2) } } @@ -331,6 +332,7 @@ import Testing var cancellations = 0 var ends: [String?] = [] var event: (@MainActor @Sendable () -> Void)? + private weak var lastMonitor: DownloadMonitor? func monitor() -> DownloadMonitor { monitor(target, 30) { [weak self] in self?.ends.append($0) } } @@ -338,9 +340,10 @@ import Testing _ target: DownloadTarget, _ quietSeconds: TimeInterval, _ finished: @escaping @MainActor (String?) -> Void ) -> DownloadMonitor { - DownloadMonitor( + let monitor = DownloadMonitor( target: target, quietSeconds: quietSeconds, read: { [self] _ in + try await Task.sleep(for: .milliseconds(5)) reads += 1 if readFails { throw CocoaError(.fileReadNoPermission) } if let readError { throw readError } @@ -350,6 +353,9 @@ import Testing self.event = event return { [self] in cancellations += 1 } }, clock: { [self] in uptime }, finished: finished) + lastMonitor = monitor + return monitor } - func drain() async { for _ in 0..<20 { await Task.yield() } } + var pendingRead: Task? { lastMonitor?.pendingReadForTesting } + func drain() async { await pendingRead?.value } } diff --git a/Tests/DilatedUITests/DriveAliveTests.swift b/Tests/DilatedUITests/DriveAliveTests.swift index c833518..7e503e6 100644 --- a/Tests/DilatedUITests/DriveAliveTests.swift +++ b/Tests/DilatedUITests/DriveAliveTests.swift @@ -327,69 +327,76 @@ import Testing #expect(delays == [120]) } - @Test(.enabled(if: ProcessInfo.processInfo.environment["DILATED_DRIVE_FIXTURE"] != nil)) - func nativeSelectedVolumeAcceptsWritesOrRejectsReadOnlyMounts() async throws { - let path = try #require(ProcessInfo.processInfo.environment["DILATED_DRIVE_FIXTURE"]) - let folder = URL(fileURLWithPath: path) - let selected = try #require(try NativeDriveAlive.mounted().first { $0.volume.path == path }) - let before = try FileManager.default.contentsOfDirectory(atPath: path).sorted() - if selected.readOnly { - await #expect(throws: (any Error).self) { try await NativeDriveAlive.pulse(selected.volume) } - } else { - try await NativeDriveAlive.pulse(selected.volume) + @Suite(.serialized) + @MainActor struct NativeFixtureTests { + @Test(.enabled(if: ProcessInfo.processInfo.environment["DILATED_DRIVE_FIXTURE"] != nil)) + func nativeSelectedVolumeAcceptsWritesOrRejectsReadOnlyMounts() async throws { + let path = try #require(ProcessInfo.processInfo.environment["DILATED_DRIVE_FIXTURE"]) + let folder = URL(fileURLWithPath: path) + let selected = try #require(try NativeDriveAlive.mounted().first { $0.volume.path == path }) + let before = try FileManager.default.contentsOfDirectory(atPath: path).sorted() + if selected.readOnly { + await #expect(throws: (any Error).self) { + try await NativeDriveAlive.pulse(selected.volume) + } + } else { + try await NativeDriveAlive.pulse(selected.volume) + } + #expect(try FileManager.default.contentsOfDirectory(atPath: path).sorted() == before) + #expect(try Data(contentsOf: folder.appendingPathComponent("user-file")) == Data([1, 2, 3])) } - #expect(try FileManager.default.contentsOfDirectory(atPath: path).sorted() == before) - #expect(try Data(contentsOf: folder.appendingPathComponent("user-file")) == Data([1, 2, 3])) - } - @Test( - .enabled(if: ProcessInfo.processInfo.environment["DILATED_DRIVE_FIXTURE"] != nil), - .timeLimit(.minutes(1))) - func nativeMixedModesPulseOnlyTheEligibleVolumeAndStillPause() async throws { - let path = try #require(ProcessInfo.processInfo.environment["DILATED_DRIVE_FIXTURE"]) - let mount = try #require(try NativeDriveAlive.mounted().first { $0.volume.path == path }) - let before = try FileManager.default.contentsOfDirectory(atPath: path).sorted() - var selected = mount.volume - selected.mode = .always - var pulses: [String] = [] - var scheduled: (@MainActor @Sendable () -> Void)? - let didSchedule = Signal() - let controller = DriveAliveController( - clock: { 1000 }, - pulse: { volume in - pulses.append(volume.id) - try await NativeDriveAlive.pulse(volume) - }, - schedule: { _, fire in - scheduled = fire - didSchedule.fire() - return {} - }) - defer { controller.stop() } - var settings = DriveAliveSettings() - settings.enabled = true - settings.volumes = [ - .init(id: "native-awake-only", name: "Inactive", path: "/Volumes/Missing", mode: .awakeOnly), - selected, - ] - controller.configure(settings, awake: false, paused: false) - try await didSchedule.wait() - try #require(scheduled != nil) - #expect(pulses == [selected.id]) - #expect(controller.statuses["native-awake-only"] == nil) - if mount.readOnly { - #expect(controller.statuses[selected.id]?.contains("read-only") == true) - } else { - #expect(controller.statuses[selected.id] == "Active") + @Test( + .enabled(if: ProcessInfo.processInfo.environment["DILATED_DRIVE_FIXTURE"] != nil), + .timeLimit(.minutes(1))) + func nativeMixedModesPulseOnlyTheEligibleVolumeAndStillPause() async throws { + let path = try #require(ProcessInfo.processInfo.environment["DILATED_DRIVE_FIXTURE"]) + let mount = try #require(try NativeDriveAlive.mounted().first { $0.volume.path == path }) + let before = try FileManager.default.contentsOfDirectory(atPath: path).sorted() + var selected = mount.volume + selected.mode = .always + var pulses: [String] = [] + var scheduled: (@MainActor @Sendable () -> Void)? + let didSchedule = Signal() + let controller = DriveAliveController( + clock: { 1000 }, + pulse: { volume in + pulses.append(volume.id) + try await NativeDriveAlive.pulse(volume) + }, + schedule: { _, fire in + scheduled = fire + didSchedule.fire() + return {} + }) + defer { controller.stop() } + var settings = DriveAliveSettings() + settings.enabled = true + settings.volumes = [ + .init( + id: "native-awake-only", name: "Inactive", path: "/Volumes/Missing", mode: .awakeOnly), + selected, + ] + controller.configure(settings, awake: false, paused: false) + try await didSchedule.wait() + try #require(scheduled != nil) + #expect(pulses == [selected.id]) + #expect(controller.statuses["native-awake-only"] == nil) + if mount.readOnly { + #expect(controller.statuses[selected.id]?.contains("read-only") == true) + } else { + #expect(controller.statuses[selected.id] == "Active") + } + controller.configure(settings, awake: false, paused: true) + scheduled?() + for _ in 0..<20 { await Task.yield() } + #expect(pulses == [selected.id]) + #expect(try FileManager.default.contentsOfDirectory(atPath: path).sorted() == before) + #expect( + try Data(contentsOf: URL(fileURLWithPath: path).appendingPathComponent("user-file")) + == Data([1, 2, 3])) } - controller.configure(settings, awake: false, paused: true) - scheduled?() - for _ in 0..<20 { await Task.yield() } - #expect(pulses == [selected.id]) - #expect(try FileManager.default.contentsOfDirectory(atPath: path).sorted() == before) - #expect( - try Data(contentsOf: URL(fileURLWithPath: path).appendingPathComponent("user-file")) - == Data([1, 2, 3])) + } @Test func writerCreatesOnlyANewFileAndCleansUpWithoutTouchingExistingFiles() throws { diff --git a/Tests/DilatedUITests/FeedbackTests.swift b/Tests/DilatedUITests/FeedbackTests.swift index 01e5bfe..c558833 100644 --- a/Tests/DilatedUITests/FeedbackTests.swift +++ b/Tests/DilatedUITests/FeedbackTests.swift @@ -365,8 +365,14 @@ import Testing var permissionGate: CheckedContinuation? var delaySend = false var sendGate: CheckedContinuation? - func controller() -> FeedbackController { FeedbackController(service: self, sounds: self) } + private weak var lastController: FeedbackController? + func controller() -> FeedbackController { + let controller = FeedbackController(service: self, sounds: self) + lastController = controller + return controller + } func authorization() async -> FeedbackAuthorization { + try? await Task.sleep(for: .milliseconds(5)) if delayAuthorization { let result = await withCheckedContinuation { authorizationGate = $0 } authorizationReturned = true @@ -389,5 +395,7 @@ import Testing played.append(choice) } func stop() { stops += 1 } - func drain() async { for _ in 0..<30 { await Task.yield() } } + func drain() async { + for delivery in lastController?.pendingDeliveriesForTesting ?? [] { await delivery.value } + } } diff --git a/Tests/DilatedUITests/WiFiTests.swift b/Tests/DilatedUITests/WiFiTests.swift index 4e24b98..4efaf9a 100644 --- a/Tests/DilatedUITests/WiFiTests.swift +++ b/Tests/DilatedUITests/WiFiTests.swift @@ -67,12 +67,14 @@ import Testing #expect(!observer.isAuthorized && observer.read() == nil && reads == 2) } - @Test func queuedPermissionChangesAreIgnoredAfterStopOrDeinit() async { + @Test func queuedPermissionChangesAreIgnoredAfterStopOrDeinit() { let source = TestWiFiAuthorization() let access = WiFiAccess { source } var changes = 0 + var pending: [@MainActor @Sendable () -> Void] = [] var observer: WiFiObserver? = WiFiObserver( - access: access, readSSIDs: { [] }, changed: { changes += 1 }) + access: access, readSSIDs: { [] }, schedule: { pending.append($0) }, changed: { changes += 1 } + ) observer?.setEnabled(true) observer?.setEnabled(true) source.permission = .allowed @@ -80,19 +82,22 @@ import Testing source.permission = .denied source.changed?() observer?.stop() - await Task.yield() + for callback in pending { callback() } + pending.removeAll() #expect(changes == 0) observer?.setEnabled(true) source.permission = .allowed source.changed?() - await Task.yield() + for callback in pending { callback() } + pending.removeAll() #expect(changes == 1) weak var released = observer source.permission = .denied source.changed?() observer = nil #expect(released == nil) - await Task.yield() + for callback in pending { callback() } + pending.removeAll() #expect(changes == 1) } From be8624b1643acaf907b6742092b8a5ea2f8df604 Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Sat, 31 Oct 2026 15:13:02 -0400 Subject: [PATCH 02/10] test(settings): cover mounted row identity and process loading --- .../DilatedUITests/MountedSettingsTests.swift | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 Tests/DilatedUITests/MountedSettingsTests.swift diff --git a/Tests/DilatedUITests/MountedSettingsTests.swift b/Tests/DilatedUITests/MountedSettingsTests.swift new file mode 100644 index 0000000..d2aea87 --- /dev/null +++ b/Tests/DilatedUITests/MountedSettingsTests.swift @@ -0,0 +1,87 @@ +import AppKit +import DilatedCore +import Observation +import SwiftUI +import Testing + +@testable import DilatedUI + +@MainActor struct MountedSettingsTests { + @Test func insertingARowPreservesTheExistingRowsStateAndFocus() throws { + _ = NSApplication.shared + FontRegistry.registerBundledFonts() + let fixture = GroupFixtureState() + let view = NSHostingView(rootView: GroupFixture(fixture: fixture).frame(width: 400)) + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 400, height: 200), styleMask: [], + backing: .buffered, defer: false) + window.isReleasedWhenClosed = false + window.contentView = view + defer { window.close() } + try render(view) + let originalHeight = view.fittingSize.height + let field = try #require(descendants(of: view).compactMap { $0 as? NSTextField }.first) + let original = field.stringValue + try #require(!original.isEmpty) + try #require(window.makeFirstResponder(field)) + let editor = try #require(field.currentEditor()) + #expect(window.firstResponder === editor) + + fixture.inserted = true + try render(view) + try #require(view.fittingSize.height > originalHeight) + let survivor = try #require(descendants(of: view).compactMap { $0 as? NSTextField }.first) + #expect(survivor.stringValue == original) + #expect(survivor === field) + #expect(window.firstResponder === editor) + } + + @Test func specificProcessPickerLoadsWithoutPressingRefresh() throws { + _ = NSApplication.shared + FontRegistry.registerBundledFonts() + let pid = ProcessInfo.processInfo.processIdentifier + let process = try #require(ProcessCatalog.read().first { $0.id == pid }) + let rule = TriggerRule(name: "This process", criteria: [.processInstance(process.identity)]) + let locale = Locale(identifier: "en_US") + let view = NSHostingView( + rootView: TriggerRuleEditor(rule: rule, save: { _ in }, remove: {}).environment( + \.locale, locale)) + try render(view) + let pickers = descendants(of: view).compactMap { $0 as? NSPopUpButton } + let title = "\(process.name) (\(pid.formatted(.number.locale(locale))))" + let picker = try #require(pickers.first { $0.title == title }) + #expect(picker.itemTitles.contains(title)) + } + + private func descendants(of root: NSView) -> [NSView] { + [root] + root.subviews.flatMap { descendants(of: $0) } + } + + private func render(_ view: NSView) throws { + view.setFrameSize(view.fittingSize) + view.layoutSubtreeIfNeeded() + let bitmap = try #require(view.bitmapImageRepForCachingDisplay(in: view.bounds)) + view.cacheDisplay(in: view.bounds, to: bitmap) + } +} + +@MainActor @Observable private final class GroupFixtureState { + var inserted = false +} + +@MainActor private struct GroupFixture: View { + @Bindable var fixture: GroupFixtureState + + var body: some View { + SettingsGroup(eyebrow: "TEST") { + if fixture.inserted { Text("Inserted above") } + StatefulGroupRow() + } + } +} + +@MainActor private struct StatefulGroupRow: View { + @State private var value = UUID().uuidString + + var body: some View { TextField("Retained value", text: $value) } +} From ce2fa7eddbded3a2f4e53df3644604d6e4195c3f Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Sat, 31 Oct 2026 15:13:02 -0400 Subject: [PATCH 03/10] fix(site): describe the public beta before metadata loads --- README.md | 2 ++ website/dist/index.html | 13 +++++++------ website/tests/release.test.mjs | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2e1a1cb..8a6fea8 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Keeps your Mac awake. A native macOS 26 menu bar app for Apple Silicon. +From [CodesWhat](https://codeswhat.com): small, sharp tools from one developer and a lot of agents. + The local preview includes timed and indefinite awake holds, display-sleep controls, battery and Low Power Mode guards, four menu bar glyphs, optional compact/clock countdowns or end time, Settings, configurable global shortcuts, local history and statistics, URL automation, AppleScript, diff --git a/website/dist/index.html b/website/dist/index.html index 8271722..72b6884 100644 --- a/website/dist/index.html +++ b/website/dist/index.html @@ -3,12 +3,13 @@ - + + Dilated · Keep your Mac awake. - + @@ -17,7 +18,7 @@
dilated - +
@@ -25,7 +26,7 @@

NOT
DONE
YET.

The render. The download. The agent having a moment. Keep your Mac up until the job is done.

-

IN TESTING · MACOS 26+ · APPLE SILICON

+

PUBLIC BETA · MACOS 26+ · APPLE SILICON

@@ -80,10 +81,10 @@

Let it finish.

The screen can sleep.

Keeping your Mac awake doesn't have to keep the display on.

Stop is a full stop.

Clears every hold. Automation waits until you explicitly resume it.

Power gets a say.

Battery and Low Power Mode guards help put a limit on late nights.

-
Dilated's yellow and red pixel-eyes app icon, sitting in a Dock

STILL IN TESTING.

Nearly on
the shelf.

Dilated is free Mac software from CodesWhat.
The first release is in testing. There's no public download yet.

Closed-lid mode is still being checked on real hardware. Installation and update checks are also underway.

See how it works

IN TESTING · MACOS 26+ · APPLE SILICON · MIT LICENSE

+
Dilated's yellow and red pixel-eyes app icon, sitting in a Dock

PUBLIC BETA.

Try the
public beta.

Dilated is a free public beta for macOS 26+ on Apple Silicon. From CodesWhat, under MIT.

Closed-lid mode is experimental. Hardware recovery checks remain incomplete. If the download details don't load, see the release history for published builds and their verification status.

Release history

PUBLIC BETA · MACOS 26+ · APPLE SILICON · MIT LICENSE

- +