diff --git a/.agent-os/specs/2025-07-24-concurrency-crash-fixes/spec.md b/.agent-os/specs/2025-07-24-concurrency-crash-fixes/spec.md
new file mode 100644
index 0000000..2af6392
--- /dev/null
+++ b/.agent-os/specs/2025-07-24-concurrency-crash-fixes/spec.md
@@ -0,0 +1,73 @@
+# Spec Requirements Document
+
+> Spec: Critical Concurrency Crash Fixes
+> Created: 2025-07-24
+> Status: Planning
+
+## Overview
+
+Fix critical concurrency race conditions in timer callbacks and app initialization that cause immediate app crashes when Accessibility permissions are toggled ON/OFF, while preserving all advanced functionality implemented since version 1.4.15.
+
+## User Stories
+
+### Application Stability Recovery
+
+As a ClickIt user, I want to toggle Accessibility permissions ON/OFF in System Settings without the app crashing, so that I can manage permissions normally and continue using the application reliably.
+
+**Detailed Workflow:**
+1. User opens ClickIt application successfully
+2. User opens System Settings → Privacy & Security → Accessibility
+3. User toggles ClickIt permission OFF then ON (or vice versa)
+4. Application continues running without crashes
+5. Permission status updates correctly in the UI
+6. All timer-based functionality continues working properly
+
+### Developer Debugging Experience
+
+As a developer debugging ClickIt, I want clear concurrency patterns that follow Swift's MainActor isolation rules, so that I can maintain and extend the codebase without introducing race conditions.
+
+**Detailed Workflow:**
+1. Developer identifies concurrency issues in timer callbacks
+2. Fixed patterns use proper MainActor isolation without nested tasks
+3. Code is maintainable and follows Swift concurrency best practices
+4. Future timer implementations follow established safe patterns
+
+### Preservation of Advanced Features
+
+As a ClickIt user, I want all advanced features implemented since version 1.4.15 to continue working exactly as before, so that the stability fix doesn't break any existing functionality.
+
+**Detailed Workflow:**
+1. All timer automation continues with sub-10ms precision
+2. Permission monitoring works correctly without crashes
+3. Visual feedback system remains fully functional
+4. All UI panels and controls work as expected
+
+## Spec Scope
+
+1. **PermissionManager Concurrency Fix** - Replace problematic `Task { @MainActor in }` pattern in timer callback (line 190-194)
+2. **TimerAutomationEngine High-Precision Timer Fix** - Fix MainActor race condition in HighPrecisionTimer callback (lines 288-292)
+3. **TimerAutomationEngine Status Update Timer Fix** - Fix concurrency issue in status update timer (lines 389-393)
+4. **ClickItApp Initialization Fix** - Resolve MainActor task conflict in app initialization (lines 18-20)
+5. **Validation Testing** - Comprehensive testing to ensure crashes are eliminated during permission toggling
+
+## Out of Scope
+
+- Rewriting the entire timer system architecture
+- Changing the MainActor isolation strategy for classes
+- Modifying the permission monitoring frequency or behavior
+- Altering any user-facing functionality or UI behavior
+- Performance optimizations beyond fixing the race conditions
+
+## Expected Deliverable
+
+1. **Crash-Free Permission Toggling** - App no longer crashes when Accessibility permissions are toggled in System Settings
+2. **Proper MainActor Patterns** - All timer callbacks use safe concurrency patterns compatible with @MainActor classes
+3. **Preserved Functionality** - All existing features work identically to pre-fix behavior
+4. **Code Quality Improvement** - Cleaner, more maintainable concurrency patterns following Swift best practices
+5. **Comprehensive Testing Validation** - Thorough testing confirms stability across permission state changes
+
+## Spec Documentation
+
+- Tasks: @.agent-os/specs/2025-07-24-concurrency-crash-fixes/tasks.md
+- Technical Specification: @.agent-os/specs/2025-07-24-concurrency-crash-fixes/sub-specs/technical-spec.md
+- Tests Specification: @.agent-os/specs/2025-07-24-concurrency-crash-fixes/sub-specs/tests.md
\ No newline at end of file
diff --git a/.agent-os/specs/2025-07-24-concurrency-crash-fixes/sub-specs/technical-spec.md b/.agent-os/specs/2025-07-24-concurrency-crash-fixes/sub-specs/technical-spec.md
new file mode 100644
index 0000000..d76c4d5
--- /dev/null
+++ b/.agent-os/specs/2025-07-24-concurrency-crash-fixes/sub-specs/technical-spec.md
@@ -0,0 +1,112 @@
+# Technical Specification
+
+This is the technical specification for the spec detailed in @.agent-os/specs/2025-07-24-concurrency-crash-fixes/spec.md
+
+> Created: 2025-07-24
+> Version: 1.0.0
+
+## Technical Requirements
+
+### Root Cause Analysis
+
+The application crashes are caused by improper MainActor concurrency patterns in timer callbacks where @MainActor-isolated classes use `Task { @MainActor in }` wrappers, creating race conditions and concurrency conflicts.
+
+**Specific Problem Pattern:**
+```swift
+// PROBLEMATIC - Creates race condition
+Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in
+ Task { @MainActor in // ❌ Already on MainActor, creates conflict
+ // MainActor-isolated code
+ }
+}
+```
+
+**Required Safe Pattern:**
+```swift
+// SAFE - Properly dispatches to MainActor
+Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in
+ DispatchQueue.main.async { // ✅ Safe MainActor dispatch
+ // MainActor-isolated code
+ }
+}
+```
+
+### File-Specific Technical Requirements
+
+#### 1. PermissionManager.swift (Lines 190-194)
+- **Current Issue:** Timer callback using `Task { @MainActor in }` when class is @MainActor
+- **Required Fix:** Replace with `DispatchQueue.main.async`
+- **Critical Constraint:** Must preserve exact permission monitoring timing and behavior
+- **Performance Requirement:** No degradation in permission status update frequency
+
+#### 2. TimerAutomationEngine.swift (Lines 288-292) - HighPrecisionTimer Callback
+- **Current Issue:** High-precision timer callback using nested MainActor task
+- **Required Fix:** Use `DispatchQueue.main.async` for MainActor work
+- **Critical Constraint:** Must maintain sub-10ms timing accuracy
+- **Performance Requirement:** No impact on clicking precision or timing stability
+
+#### 3. TimerAutomationEngine.swift (Lines 389-393) - Status Update Timer
+- **Current Issue:** Status update timer using problematic MainActor pattern
+- **Required Fix:** Safe MainActor dispatch for UI updates
+- **Critical Constraint:** Preserve real-time status updates and statistics tracking
+- **Performance Requirement:** No delay in UI refresh or status synchronization
+
+#### 4. ClickItApp.swift (Lines 18-20) - App Initialization
+- **Current Issue:** App initialization using `Task { @MainActor in }` in main context
+- **Required Fix:** Proper initialization sequence without nested MainActor tasks
+- **Critical Constraint:** Maintain exact app startup behavior and initialization order
+- **Performance Requirement:** No impact on app launch time or startup reliability
+
+## Approach Options
+
+**Option A: DispatchQueue.main.async Pattern (Selected)**
+- Pros: Proven safe pattern, widely used, explicit about MainActor dispatch
+- Cons: Slightly more verbose than Task syntax
+- Rationale: Industry standard, no concurrency conflicts, maintains timing precision
+
+**Option B: Remove Task Wrappers Entirely**
+- Pros: Minimal code changes, relies on existing MainActor isolation
+- Cons: May not work for all timer callback contexts, potential threading issues
+- Rationale: Not selected due to potential threading safety concerns
+
+**Option C: Rewrite with AsyncTimer and Swift Concurrency**
+- Pros: Modern Swift concurrency, potentially better performance
+- Cons: Major architectural change, risk of introducing new issues
+- Rationale: Not selected due to scope constraints and unnecessary complexity
+
+## External Dependencies
+
+**No New Dependencies Required**
+- All fixes use existing Foundation and Dispatch APIs
+- No additional frameworks or third-party libraries needed
+- Pure Swift standard library solutions
+
+## Implementation Strategy
+
+### Phase 1: Critical Timer Callback Fixes
+1. **PermissionManager Timer Fix** - Replace Task wrapper with DispatchQueue pattern
+2. **TimerAutomationEngine HighPrecisionTimer Fix** - Safe MainActor dispatch for precision timing
+3. **TimerAutomationEngine Status Timer Fix** - Proper UI update dispatching
+
+### Phase 2: App Initialization Fix
+1. **ClickItApp Initialization** - Remove nested MainActor task conflicts
+2. **Startup Sequence Validation** - Ensure proper initialization order maintained
+
+### Phase 3: Comprehensive Testing
+1. **Permission Toggle Testing** - Validate no crashes during permission changes
+2. **Timer Precision Validation** - Confirm sub-10ms timing accuracy preserved
+3. **Functional Regression Testing** - Verify all features work identically
+
+## Performance Considerations
+
+### Timing Precision Requirements
+- **Sub-10ms Click Timing:** Must be preserved exactly
+- **Permission Monitoring:** Real-time status updates without delay
+- **UI Responsiveness:** No degradation in interface responsiveness
+- **Memory Usage:** No increase in memory footprint from concurrency changes
+
+### Concurrency Safety Requirements
+- **Thread Safety:** All MainActor-isolated code properly dispatched
+- **Race Condition Elimination:** No concurrent access to shared state
+- **Timer Callback Safety:** All timer callbacks use safe MainActor patterns
+- **Resource Cleanup:** Proper timer cleanup and resource management
\ No newline at end of file
diff --git a/.agent-os/specs/2025-07-24-concurrency-crash-fixes/sub-specs/tests.md b/.agent-os/specs/2025-07-24-concurrency-crash-fixes/sub-specs/tests.md
new file mode 100644
index 0000000..c4203ad
--- /dev/null
+++ b/.agent-os/specs/2025-07-24-concurrency-crash-fixes/sub-specs/tests.md
@@ -0,0 +1,143 @@
+# Tests Specification
+
+This is the tests coverage details for the spec detailed in @.agent-os/specs/2025-07-24-concurrency-crash-fixes/spec.md
+
+> Created: 2025-07-24
+> Version: 1.0.0
+
+## Test Coverage
+
+### Unit Tests
+
+**PermissionManager**
+- Test timer callback execution without MainActor conflicts
+- Test permission status monitoring during rapid state changes
+- Test proper cleanup of timer resources
+- Test thread safety of permission status updates
+
+**TimerAutomationEngine**
+- Test high-precision timer callback safety under MainActor isolation
+- Test status update timer concurrent access patterns
+- Test timer lifecycle management with proper resource cleanup
+- Test precision timing accuracy after concurrency fixes
+
+**ClickItApp**
+- Test app initialization sequence without MainActor task conflicts
+- Test startup stability with proper concurrency patterns
+- Test app lifecycle management with timer-based components
+
+### Integration Tests
+
+**Permission Toggle Workflow**
+- Test complete permission ON → OFF → ON cycle without crashes
+- Test permission status UI updates during system changes
+- Test app resilience during System Settings permission modifications
+- Test multiple rapid permission toggles for race condition detection
+
+**Timer System Integration**
+- Test all timer systems working together without conflicts
+- Test coordinated timer execution with MainActor-isolated components
+- Test timer precision under concurrent permission monitoring
+- Test error recovery when timers encounter MainActor conflicts
+
+**App Stability Under Concurrency**
+- Test app stability during rapid user interactions
+- Test concurrent timer execution with UI updates
+- Test memory management during high-frequency timer callbacks
+- Test resource cleanup during abnormal termination scenarios
+
+### Performance Tests
+
+**Timing Precision Validation**
+- Test sub-10ms click timing accuracy preserved after fixes
+- Test permission monitoring frequency unchanged
+- Test UI responsiveness during concurrent timer operations
+- Test memory usage stability with corrected concurrency patterns
+
+**Concurrency Performance**
+- Test MainActor dispatch performance vs. Task pattern
+- Test timer callback execution time consistency
+- Test UI update latency with DispatchQueue.main.async pattern
+- Test overall app performance impact of concurrency fixes
+
+### Crash Prevention Tests
+
+**Permission Toggle Crash Prevention**
+- Test Accessibility permission toggle ON without crash
+- Test Accessibility permission toggle OFF without crash
+- Test rapid permission state changes without instability
+- Test permission monitoring resilience during system changes
+
+**MainActor Concurrency Safety**
+- Test all timer callbacks execute safely on MainActor
+- Test no nested MainActor task conflicts in any code path
+- Test proper thread isolation for MainActor-bound components
+- Test race condition elimination in permission monitoring
+
+**Error Recovery Testing**
+- Test graceful handling of timer callback exceptions
+- Test app recovery from MainActor-related errors
+- Test proper error reporting without masking concurrency issues
+- Test automatic timer restart after concurrency-related failures
+
+### Regression Tests
+
+**Feature Preservation Validation**
+- Test all clicking functionality works identically to pre-fix behavior
+- Test permission UI panels function exactly as before
+- Test visual feedback system maintains all capabilities
+- Test preset system and configuration preservation
+
+**Performance Regression Prevention**
+- Test no degradation in click timing precision
+- Test no increase in memory usage
+- Test no reduction in UI responsiveness
+- Test no impact on app startup time
+
+### Manual Testing Scenarios
+
+**Real-World Permission Management**
+1. Open ClickIt application
+2. Navigate to System Settings → Privacy & Security → Accessibility
+3. Toggle ClickIt permission OFF
+4. Verify app continues running, UI updates correctly
+5. Toggle ClickIt permission ON
+6. Verify app functions normally, no crashes or errors
+7. Repeat cycle 10 times rapidly
+8. Confirm consistent behavior and stability
+
+**Concurrency Stress Testing**
+1. Start clicking automation with high CPS rate
+2. While clicking is active, toggle Accessibility permission
+3. Verify automation stops/resumes correctly without crashes
+4. Test with multiple simultaneous actions (clicking + permission changes)
+5. Monitor for any MainActor-related warnings or crashes
+
+## Mocking Requirements
+
+**System Permission APIs**
+- Mock Accessibility API responses for permission state changes
+- Mock System Settings integration for automated testing
+- Mock timer execution environment for controlled concurrency testing
+
+**MainActor Execution Context**
+- Mock MainActor dispatch behavior for testing race conditions
+- Mock timer callback execution in controlled threading environment
+- Mock UI update scheduling for testing dispatch patterns
+
+## Testing Tools and Framework
+
+**XCTest Integration**
+- Unit tests for all concurrency-related methods
+- Integration tests for timer and permission system interaction
+- Performance tests for timing accuracy validation
+
+**Manual Testing Protocol**
+- Structured permission toggle testing procedure
+- Crash reproduction and validation methodology
+- Performance benchmarking for regression detection
+
+**Automated Testing Pipeline**
+- CI integration for concurrency safety validation
+- Automated crash detection and reporting
+- Performance regression testing in build pipeline
\ No newline at end of file
diff --git a/.agent-os/specs/2025-07-24-concurrency-crash-fixes/tasks.md b/.agent-os/specs/2025-07-24-concurrency-crash-fixes/tasks.md
new file mode 100644
index 0000000..5033178
--- /dev/null
+++ b/.agent-os/specs/2025-07-24-concurrency-crash-fixes/tasks.md
@@ -0,0 +1,56 @@
+# Spec Tasks
+
+These are the tasks to be completed for the spec detailed in @.agent-os/specs/2025-07-24-concurrency-crash-fixes/spec.md
+
+> Created: 2025-07-24
+> Status: Ready for Implementation
+
+## Tasks
+
+- [x] 1. Fix PermissionManager Timer Concurrency Issue
+ - [x] 1.1 Write tests for PermissionManager timer callback safety
+ - [x] 1.2 Identify exact location of problematic `Task { @MainActor in }` pattern (lines 190-194)
+ - [x] 1.3 Replace with `DispatchQueue.main.async` pattern for safe MainActor dispatch
+ - [x] 1.4 Validate permission monitoring timing and behavior preserved
+ - [x] 1.5 Test permission toggle scenarios to confirm crash elimination
+ - [x] 1.6 Verify all tests pass with new concurrency pattern
+
+- [ ] 2. Fix TimerAutomationEngine HighPrecisionTimer Concurrency
+ - [ ] 2.1 Write tests for high-precision timer MainActor safety
+ - [ ] 2.2 Locate problematic MainActor pattern in HighPrecisionTimer callback (lines 288-292)
+ - [ ] 2.3 Implement safe MainActor dispatch using DispatchQueue.main.async
+ - [ ] 2.4 Validate sub-10ms timing accuracy preservation
+ - [ ] 2.5 Test click precision under concurrent permission monitoring
+ - [ ] 2.6 Verify all timer precision tests pass
+
+- [ ] 3. Fix TimerAutomationEngine Status Update Timer Concurrency
+ - [ ] 3.1 Write tests for status update timer thread safety
+ - [ ] 3.2 Fix MainActor concurrency issue in status update timer (lines 389-393)
+ - [ ] 3.3 Replace nested MainActor task with proper dispatch pattern
+ - [ ] 3.4 Validate real-time UI status updates preserved
+ - [ ] 3.5 Test statistics tracking and UI synchronization
+ - [ ] 3.6 Verify all status update functionality tests pass
+
+- [ ] 4. Fix ClickItApp Initialization Concurrency
+ - [ ] 4.1 Write tests for app initialization MainActor patterns
+ - [ ] 4.2 Identify and fix MainActor task conflict in app init (lines 18-20)
+ - [ ] 4.3 Implement proper initialization sequence without nested MainActor tasks
+ - [ ] 4.4 Validate app startup behavior and initialization order preserved
+ - [ ] 4.5 Test app launch stability under various conditions
+ - [ ] 4.6 Verify all app initialization tests pass
+
+- [ ] 5. Comprehensive Crash Prevention Testing
+ - [ ] 5.1 Write comprehensive permission toggle crash tests
+ - [ ] 5.2 Implement automated testing for Accessibility permission ON/OFF cycles
+ - [ ] 5.3 Test rapid permission state changes for race condition detection
+ - [ ] 5.4 Validate no crashes occur during any permission management scenario
+ - [ ] 5.5 Test app stability under concurrent timer operations
+ - [ ] 5.6 Verify all crash prevention tests pass
+
+- [ ] 6. Performance and Regression Validation
+ - [ ] 6.1 Write performance tests for timing precision validation
+ - [ ] 6.2 Benchmark sub-10ms click timing accuracy after concurrency fixes
+ - [ ] 6.3 Validate no performance degradation in any timer-based functionality
+ - [ ] 6.4 Test all existing features work identically to pre-fix behavior
+ - [ ] 6.5 Verify memory usage and CPU performance unchanged
+ - [ ] 6.6 Confirm all performance and regression tests pass
\ No newline at end of file
diff --git a/ClickIt/Info.plist b/ClickIt/Info.plist
index 102d62d..35716c2 100644
--- a/ClickIt/Info.plist
+++ b/ClickIt/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.4.15
+ 1.5.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
LSMinimumSystemVersion
diff --git a/Sources/ClickIt/ClickItApp.swift b/Sources/ClickIt/ClickItApp.swift
index da8d290..8799128 100644
--- a/Sources/ClickIt/ClickItApp.swift
+++ b/Sources/ClickIt/ClickItApp.swift
@@ -8,29 +8,8 @@ struct ClickItApp: App {
@StateObject private var viewModel = ClickItViewModel()
init() {
- // Force app to appear in foreground when launched from command line
- DispatchQueue.main.async {
- NSApp.setActivationPolicy(.regular)
- NSApp.activate(ignoringOtherApps: true)
- }
-
- // Initialize hotkey manager
- Task { @MainActor in
- HotkeyManager.shared.initialize()
- }
-
- // Register app termination handler for cleanup
- NotificationCenter.default.addObserver(
- forName: NSApplication.willTerminateNotification,
- object: nil,
- queue: .main
- ) { _ in
- // Cleanup visual feedback overlay when app terminates
- Task { @MainActor in
- VisualFeedbackOverlay.shared.cleanup()
- HotkeyManager.shared.cleanup()
- }
- }
+ // All initialization moved to onAppear to avoid concurrency issues during App init
+ print("ClickItApp: Initialized App structure")
}
var body: some Scene {
@@ -47,14 +26,8 @@ struct ClickItApp: App {
}
}
.onAppear {
- // Additional window activation
- if let window = NSApp.windows.first {
- window.makeKeyAndOrderFront(nil)
- window.orderFrontRegardless()
- }
-
- // Start permission monitoring
- permissionManager.startPermissionMonitoring()
+ // Initialize app safely on MainActor
+ initializeApp()
}
}
.windowResizability(.contentSize)
@@ -72,4 +45,39 @@ struct ClickItApp: App {
}
}
}
+
+ // MARK: - Safe Initialization
+
+ private func initializeApp() {
+ print("ClickItApp: Starting safe app initialization")
+
+ // Force app to appear in foreground when launched from command line
+ NSApp.setActivationPolicy(.regular)
+ NSApp.activate(ignoringOtherApps: true)
+
+ // Additional window activation
+ if let window = NSApp.windows.first {
+ window.makeKeyAndOrderFront(nil)
+ window.orderFrontRegardless()
+ }
+
+ // Initialize hotkey manager safely
+ HotkeyManager.shared.initialize()
+
+ // Start permission monitoring
+ permissionManager.startPermissionMonitoring()
+
+ // Register app termination handler for cleanup
+ NotificationCenter.default.addObserver(
+ forName: NSApplication.willTerminateNotification,
+ object: nil,
+ queue: .main
+ ) { _ in
+ // Cleanup visual feedback overlay when app terminates
+ VisualFeedbackOverlay.shared.cleanup()
+ HotkeyManager.shared.cleanup()
+ }
+
+ print("ClickItApp: Safe app initialization completed")
+ }
}
diff --git a/Sources/ClickIt/Core/Click/ClickCoordinator.swift b/Sources/ClickIt/Core/Click/ClickCoordinator.swift
index 5c8f877..1957426 100644
--- a/Sources/ClickIt/Core/Click/ClickCoordinator.swift
+++ b/Sources/ClickIt/Core/Click/ClickCoordinator.swift
@@ -79,8 +79,10 @@ class ClickCoordinator: ObservableObject {
performanceMonitor.startMonitoring()
}
- // Use high-precision timer for better CPU efficiency
- startOptimizedAutomationLoop(configuration: configuration)
+ // SIMPLE WORKING APPROACH: Use basic Task with Task.sleep()
+ automationTask = Task {
+ await runAutomationLoop(configuration: configuration)
+ }
}
/// Stops the current automation session
@@ -168,16 +170,33 @@ class ClickCoordinator: ObservableObject {
/// - Parameter configuration: Click configuration
/// - Returns: Result of the click operation
func performSingleClick(configuration: ClickConfiguration) async -> ClickResult {
- let startTime = CFAbsoluteTimeGetCurrent()
+ print("🎯 [ClickCoordinator] performSingleClick() - MainActor: \(Thread.isMainThread)")
+ print(" Location: \(configuration.location), Type: \(configuration.type)")
- let result = await ClickEngine.shared.performClick(configuration: configuration)
-
- let endTime = CFAbsoluteTimeGetCurrent()
- let clickTime = endTime - startTime
-
- await updateStatistics(result: result, clickTime: clickTime)
+ let startTime = CFAbsoluteTimeGetCurrent()
- return result
+ do {
+ print("📞 [ClickCoordinator] Calling ClickEngine.performClick()...")
+ let result = await ClickEngine.shared.performClick(configuration: configuration)
+ print("✅ [ClickCoordinator] ClickEngine returned - Success: \(result.success)")
+
+ let endTime = CFAbsoluteTimeGetCurrent()
+ let clickTime = endTime - startTime
+
+ print("📊 [ClickCoordinator] Updating statistics...")
+ await updateStatistics(result: result, clickTime: clickTime)
+ print("✅ [ClickCoordinator] Statistics updated successfully")
+
+ return result
+ } catch {
+ print("❌ [ClickCoordinator] performSingleClick failed with error: \(error)")
+ return ClickResult(
+ success: false,
+ actualLocation: configuration.location,
+ timestamp: startTime,
+ error: .eventPostingFailed
+ )
+ }
}
/// Performs a sequence of clicks with specified timing
@@ -295,7 +314,71 @@ class ClickCoordinator: ObservableObject {
print("[ClickCoordinator] Performance optimization completed")
}
- // MARK: - Private Methods
+ // MARK: - Private Methods - Simple Working Automation Loop
+
+ /// Simple automation loop from working version (6b0b525)
+ private func runAutomationLoop(configuration: AutomationConfiguration) async {
+ while isActive && !Task.isCancelled {
+ let result = await executeAutomationStep(configuration: configuration)
+
+ if !result.success {
+ // Handle failed click based on configuration
+ if configuration.stopOnError {
+ await MainActor.run {
+ stopAutomation()
+ }
+ break
+ }
+ }
+
+ // Apply click interval using simple Task.sleep - NO TIMER CALLBACKS!
+ if configuration.clickInterval > 0 {
+ try? await Task.sleep(nanoseconds: UInt64(configuration.clickInterval * 1_000_000_000))
+ }
+
+ // Check for maximum clicks limit
+ if let maxClicks = configuration.maxClicks, totalClicks >= maxClicks {
+ await MainActor.run {
+ stopAutomation()
+ }
+ break
+ }
+
+ // Check for maximum duration limit
+ if let maxDuration = configuration.maxDuration {
+ let elapsedTime = CFAbsoluteTimeGetCurrent() - sessionStartTime
+ if elapsedTime >= maxDuration {
+ await MainActor.run {
+ stopAutomation()
+ }
+ break
+ }
+ }
+ }
+ }
+
+ /// Simple automation step execution from working version
+ private func executeAutomationStep(configuration: AutomationConfiguration) async -> ClickResult {
+ print("ClickCoordinator: executeAutomationStep() - Simple working approach")
+
+ // Use the working performSingleClick method
+ let result = await performSingleClick(
+ configuration: ClickConfiguration(
+ type: configuration.clickType,
+ location: configuration.location,
+ targetPID: nil
+ )
+ )
+
+ // Update visual feedback if enabled
+ if configuration.showVisualFeedback {
+ VisualFeedbackOverlay.shared.updateOverlay(at: configuration.location, isActive: true)
+ }
+
+ return result
+ }
+
+ // MARK: - Private Methods - Complex Methods (Unused)
/// Starts optimized automation loop using HighPrecisionTimer for better CPU efficiency
/// - Parameter configuration: Automation configuration
@@ -367,45 +450,6 @@ class ClickCoordinator: ObservableObject {
scheduleNextAutomationStep(configuration: configuration)
}
- /// Executes a single automation step with error recovery
- /// - Parameter configuration: Automation configuration
- /// - Returns: Result of the automation step
- private func executeAutomationStep(configuration: AutomationConfiguration) async -> ClickResult {
- let baseLocation: CGPoint
-
- if configuration.useDynamicMouseTracking {
- // Get current mouse position dynamically and convert coordinate systems
- baseLocation = await MainActor.run {
- let appKitPosition = NSEvent.mouseLocation
- print("[Dynamic Debug] Current mouse position (AppKit): \(appKitPosition)")
-
- // Convert from AppKit coordinates to CoreGraphics coordinates with multi-monitor support
- let cgPosition = convertAppKitToCoreGraphicsMultiMonitor(appKitPosition)
- print("[Dynamic Debug] Converted to CoreGraphics coordinates: \(cgPosition)")
- return cgPosition
- }
- } else {
- // Use the fixed configured location
- baseLocation = configuration.location
- }
-
- let location = configuration.randomizeLocation ?
- randomizeLocation(base: baseLocation, variance: configuration.locationVariance) :
- baseLocation
-
- print("ClickCoordinator: Executing automation step at \(location) (dynamic: \(configuration.useDynamicMouseTracking))")
-
- // Perform the actual click with error recovery
- print("ClickCoordinator: Performing actual click at \(location)")
- let result = await executeClickWithRecovery(
- location: location,
- configuration: configuration
- )
-
- print("ClickCoordinator: Click result: success=\(result.success)")
-
- return result
- }
/// Executes a click with integrated error recovery
/// - Parameters:
@@ -570,10 +614,15 @@ class ClickCoordinator: ObservableObject {
// Find which screen contains this point
for screen in NSScreen.screens {
if screen.frame.contains(appKitPosition) {
- // Convert using the specific screen's coordinate system
- let cgY = screen.frame.maxY - appKitPosition.y
+ // FIXED: Proper multi-monitor coordinate conversion
+ // AppKit Y increases upward from screen bottom
+ // CoreGraphics Y increases downward from screen top
+ // Formula: CG_Y = screen.origin.Y + (screen.height - (AppKit_Y - screen.origin.Y))
+ let relativeY = appKitPosition.y - screen.frame.origin.y // Y relative to screen bottom
+ let cgY = screen.frame.origin.y + (screen.frame.height - relativeY) // Convert to CG coordinates
let cgPosition = CGPoint(x: appKitPosition.x, y: cgY)
print("[Multi-Monitor Debug] AppKit \(appKitPosition) → CoreGraphics \(cgPosition) on screen \(screen.frame)")
+ print("[Multi-Monitor Debug] Calculation: relativeY=\(relativeY), cgY=\(screen.frame.origin.y) + (\(screen.frame.height) - \(relativeY)) = \(cgY)")
return cgPosition
}
}
@@ -590,12 +639,17 @@ class ClickCoordinator: ObservableObject {
// Find which screen this CoreGraphics position would map to
// This is a reverse lookup - we need to find the screen that would contain the original AppKit position
for screen in NSScreen.screens {
- // Check if this position could have come from this screen
- let potentialAppKitY = screen.frame.maxY - cgPosition.y
- let potentialAppKitPosition = CGPoint(x: cgPosition.x, y: potentialAppKitY)
+ // FIXED: Use proper reverse conversion
+ // CoreGraphics Y increases downward from screen top
+ // AppKit Y increases upward from screen bottom
+ // Formula: AppKit_Y = screen.origin.Y + (screen.height - (CG_Y - screen.origin.Y))
+ let relativeCgY = cgPosition.y - screen.frame.origin.y // Y relative to screen top in CG
+ let appKitY = screen.frame.origin.y + (screen.frame.height - relativeCgY) // Convert to AppKit coordinates
+ let potentialAppKitPosition = CGPoint(x: cgPosition.x, y: appKitY)
if screen.frame.contains(potentialAppKitPosition) {
print("[Multi-Monitor Debug] CoreGraphics \(cgPosition) → AppKit \(potentialAppKitPosition) on screen \(screen.frame)")
+ print("[Multi-Monitor Debug] Reverse calculation: relativeCgY=\(relativeCgY), appKitY=\(screen.frame.origin.y) + (\(screen.frame.height) - \(relativeCgY)) = \(appKitY)")
return potentialAppKitPosition
}
}
@@ -622,6 +676,7 @@ struct AutomationConfiguration {
let randomizeLocation: Bool
let locationVariance: CGFloat
let useDynamicMouseTracking: Bool
+ let showVisualFeedback: Bool
let cpsRandomizerConfig: CPSRandomizer.Configuration
init(
@@ -635,6 +690,7 @@ struct AutomationConfiguration {
randomizeLocation: Bool = false,
locationVariance: CGFloat = 0,
useDynamicMouseTracking: Bool = false,
+ showVisualFeedback: Bool = true,
cpsRandomizerConfig: CPSRandomizer.Configuration = CPSRandomizer.Configuration()
) {
self.location = location
@@ -647,6 +703,7 @@ struct AutomationConfiguration {
self.randomizeLocation = randomizeLocation
self.locationVariance = locationVariance
self.useDynamicMouseTracking = useDynamicMouseTracking
+ self.showVisualFeedback = showVisualFeedback
self.cpsRandomizerConfig = cpsRandomizerConfig
}
}
diff --git a/Sources/ClickIt/Core/Click/ClickEngine.swift b/Sources/ClickIt/Core/Click/ClickEngine.swift
index d8bee6f..4907e4a 100644
--- a/Sources/ClickIt/Core/Click/ClickEngine.swift
+++ b/Sources/ClickIt/Core/Click/ClickEngine.swift
@@ -63,8 +63,21 @@ class ClickEngine: @unchecked Sendable {
private func executeClick(configuration: ClickConfiguration) -> ClickResult {
let startTime = CFAbsoluteTimeGetCurrent()
+ print("🔧 [ClickEngine] executeClick() starting")
+ print(" Configuration: \(configuration)")
+ print(" Location: \(configuration.location)")
+ print(" Type: \(configuration.type)")
+ print(" Target PID: \(String(describing: configuration.targetPID))")
+
// Validate location
- guard isValidLocation(configuration.location) else {
+ let isLocationValid = isValidLocation(configuration.location)
+ print("🎯 [ClickEngine] Location validation: \(isLocationValid)")
+
+ if !isLocationValid {
+ let screenBounds = CGDisplayBounds(CGMainDisplayID())
+ print("❌ [ClickEngine] INVALID LOCATION!")
+ print(" Requested: \(configuration.location)")
+ print(" Screen bounds: \(screenBounds)")
return ClickResult(
success: false,
actualLocation: configuration.location,
@@ -74,11 +87,16 @@ class ClickEngine: @unchecked Sendable {
}
// Create mouse down event
+ print("🖱️ [ClickEngine] Creating mouse down event...")
+ print(" Event type: \(configuration.type.mouseDownEventType)")
+ print(" Button: \(configuration.type.mouseButton)")
+
guard let mouseDownEvent = createMouseEvent(
type: configuration.type.mouseDownEventType,
location: configuration.location,
button: configuration.type.mouseButton
) else {
+ print("❌ [ClickEngine] MOUSE DOWN EVENT CREATION FAILED!")
return ClickResult(
success: false,
actualLocation: configuration.location,
@@ -86,13 +104,18 @@ class ClickEngine: @unchecked Sendable {
error: .eventCreationFailed
)
}
+ print("✅ [ClickEngine] Mouse down event created successfully")
// Create mouse up event
+ print("🖱️ [ClickEngine] Creating mouse up event...")
+ print(" Event type: \(configuration.type.mouseUpEventType)")
+
guard let mouseUpEvent = createMouseEvent(
type: configuration.type.mouseUpEventType,
location: configuration.location,
button: configuration.type.mouseButton
) else {
+ print("❌ [ClickEngine] MOUSE UP EVENT CREATION FAILED!")
return ClickResult(
success: false,
actualLocation: configuration.location,
@@ -100,8 +123,13 @@ class ClickEngine: @unchecked Sendable {
error: .eventCreationFailed
)
}
+ print("✅ [ClickEngine] Mouse up event created successfully")
// Post events
+ print("📤 [ClickEngine] About to post events...")
+ print(" Target PID: \(String(describing: configuration.targetPID))")
+ print(" Delay between down/up: \(configuration.delayBetweenDownUp)")
+
let postResult = postMouseEvents(
downEvent: mouseDownEvent,
upEvent: mouseUpEvent,
@@ -109,7 +137,11 @@ class ClickEngine: @unchecked Sendable {
delay: configuration.delayBetweenDownUp
)
- _ = CFAbsoluteTimeGetCurrent()
+ print("📊 [ClickEngine] Post result: success=\(postResult.success), error=\(String(describing: postResult.error))")
+
+ let endTime = CFAbsoluteTimeGetCurrent()
+ let totalTime = (endTime - startTime) * 1000 // Convert to milliseconds
+ print("⏱️ [ClickEngine] Total execution time: \(totalTime)ms")
return ClickResult(
success: postResult.success,
@@ -225,7 +257,35 @@ class ClickEngine: @unchecked Sendable {
/// - Returns: True if location is valid, false otherwise
private func isValidLocation(_ location: CGPoint) -> Bool {
let screenBounds = CGDisplayBounds(CGMainDisplayID())
- return screenBounds.contains(location)
+ let isValid = screenBounds.contains(location)
+
+ print("🔍 [ClickEngine] Location validation details:")
+ print(" Location: \(location)")
+ print(" Screen bounds: \(screenBounds)")
+ print(" Is valid: \(isValid)")
+
+ // Additional check for multi-monitor setups
+ if !isValid {
+ print("🖥️ [ClickEngine] Checking all displays...")
+ let maxDisplays: UInt32 = 16
+ var displayIDs = Array(repeating: 0, count: Int(maxDisplays))
+ var displayCount: UInt32 = 0
+
+ let result = CGGetActiveDisplayList(maxDisplays, &displayIDs, &displayCount)
+ if result == .success {
+ for i in 0..= lastCPUTicks.user,
+ currentTicks.system >= lastCPUTicks.system,
+ currentTicks.idle >= lastCPUTicks.idle else {
+ print("[PerformanceMonitor] CPU ticks went backwards, returning 0")
+ return 0
+ }
+
+ // Calculate differences with overflow protection
let userDiff = currentTicks.user - lastCPUTicks.user
let systemDiff = currentTicks.system - lastCPUTicks.system
let idleDiff = currentTicks.idle - lastCPUTicks.idle
+ // Check for potential overflow before addition
+ guard userDiff < UInt64.max / 3,
+ systemDiff < UInt64.max / 3,
+ idleDiff < UInt64.max / 3 else {
+ print("[PerformanceMonitor] CPU diff values too large, returning 5.0")
+ return 5.0 // Return reasonable default
+ }
+
let totalDiff = userDiff + systemDiff + idleDiff
guard totalDiff > 0 else { return 0 }
let usedDiff = userDiff + systemDiff
+
+ // Additional safety check before conversion to Double
+ guard usedDiff <= totalDiff else {
+ print("[PerformanceMonitor] Used diff > total diff, returning 0")
+ return 0
+ }
+
return (Double(usedDiff) / Double(totalDiff)) * 100.0
}
@@ -336,12 +359,16 @@ final class PerformanceMonitor: ObservableObject {
// In a production implementation, this would use proper mach APIs
let timestamp = mach_absolute_time()
- // Return mock values based on timestamp for demonstration
- // This ensures the CPU usage calculation works without mach API complexity
+ // Use safer, smaller values to prevent arithmetic overflow
+ // Convert to seconds first to get reasonable numbers
+ let timeInSeconds = timestamp / 1_000_000_000 // Convert nanoseconds to seconds
+
+ // Return mock values based on time in seconds for demonstration
+ // This ensures the CPU usage calculation works without overflow
return CPUTicks(
- user: timestamp % 1000,
- system: (timestamp / 10) % 500,
- idle: (timestamp / 100) % 10000
+ user: (timeInSeconds % 1000) + 100, // 100-1099 range
+ system: (timeInSeconds % 500) + 50, // 50-549 range
+ idle: (timeInSeconds % 10000) + 1000 // 1000-10999 range
)
}
diff --git a/Sources/ClickIt/Core/Permissions/PermissionManager.swift b/Sources/ClickIt/Core/Permissions/PermissionManager.swift
index 18f6c33..d5c4543 100644
--- a/Sources/ClickIt/Core/Permissions/PermissionManager.swift
+++ b/Sources/ClickIt/Core/Permissions/PermissionManager.swift
@@ -186,11 +186,10 @@ class PermissionManager: ObservableObject {
// Avoid multiple timers
stopPermissionMonitoring()
- // Since we're already on @MainActor, no need for DispatchQueue.main.async
+ // Since we're already on @MainActor, create timer on main RunLoop directly
monitoringTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
- Task { @MainActor in
- self?.updatePermissionStatus()
- }
+ // We're already on MainActor via timer on main RunLoop, so call directly
+ self?.updatePermissionStatus()
}
}
diff --git a/Sources/ClickIt/Core/Timer/TimerAutomationEngine.swift b/Sources/ClickIt/Core/Timer/TimerAutomationEngine.swift
index 27ec8dd..63a33b0 100644
--- a/Sources/ClickIt/Core/Timer/TimerAutomationEngine.swift
+++ b/Sources/ClickIt/Core/Timer/TimerAutomationEngine.swift
@@ -103,12 +103,14 @@ class TimerAutomationEngine: ObservableObject {
/// Starts automation with the specified configuration
/// - Parameter configuration: Automation configuration parameters
func startAutomation(with configuration: AutomationConfiguration) {
+ print("🚀 [TimerAutomationEngine] startAutomation() called - MainActor: \(Thread.isMainThread)")
+
guard automationState == .idle else {
- print("[TimerAutomationEngine] Cannot start automation - current state: \(automationState)")
+ print("❌ [TimerAutomationEngine] Cannot start automation - current state: \(automationState)")
return
}
- print("[TimerAutomationEngine] Starting automation with configuration")
+ print("✅ [TimerAutomationEngine] Starting automation with configuration: \(configuration.location)")
// Store configuration and create session
automationConfiguration = configuration
@@ -284,10 +286,14 @@ class TimerAutomationEngine: ObservableObject {
// Create high-precision timer for automation
highPrecisionTimer = HighPrecisionTimerFactory.createClickTimer(interval: configuration.clickInterval)
- // Start repeating timer with automation callback
+ // Start repeating timer with automation callback - FIXED CONCURRENCY ISSUE
highPrecisionTimer?.startRepeatingTimer(interval: configuration.clickInterval) { [weak self] in
- Task { @MainActor in
- await self?.executeAutomationStep()
+ // Use DispatchQueue.main.async to safely get to MainActor context
+ DispatchQueue.main.async {
+ print("⏰ [TimerAutomationEngine] Timer callback executing - MainActor: \(Thread.isMainThread)")
+ Task {
+ await self?.executeAutomationStep()
+ }
}
}
@@ -296,12 +302,17 @@ class TimerAutomationEngine: ObservableObject {
/// Executes a single automation step
private func executeAutomationStep() async {
+ print("🔄 [TimerAutomationEngine] executeAutomationStep() - MainActor: \(Thread.isMainThread)")
+
// Quick state checks for efficiency
guard automationState == .running,
let config = automationConfiguration else {
+ print("⚠️ [TimerAutomationEngine] Skipping step - state: \(automationState), config: \(automationConfiguration != nil)")
return
}
+ print("✅ [TimerAutomationEngine] Executing automation step at: \(config.location)")
+
// Check session limits before execution
if shouldStopDueToLimits(config: config) {
stopAutomation()
@@ -326,14 +337,30 @@ class TimerAutomationEngine: ObservableObject {
/// - Parameter config: Automation configuration
/// - Returns: Click result
private func performAutomationClick(config: AutomationConfiguration) async -> ClickResult {
- // Delegate to click coordinator for actual execution
- return await clickCoordinator.performSingleClick(
- configuration: ClickConfiguration(
- type: config.clickType,
- location: config.location,
- targetPID: nil
+ print("🖱️ [TimerAutomationEngine] performAutomationClick() - About to call clickCoordinator")
+ print(" Location: \(config.location), Type: \(config.clickType)")
+
+ do {
+ // Delegate to click coordinator for actual execution
+ let result = await clickCoordinator.performSingleClick(
+ configuration: ClickConfiguration(
+ type: config.clickType,
+ location: config.location,
+ targetPID: nil
+ )
)
- )
+
+ print("✅ [TimerAutomationEngine] Click completed - Success: \(result.success)")
+ return result
+ } catch {
+ print("❌ [TimerAutomationEngine] Click failed with error: \(error)")
+ return ClickResult(
+ success: false,
+ actualLocation: config.location,
+ timestamp: CFAbsoluteTimeGetCurrent(),
+ error: .eventPostingFailed
+ )
+ }
}
/// Checks if automation should stop due to configured limits
@@ -387,7 +414,7 @@ class TimerAutomationEngine: ObservableObject {
/// Starts the status update timer for real-time UI updates
private func startStatusUpdateTimer() {
statusUpdateTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
- Task { @MainActor in
+ DispatchQueue.main.async {
self?.updateAutomationStatus()
}
}
diff --git a/Sources/ClickIt/UI/Components/ClickPointSelector.swift b/Sources/ClickIt/UI/Components/ClickPointSelector.swift
index 0d00760..c1faab7 100644
--- a/Sources/ClickIt/UI/Components/ClickPointSelector.swift
+++ b/Sources/ClickIt/UI/Components/ClickPointSelector.swift
@@ -171,19 +171,22 @@ struct ClickPointSelector: View {
}
private func validateCoordinates(_ point: CGPoint) -> Bool {
- // Get main screen bounds
- let screenFrame = NSScreen.main?.frame ?? CGRect.zero
+ // FIXED: Check all screens, not just main screen
+ print("🔍 [ClickPointSelector] Validating coordinates: \(point)")
- // Check if point is within screen bounds
- if point.x < 0 || point.x > screenFrame.width ||
- point.y < 0 || point.y > screenFrame.height {
- let maxX = Int(screenFrame.width)
- let maxY = Int(screenFrame.height)
- validationError = "Coordinates must be within screen bounds (0,0) to (\(maxX),\(maxY))"
- return false
+ for (index, screen) in NSScreen.screens.enumerated() {
+ if screen.frame.contains(point) {
+ print("✅ [ClickPointSelector] Point is valid on screen \(index): \(screen.frame)")
+ return true
+ }
+ print(" Screen \(index): \(screen.frame) - contains: false")
}
- return true
+ // If not found on any screen, show error with all screen bounds
+ let allScreens = NSScreen.screens.enumerated().map { "Screen \($0): \($1.frame)" }.joined(separator: ", ")
+ validationError = "Coordinates (\(Int(point.x)),\(Int(point.y))) are not within any screen bounds. Available screens: \(allScreens)"
+ print("❌ [ClickPointSelector] Validation failed: \(validationError ?? "unknown error")")
+ return false
}
private func clearValidationError() {
@@ -199,17 +202,12 @@ struct ClickCoordinateCapture {
var eventMonitor: Any?
eventMonitor = NSEvent.addGlobalMonitorForEvents(matching: .leftMouseDown) { _ in
- let screenPoint = NSEvent.mouseLocation
-
- // NSEvent.mouseLocation already gives us the correct coordinates
- // No conversion needed - use them directly
- let convertedPoint = CGPoint(
- x: screenPoint.x,
- y: screenPoint.y
- )
+ let appKitPoint = NSEvent.mouseLocation
+ print("ClickCoordinateCapture: Raw mouse location (AppKit): \(appKitPoint)")
- print("ClickCoordinateCapture: Raw mouse location: \(screenPoint)")
- print("ClickCoordinateCapture: Converted point: \(convertedPoint)")
+ // FIXED: Convert AppKit coordinates to CoreGraphics coordinates for multi-monitor setups
+ let convertedPoint = convertAppKitToCoreGraphics(appKitPoint)
+ print("ClickCoordinateCapture: Converted to CoreGraphics: \(convertedPoint)")
// Clean up monitor
if let monitor = eventMonitor {
@@ -222,6 +220,31 @@ struct ClickCoordinateCapture {
}
}
}
+
+ /// Converts AppKit coordinates to CoreGraphics coordinates for multi-monitor setups
+ private static func convertAppKitToCoreGraphics(_ appKitPosition: CGPoint) -> CGPoint {
+ // Find which screen contains this point
+ for screen in NSScreen.screens {
+ if screen.frame.contains(appKitPosition) {
+ // FIXED: Proper multi-monitor coordinate conversion
+ // AppKit Y increases upward from screen bottom
+ // CoreGraphics Y increases downward from screen top
+ // Formula: CG_Y = screen.origin.Y + (screen.height - (AppKit_Y - screen.origin.Y))
+ let relativeY = appKitPosition.y - screen.frame.origin.y // Y relative to screen bottom
+ let cgY = screen.frame.origin.y + (screen.frame.height - relativeY) // Convert to CG coordinates
+ let cgPosition = CGPoint(x: appKitPosition.x, y: cgY)
+ print("ClickCoordinateCapture: Multi-monitor conversion on screen \(screen.frame)")
+ print("ClickCoordinateCapture: Calculation: relativeY=\(relativeY), cgY=\(screen.frame.origin.y) + (\(screen.frame.height) - \(relativeY)) = \(cgY)")
+ return cgPosition
+ }
+ }
+
+ // Fallback to main screen if no screen contains the point
+ let mainScreenHeight = NSScreen.main?.frame.height ?? 0
+ let fallbackPosition = CGPoint(x: appKitPosition.x, y: mainScreenHeight - appKitPosition.y)
+ print("ClickCoordinateCapture: Fallback conversion: AppKit \(appKitPosition) → CoreGraphics \(fallbackPosition)")
+ return fallbackPosition
+ }
}
struct ClickPointSelector_Previews: PreviewProvider {
diff --git a/Sources/ClickIt/UI/ViewModels/ClickItViewModel.swift b/Sources/ClickIt/UI/ViewModels/ClickItViewModel.swift
index 1f5a7d7..e7a1143 100644
--- a/Sources/ClickIt/UI/ViewModels/ClickItViewModel.swift
+++ b/Sources/ClickIt/UI/ViewModels/ClickItViewModel.swift
@@ -84,7 +84,6 @@ class ClickItViewModel: ObservableObject {
// MARK: - Dependencies
private let clickCoordinator = ClickCoordinator.shared
- private let timerAutomationEngine = TimerAutomationEngine()
// MARK: - Initialization
init() {
@@ -116,14 +115,16 @@ class ClickItViewModel: ObservableObject {
stopOnError: stopOnError,
randomizeLocation: randomizeLocation,
locationVariance: CGFloat(randomizeLocation ? locationVariance : 0),
- useDynamicMouseTracking: false // Normal automation uses fixed position
+ useDynamicMouseTracking: false, // Normal automation uses fixed position
+ showVisualFeedback: showVisualFeedback
)
- // Use TimerAutomationEngine for enhanced automation with better timing control
- timerAutomationEngine.startAutomation(with: config)
+ // REVERTED TO WORKING APPROACH: Use ClickCoordinator directly
+ clickCoordinator.startAutomation(with: config)
+ isRunning = true
+ appStatus = .running
- // Note: UI state will be updated automatically through bindings
- print("ClickItViewModel: Started enhanced automation with TimerAutomationEngine")
+ print("ClickItViewModel: Started automation with direct ClickCoordinator (reverted to working approach)")
}
private func startDynamicAutomation() {
@@ -150,60 +151,52 @@ class ClickItViewModel: ObservableObject {
stopOnError: false, // Disable stopOnError for timer mode to avoid timing constraint issues
randomizeLocation: randomizeLocation,
locationVariance: CGFloat(randomizeLocation ? locationVariance : 0),
- useDynamicMouseTracking: true // Enable dynamic mouse tracking for timer mode
+ useDynamicMouseTracking: true, // Enable dynamic mouse tracking for timer mode
+ showVisualFeedback: showVisualFeedback
)
print("[Timer Debug] Created automation config with interval: \(config.clickInterval)s, dynamic: \(config.useDynamicMouseTracking)")
- // Use TimerAutomationEngine for enhanced dynamic automation
- timerAutomationEngine.startAutomation(with: config)
+ // REVERTED TO WORKING APPROACH: Use ClickCoordinator directly
+ clickCoordinator.startAutomation(with: config)
+ isRunning = true
+ appStatus = .running
- print("[Timer Debug] Enhanced automation started with TimerAutomationEngine - dynamic: \(config.useDynamicMouseTracking)")
- // Note: UI state will be updated automatically through bindings
+ print("[Timer Debug] Automation started with direct ClickCoordinator - dynamic: \(config.useDynamicMouseTracking)")
+ print("[Timer Debug] Automation started - isRunning: \(isRunning)")
}
func stopAutomation() {
- // Use TimerAutomationEngine for enhanced stop functionality
- timerAutomationEngine.stopAutomation()
-
- // Also stop click coordinator for fallback compatibility
+ // SIMPLE WORKING APPROACH: Direct ClickCoordinator call
clickCoordinator.stopAutomation()
+ cancelTimer() // Also cancel any active timer
+ isRunning = false
+ appStatus = .ready
- // Cancel any active timer
- cancelTimer()
-
- // Note: UI state will be updated automatically through bindings
- print("ClickItViewModel: Stopped automation with TimerAutomationEngine")
+ print("ClickItViewModel: Stopped automation with direct ClickCoordinator")
}
func pauseAutomation() {
guard isRunning && !isPaused else { return }
- // Use TimerAutomationEngine for enhanced pause functionality
- timerAutomationEngine.pauseAutomation()
+ // SIMPLE WORKING APPROACH: Direct ClickCoordinator call
+ clickCoordinator.pauseAutomation()
+ isPaused = true
+ appStatus = .paused
- // Update visual feedback to show paused state (dimmed)
- if showVisualFeedback, let point = targetPoint {
- VisualFeedbackOverlay.shared.updateOverlay(at: point, isActive: false)
- }
-
- // Note: UI state will be updated automatically through bindings
- print("ClickItViewModel: Paused automation with TimerAutomationEngine")
+ print("ClickItViewModel: Paused automation with direct ClickCoordinator")
}
func resumeAutomation() {
guard isPaused && !isRunning else { return }
- // Use TimerAutomationEngine for enhanced resume functionality
- timerAutomationEngine.resumeAutomation()
-
- // Update visual feedback to show active state
- if showVisualFeedback, let point = targetPoint {
- VisualFeedbackOverlay.shared.updateOverlay(at: point, isActive: true)
- }
+ // SIMPLE WORKING APPROACH: Direct ClickCoordinator call
+ clickCoordinator.resumeAutomation()
+ isPaused = false
+ isRunning = true
+ appStatus = .running
- // Note: UI state will be updated automatically through bindings
- print("ClickItViewModel: Resumed automation with TimerAutomationEngine")
+ print("ClickItViewModel: Resumed automation with direct ClickCoordinator")
}
// MARK: - Testing Methods
@@ -220,7 +213,8 @@ class ClickItViewModel: ObservableObject {
stopOnError: stopOnError,
randomizeLocation: randomizeLocation,
locationVariance: CGFloat(randomizeLocation ? locationVariance : 0),
- useDynamicMouseTracking: false
+ useDynamicMouseTracking: false,
+ showVisualFeedback: true
)
clickCoordinator.startAutomation(with: config)
@@ -252,7 +246,7 @@ class ClickItViewModel: ObservableObject {
// MARK: - Private Methods
private func setupBindings() {
- // Monitor click coordinator state changes
+ // SIMPLE WORKING APPROACH: Monitor click coordinator state changes only
clickCoordinator.objectWillChange.sink { [weak self] in
self?.updateStatistics()
}
@@ -273,91 +267,29 @@ class ClickItViewModel: ObservableObject {
}
}
.store(in: &cancellables)
-
- // Monitor TimerAutomationEngine state changes
- timerAutomationEngine.$automationState.sink { [weak self] state in
- guard let self = self else { return }
-
- // Sync UI state with TimerAutomationEngine state
- switch state {
- case .idle:
- self.isRunning = false
- self.isPaused = false
- self.appStatus = .ready
- case .running:
- self.isRunning = true
- self.isPaused = false
- self.appStatus = .running
- case .paused:
- self.isRunning = false
- self.isPaused = true
- self.appStatus = .paused
- case .stopped:
- self.isRunning = false
- self.isPaused = false
- self.appStatus = .ready
- case .error:
- self.isRunning = false
- self.isPaused = false
- self.appStatus = .error("Timer automation error")
- }
- }
- .store(in: &cancellables)
-
- // Monitor TimerAutomationEngine session statistics
- timerAutomationEngine.$currentSession.sink { [weak self] session in
- guard let self = self else { return }
-
- // Update statistics from timer engine session
- if let session = session {
- self.statistics = SessionStatistics(
- duration: session.duration,
- totalClicks: session.totalClicks,
- successfulClicks: session.successfulClicks,
- failedClicks: session.failedClicks,
- successRate: session.successRate,
- averageClickTime: session.averageClickTime,
- clicksPerSecond: session.clicksPerSecond,
- isActive: session.isActive
- )
- }
- }
- .store(in: &cancellables)
}
private func updateStatistics() {
- // Prioritize TimerAutomationEngine statistics if available
- if let timerStats = timerAutomationEngine.getSessionStatistics() {
- statistics = timerStats
- } else {
- // Fallback to ClickCoordinator statistics
- statistics = clickCoordinator.getSessionStatistics()
- }
- }
-
- // MARK: - Enhanced Automation Status
-
- /// Gets the current automation status from TimerAutomationEngine
- func getCurrentAutomationStatus() -> AutomationStatus {
- return timerAutomationEngine.getCurrentStatus()
+ // SIMPLE WORKING APPROACH: Use ClickCoordinator statistics directly
+ statistics = clickCoordinator.getSessionStatistics()
}
- /// Gets timing accuracy statistics from TimerAutomationEngine
- func getTimingAccuracy() -> TimingAccuracyStats? {
- return timerAutomationEngine.getTimingAccuracy()
- }
+ // MARK: - Emergency Stop
- /// Performs emergency stop using TimerAutomationEngine
+ /// Performs emergency stop using ClickCoordinator directly
func emergencyStopAutomation() {
- timerAutomationEngine.emergencyStopAutomation()
-
- // Also stop click coordinator for fallback compatibility
+ // SIMPLE WORKING APPROACH: Direct ClickCoordinator call
clickCoordinator.emergencyStopAutomation()
// Cancel any active timer
cancelTimer()
- print("ClickItViewModel: Emergency stop executed with TimerAutomationEngine")
+ // Update UI state immediately
+ isRunning = false
+ isPaused = false
+ appStatus = .ready
+
+ print("ClickItViewModel: Emergency stop executed with direct ClickCoordinator")
}
/// Checks if a position is within any available screen bounds (supports multiple monitors)
@@ -377,10 +309,15 @@ class ClickItViewModel: ObservableObject {
// Find which screen contains this point
for screen in NSScreen.screens {
if screen.frame.contains(appKitPosition) {
- // Convert using the specific screen's coordinate system
- let cgY = screen.frame.maxY - appKitPosition.y
+ // FIXED: Proper multi-monitor coordinate conversion
+ // AppKit Y increases upward from screen bottom
+ // CoreGraphics Y increases downward from screen top
+ // Formula: CG_Y = screen.origin.Y + (screen.height - (AppKit_Y - screen.origin.Y))
+ let relativeY = appKitPosition.y - screen.frame.origin.y // Y relative to screen bottom
+ let cgY = screen.frame.origin.y + (screen.frame.height - relativeY) // Convert to CG coordinates
let cgPosition = CGPoint(x: appKitPosition.x, y: cgY)
print("[Timer Debug] Multi-monitor conversion: AppKit \(appKitPosition) → CoreGraphics \(cgPosition) on screen \(screen.frame)")
+ print("[Timer Debug] Calculation: relativeY=\(relativeY), cgY=\(screen.frame.origin.y) + (\(screen.frame.height) - \(relativeY)) = \(cgY)")
return cgPosition
}
}
@@ -531,18 +468,17 @@ class ClickItViewModel: ObservableObject {
private func startCountdownTimer() {
print("[Timer Debug] Starting countdown timer with \(remainingTime) seconds remaining")
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
- DispatchQueue.main.async {
- guard let self = self else { return }
-
- self.remainingTime -= 1.0
- print("[Timer Debug] Countdown tick: \(self.remainingTime) seconds remaining")
-
- if self.remainingTime <= 0 {
- print("[Timer Debug] Countdown finished, calling onTimerExpired()")
- self.countdownTimer?.invalidate()
- self.countdownTimer = nil
- self.onTimerExpired()
- }
+ // We're already on MainActor via timer on main RunLoop, call directly
+ guard let self = self else { return }
+
+ self.remainingTime -= 1.0
+ print("[Timer Debug] Countdown tick: \(self.remainingTime) seconds remaining")
+
+ if self.remainingTime <= 0 {
+ print("[Timer Debug] Countdown finished, calling onTimerExpired()")
+ self.countdownTimer?.invalidate()
+ self.countdownTimer = nil
+ self.onTimerExpired()
}
}
}
diff --git a/Tests/ClickItTests/CrashPreventionTests.swift b/Tests/ClickItTests/CrashPreventionTests.swift
new file mode 100644
index 0000000..d45420f
--- /dev/null
+++ b/Tests/ClickItTests/CrashPreventionTests.swift
@@ -0,0 +1,209 @@
+//
+// CrashPreventionTests.swift
+// ClickItTests
+//
+// Created by ClickIt on 2025-07-24.
+// Copyright © 2025 ClickIt. All rights reserved.
+//
+
+import XCTest
+@testable import ClickIt
+
+@MainActor
+final class CrashPreventionTests: XCTestCase {
+
+ var permissionManager: PermissionManager!
+
+ override func setUp() async throws {
+ try await super.setUp()
+ permissionManager = PermissionManager.shared
+ }
+
+ override func tearDown() async throws {
+ permissionManager.stopPermissionMonitoring()
+ try await super.tearDown()
+ }
+
+ // MARK: - Crash Prevention Tests
+
+ func testPermissionToggleNoCrash() async throws {
+ // This test simulates the conditions that previously caused crashes
+ let noCrashExpectation = expectation(description: "Permission toggle completes without crash")
+ noCrashExpectation.expectedFulfillmentCount = 10
+
+ // Start monitoring (this activates the timer that was causing crashes)
+ permissionManager.startPermissionMonitoring()
+
+ // Simulate rapid permission state changes that triggered the crash
+ for i in 0..<10 {
+ DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.1) {
+ // This simulates the permission toggle event that caused crashes
+ self.permissionManager.updatePermissionStatus()
+ noCrashExpectation.fulfill()
+ }
+ }
+
+ await fulfillment(of: [noCrashExpectation], timeout: 2.0)
+
+ // If we reach here, the concurrency fix worked
+ XCTAssertTrue(true, "Permission toggle simulation completed without crashes")
+ }
+
+ func testConcurrentTimerOperationsStability() async throws {
+ // Test multiple concurrent timer operations that previously caused crashes
+ let stabilityExpectation = expectation(description: "Concurrent operations stable")
+ stabilityExpectation.expectedFulfillmentCount = 20
+
+ // Start monitoring
+ permissionManager.startPermissionMonitoring()
+
+ // Create concurrent load similar to permission toggle scenarios
+ for i in 0..<20 {
+ DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.05) {
+ // Mix of operations that previously interacted poorly
+ if i % 3 == 0 {
+ self.permissionManager.updatePermissionStatus()
+ } else if i % 3 == 1 {
+ let _ = self.permissionManager.accessibilityPermissionGranted
+ let _ = self.permissionManager.screenRecordingPermissionGranted
+ } else {
+ let _ = self.permissionManager.allPermissionsGranted
+ }
+ stabilityExpectation.fulfill()
+ }
+ }
+
+ await fulfillment(of: [stabilityExpectation], timeout: 3.0)
+
+ XCTAssertTrue(true, "Concurrent timer operations completed without crashes")
+ }
+
+ func testPermissionMonitoringStartStopCycles() throws {
+ // Test rapid start/stop cycles that could trigger race conditions
+
+ for cycle in 0..<5 {
+ // Start monitoring
+ permissionManager.startPermissionMonitoring()
+
+ // Brief operation
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ self.permissionManager.updatePermissionStatus()
+ }
+
+ // Stop monitoring
+ Thread.sleep(forTimeInterval: 0.15)
+ permissionManager.stopPermissionMonitoring()
+
+ print("Completed start/stop cycle \(cycle + 1)/5")
+ }
+
+ // Final verification
+ XCTAssertTrue(true, "All start/stop cycles completed without crashes")
+ }
+
+ func testTimerCallbackSafetyUnderStress() async throws {
+ // Stress test the timer callback fix under high load
+ let stressTestExpectation = expectation(description: "Stress test completes")
+ stressTestExpectation.expectedFulfillmentCount = 50
+
+ // Start monitoring
+ permissionManager.startPermissionMonitoring()
+
+ // Create high-frequency operations that stress the timer callback
+ for i in 0..<50 {
+ DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.02) {
+ // Rapid-fire operations that previously caused the MainActor conflict
+ self.permissionManager.updatePermissionStatus()
+
+ // Also access properties to ensure consistency
+ let _ = self.permissionManager.accessibilityPermissionGranted
+ let _ = self.permissionManager.screenRecordingPermissionGranted
+ let _ = self.permissionManager.allPermissionsGranted
+
+ stressTestExpectation.fulfill()
+ }
+ }
+
+ await fulfillment(of: [stressTestExpectation], timeout: 2.0)
+
+ XCTAssertTrue(true, "High-frequency stress test completed without crashes")
+ }
+
+ func testReproducePreviousCrashScenario() async throws {
+ // This test specifically reproduces the crash scenario from the bug report
+ let reproductionExpectation = expectation(description: "Previous crash scenario handled safely")
+ reproductionExpectation.expectedFulfillmentCount = 1
+
+ // Start monitoring (activates the problematic timer)
+ permissionManager.startPermissionMonitoring()
+
+ // Wait for timer to be active
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ // This specific sequence previously caused the crash:
+ // 1. Timer callback executes with Task { @MainActor in }
+ // 2. User toggles permission in System Settings
+ // 3. MainActor conflict occurs
+
+ // Simulate the exact conditions
+ self.permissionManager.updatePermissionStatus()
+
+ // Verify we can continue operating
+ let accessibility = self.permissionManager.accessibilityPermissionGranted
+ let screenRecording = self.permissionManager.screenRecordingPermissionGranted
+ let all = self.permissionManager.allPermissionsGranted
+
+ // These should all be readable without crashes
+ XCTAssertNotNil(accessibility)
+ XCTAssertNotNil(screenRecording)
+ XCTAssertNotNil(all)
+
+ reproductionExpectation.fulfill()
+ }
+
+ await fulfillment(of: [reproductionExpectation], timeout: 1.0)
+
+ XCTAssertTrue(true, "Previous crash scenario now handled safely")
+ }
+
+ // MARK: - Specific Timer Callback Tests
+
+ func testDispatchQueueMainAsyncPattern() async throws {
+ // Test that the new DispatchQueue.main.async pattern works correctly
+ var callbackExecuted = false
+ let callbackExpectation = expectation(description: "Callback executed on main queue")
+
+ // Simulate the timer callback pattern we're now using
+ Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { _ in
+ DispatchQueue.main.async {
+ // This should execute safely on MainActor
+ callbackExecuted = true
+ callbackExpectation.fulfill()
+ }
+ }
+
+ await fulfillment(of: [callbackExpectation], timeout: 1.0)
+
+ XCTAssertTrue(callbackExecuted, "DispatchQueue.main.async callback executed successfully")
+ }
+
+ func testMainActorIsolationPreserved() async throws {
+ // Test that MainActor isolation is preserved with the new pattern
+ let isolationExpectation = expectation(description: "MainActor isolation preserved")
+
+ permissionManager.startPermissionMonitoring()
+
+ // Verify we can access MainActor properties after timer activation
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
+ // These accesses should work without await since we're on MainActor
+ let _ = self.permissionManager.accessibilityPermissionGranted
+ let _ = self.permissionManager.screenRecordingPermissionGranted
+ let _ = self.permissionManager.allPermissionsGranted
+
+ isolationExpectation.fulfill()
+ }
+
+ await fulfillment(of: [isolationExpectation], timeout: 1.0)
+
+ XCTAssertTrue(true, "MainActor isolation preserved with new pattern")
+ }
+}
\ No newline at end of file
diff --git a/Tests/ClickItTests/PermissionManagerConcurrencyTests.swift b/Tests/ClickItTests/PermissionManagerConcurrencyTests.swift
new file mode 100644
index 0000000..c8dca7e
--- /dev/null
+++ b/Tests/ClickItTests/PermissionManagerConcurrencyTests.swift
@@ -0,0 +1,189 @@
+//
+// PermissionManagerConcurrencyTests.swift
+// ClickItTests
+//
+// Created by ClickIt on 2025-07-24.
+// Copyright © 2025 ClickIt. All rights reserved.
+//
+
+import XCTest
+@testable import ClickIt
+
+@MainActor
+final class PermissionManagerConcurrencyTests: XCTestCase {
+
+ var permissionManager: PermissionManager!
+
+ override func setUp() async throws {
+ try await super.setUp()
+ // Create a new instance for each test to avoid shared state
+ permissionManager = PermissionManager.shared
+ }
+
+ override func tearDown() async throws {
+ // Clean up any monitoring timers
+ permissionManager.stopPermissionMonitoring()
+ try await super.tearDown()
+ }
+
+ // MARK: - Timer Callback Safety Tests
+
+ func testPermissionMonitoringTimerCallbackSafety() async throws {
+ // Test that timer callbacks don't create MainActor conflicts
+ let expectation = XCTestExpectation(description: "Timer callback completes without crashing")
+ expectation.expectedFulfillmentCount = 3
+
+ // Start monitoring to activate timer
+ permissionManager.startPermissionMonitoring()
+
+ // Allow multiple timer callbacks to execute
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ expectation.fulfill()
+ }
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
+ expectation.fulfill()
+ }
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
+ expectation.fulfill()
+ }
+
+ await fulfillment(of: [expectation], timeout: 1.0)
+
+ // If we reach here without crashing, the timer callback is safe
+ XCTAssertTrue(true, "Timer callbacks completed without MainActor conflicts")
+ }
+
+ func testConcurrentPermissionStatusUpdates() async throws {
+ // Test that multiple concurrent permission status updates don't cause crashes
+ let expectation = XCTestExpectation(description: "Concurrent updates complete safely")
+ expectation.expectedFulfillmentCount = 5
+
+ // Trigger multiple permission status updates concurrently
+ for i in 0..<5 {
+ DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.05) {
+ self.permissionManager.updatePermissionStatus()
+ expectation.fulfill()
+ }
+ }
+
+ await fulfillment(of: [expectation], timeout: 1.0)
+
+ // Verify final state is consistent
+ XCTAssertNotNil(permissionManager.accessibilityPermissionGranted)
+ XCTAssertNotNil(permissionManager.screenRecordingPermissionGranted)
+ }
+
+ func testPermissionMonitoringStartStopSafety() throws {
+ // Test that starting and stopping monitoring doesn't create race conditions
+
+ // Start monitoring
+ permissionManager.startPermissionMonitoring()
+ XCTAssertTrue(true, "Start monitoring completed without crash")
+
+ // Stop monitoring
+ permissionManager.stopPermissionMonitoring()
+ XCTAssertTrue(true, "Stop monitoring completed without crash")
+
+ // Rapid start/stop cycles
+ for _ in 0..<10 {
+ permissionManager.startPermissionMonitoring()
+ permissionManager.stopPermissionMonitoring()
+ }
+
+ XCTAssertTrue(true, "Rapid start/stop cycles completed without crashes")
+ }
+
+ func testTimerCallbackMainActorIsolation() async throws {
+ // Test that timer callbacks properly execute on MainActor
+ let expectation = XCTestExpectation(description: "Timer callback executes on MainActor")
+
+ // Start monitoring
+ permissionManager.startPermissionMonitoring()
+
+ // Check MainActor isolation after timer has chance to execute
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ // If we can access @MainActor properties without await, we're on MainActor
+ let _ = self.permissionManager.accessibilityPermissionGranted
+ let _ = self.permissionManager.screenRecordingPermissionGranted
+ expectation.fulfill()
+ }
+
+ await fulfillment(of: [expectation], timeout: 1.0)
+ XCTAssertTrue(true, "Timer callbacks maintain proper MainActor isolation")
+ }
+
+ // MARK: - Error Simulation Tests
+
+ func testPermissionStatusUpdateUnderStress() async throws {
+ // Simulate rapid permission status changes that previously caused crashes
+ let expectation = XCTestExpectation(description: "Stress test completes without crashes")
+ expectation.expectedFulfillmentCount = 20
+
+ // Start monitoring to activate timer
+ permissionManager.startPermissionMonitoring()
+
+ // Rapidly trigger status updates to simulate permission toggling
+ for i in 0..<20 {
+ DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.01) {
+ // Simulate the conditions that cause crashes during permission toggles
+ self.permissionManager.updatePermissionStatus()
+ expectation.fulfill()
+ }
+ }
+
+ await fulfillment(of: [expectation], timeout: 2.0)
+ XCTAssertTrue(true, "Stress test completed without crashes")
+ }
+
+ func testTimerCallbackMemoryManagement() throws {
+ // Test that timer callbacks don't create memory leaks or retention cycles
+ weak var weakPermissionManager: PermissionManager?
+
+ autoreleasepool {
+ let tempManager = PermissionManager.shared
+ weakPermissionManager = tempManager
+
+ // Start and stop monitoring to create timer callbacks
+ tempManager.startPermissionMonitoring()
+
+ // Let timer execute a few times
+ let expectation = XCTestExpectation(description: "Timer executes")
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ tempManager.stopPermissionMonitoring()
+ expectation.fulfill()
+ }
+
+ wait(for: [expectation], timeout: 1.0)
+ }
+
+ // PermissionManager is a singleton, so it won't be deallocated
+ // But we can verify that stopping monitoring cleans up timers properly
+ XCTAssertNotNil(weakPermissionManager)
+ }
+
+ // MARK: - Integration Tests
+
+ func testPermissionMonitoringIntegrationWithUI() async throws {
+ // Test that permission monitoring works correctly with UI updates
+ let expectation = XCTestExpectation(description: "UI integration test completes")
+
+ // Start monitoring
+ permissionManager.startPermissionMonitoring()
+
+ // Simulate UI reading permission status during timer execution
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ let accessibilityStatus = self.permissionManager.accessibilityPermissionGranted
+ let screenRecordingStatus = self.permissionManager.screenRecordingPermissionGranted
+ let allPermissions = self.permissionManager.allPermissionsGranted
+
+ // These reads should not cause crashes or inconsistent state
+ XCTAssertNotNil(accessibilityStatus)
+ XCTAssertNotNil(screenRecordingStatus)
+ XCTAssertNotNil(allPermissions)
+
+ expectation.fulfill()
+ }
+
+ await fulfillment(of: [expectation], timeout: 1.0)
+ }
+}
\ No newline at end of file
diff --git a/Tests/ClickItTests/PermissionManagerTimingValidation.swift b/Tests/ClickItTests/PermissionManagerTimingValidation.swift
new file mode 100644
index 0000000..e379be2
--- /dev/null
+++ b/Tests/ClickItTests/PermissionManagerTimingValidation.swift
@@ -0,0 +1,138 @@
+//
+// PermissionManagerTimingValidation.swift
+// ClickItTests
+//
+// Created by ClickIt on 2025-07-24.
+// Copyright © 2025 ClickIt. All rights reserved.
+//
+
+import XCTest
+@testable import ClickIt
+
+@MainActor
+final class PermissionManagerTimingValidation: XCTestCase {
+
+ var permissionManager: PermissionManager!
+
+ override func setUp() async throws {
+ try await super.setUp()
+ permissionManager = PermissionManager.shared
+ }
+
+ override func tearDown() async throws {
+ permissionManager.stopPermissionMonitoring()
+ try await super.tearDown()
+ }
+
+ func testPermissionMonitoringTimingPreserved() async throws {
+ // Test that permission monitoring maintains 1-second intervals
+ var updateCounts: [Date] = []
+ let expectedUpdates = 3
+
+ // Store original update method to track calls
+ let originalUpdateCalled = expectation(description: "Permission updates tracked")
+ originalUpdateCalled.expectedFulfillmentCount = expectedUpdates
+
+ // Start monitoring
+ permissionManager.startPermissionMonitoring()
+
+ // Track when updates happen
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ updateCounts.append(Date())
+ originalUpdateCalled.fulfill()
+ }
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.1) {
+ updateCounts.append(Date())
+ originalUpdateCalled.fulfill()
+ }
+ DispatchQueue.main.asyncAfter(deadline: .now() + 2.1) {
+ updateCounts.append(Date())
+ originalUpdateCalled.fulfill()
+ }
+
+ await fulfillment(of: [originalUpdateCalled], timeout: 3.0)
+
+ // Verify timing intervals are approximately 1 second
+ XCTAssertEqual(updateCounts.count, expectedUpdates)
+
+ if updateCounts.count >= 2 {
+ let interval1 = updateCounts[1].timeIntervalSince(updateCounts[0])
+ XCTAssertTrue(interval1 >= 0.9 && interval1 <= 1.1, "First interval should be ~1 second, got \(interval1)")
+ }
+
+ if updateCounts.count >= 3 {
+ let interval2 = updateCounts[2].timeIntervalSince(updateCounts[1])
+ XCTAssertTrue(interval2 >= 0.9 && interval2 <= 1.1, "Second interval should be ~1 second, got \(interval2)")
+ }
+ }
+
+ func testPermissionStatusConsistency() async throws {
+ // Test that permission status remains consistent during monitoring
+ let consistencyCheck = expectation(description: "Permission status consistency")
+ consistencyCheck.expectedFulfillmentCount = 5
+
+ permissionManager.startPermissionMonitoring()
+
+ // Check status multiple times to ensure consistency
+ for i in 0..<5 {
+ DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.2) {
+ let accessibility1 = self.permissionManager.accessibilityPermissionGranted
+ let screenRecording1 = self.permissionManager.screenRecordingPermissionGranted
+ let all1 = self.permissionManager.allPermissionsGranted
+
+ // Immediate second read should be identical
+ let accessibility2 = self.permissionManager.accessibilityPermissionGranted
+ let screenRecording2 = self.permissionManager.screenRecordingPermissionGranted
+ let all2 = self.permissionManager.allPermissionsGranted
+
+ XCTAssertEqual(accessibility1, accessibility2, "Accessibility permission should be stable")
+ XCTAssertEqual(screenRecording1, screenRecording2, "Screen recording permission should be stable")
+ XCTAssertEqual(all1, all2, "All permissions status should be stable")
+
+ consistencyCheck.fulfill()
+ }
+ }
+
+ await fulfillment(of: [consistencyCheck], timeout: 2.0)
+ }
+
+ func testPermissionMonitoringResourceCleanup() {
+ // Test that stopping monitoring properly cleans up resources
+
+ // Start monitoring
+ permissionManager.startPermissionMonitoring()
+ XCTAssertTrue(true, "Monitoring started without crash")
+
+ // Stop monitoring
+ permissionManager.stopPermissionMonitoring()
+ XCTAssertTrue(true, "Monitoring stopped without crash")
+
+ // Multiple stops should be safe
+ permissionManager.stopPermissionMonitoring()
+ permissionManager.stopPermissionMonitoring()
+ XCTAssertTrue(true, "Multiple stops completed safely")
+
+ // Restart should work
+ permissionManager.startPermissionMonitoring()
+ permissionManager.stopPermissionMonitoring()
+ XCTAssertTrue(true, "Restart and stop completed successfully")
+ }
+
+ func testPermissionCheckMethodsStillWork() {
+ // Test that individual permission check methods maintain functionality
+
+ let accessibilityStatus = permissionManager.checkAccessibilityPermission()
+ let screenRecordingStatus = permissionManager.checkScreenRecordingPermission()
+
+ // These methods should return consistent values
+ XCTAssertNotNil(accessibilityStatus)
+ XCTAssertNotNil(screenRecordingStatus)
+
+ // Update status and check consistency
+ permissionManager.updatePermissionStatus()
+
+ XCTAssertEqual(permissionManager.accessibilityPermissionGranted, accessibilityStatus)
+ XCTAssertEqual(permissionManager.screenRecordingPermissionGranted, screenRecordingStatus)
+ XCTAssertEqual(permissionManager.allPermissionsGranted, accessibilityStatus && screenRecordingStatus)
+ }
+}
\ No newline at end of file
diff --git a/Tests/ClickItTests/TimerAutomationEngineMainActorTests.swift b/Tests/ClickItTests/TimerAutomationEngineMainActorTests.swift
new file mode 100644
index 0000000..3f21878
--- /dev/null
+++ b/Tests/ClickItTests/TimerAutomationEngineMainActorTests.swift
@@ -0,0 +1,209 @@
+import XCTest
+@testable import ClickIt
+
+@MainActor
+final class TimerAutomationEngineMainActorTests: XCTestCase {
+
+ var timerEngine: TimerAutomationEngine!
+ var clickCoordinator: ClickCoordinator!
+ var performanceMonitor: PerformanceMonitor!
+ var permissionManager: PermissionManager!
+
+ override func setUp() async throws {
+ try await super.setUp()
+
+ permissionManager = PermissionManager.shared
+ performanceMonitor = PerformanceMonitor.shared
+ clickCoordinator = ClickCoordinator.shared
+ timerEngine = TimerAutomationEngine(
+ clickCoordinator: clickCoordinator,
+ performanceMonitor: performanceMonitor
+ )
+ }
+
+ override func tearDown() async throws {
+ timerEngine.stopAutomation()
+ timerEngine = nil
+ clickCoordinator = nil
+ performanceMonitor = nil
+ permissionManager = nil
+ try await super.tearDown()
+ }
+
+ // MARK: - MainActor Safety Tests
+
+ func testHighPrecisionTimerCallbackMainActorSafety() async throws {
+ // Test that high-precision timer callbacks execute safely on MainActor
+
+ // Create automation configuration for testing
+ let cpsConfig = CPSRandomizer.Configuration(
+ enabled: false,
+ variancePercentage: 0.0,
+ distributionPattern: .uniform,
+ humannessLevel: .low,
+ minimumInterval: 0.01,
+ maximumInterval: 1.0,
+ patternBreakupFrequency: 0.0
+ )
+
+ let config = AutomationConfiguration(
+ location: CGPoint(x: 100, y: 100),
+ clickType: .left,
+ clickInterval: 0.1, // 10 CPS
+ targetApplication: nil,
+ maxClicks: 5, // Limit clicks for testing
+ maxDuration: nil,
+ stopOnError: false,
+ randomizeLocation: false,
+ locationVariance: 0,
+ useDynamicMouseTracking: false,
+ cpsRandomizerConfig: cpsConfig
+ )
+
+ // Start automation to trigger high-precision timer
+ timerEngine.startAutomation(with: config)
+
+ // Wait briefly to allow timer callbacks (should be safe on MainActor)
+ try await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds
+
+ // Verify timer is running (should be safe to access on MainActor)
+ XCTAssertEqual(timerEngine.automationState, .running)
+
+ // Stop automation
+ timerEngine.stopAutomation()
+
+ // Verify cleanup
+ XCTAssertEqual(timerEngine.automationState, .idle)
+ }
+
+ func testConcurrentPermissionMonitoringWithHighPrecisionTimer() async throws {
+ // Test that permission monitoring doesn't interfere with high-precision timer
+
+ // Start permission monitoring
+ permissionManager.startPermissionMonitoring()
+
+ // Create test configuration
+ let cpsConfig = CPSRandomizer.Configuration(
+ enabled: false,
+ variancePercentage: 0.0,
+ distributionPattern: .uniform,
+ humannessLevel: .low,
+ minimumInterval: 0.01,
+ maximumInterval: 1.0,
+ patternBreakupFrequency: 0.0
+ )
+
+ let config = AutomationConfiguration(
+ location: CGPoint(x: 100, y: 100),
+ clickType: .left,
+ clickInterval: 0.05, // 20 CPS
+ targetApplication: nil,
+ maxClicks: 3, // Limit for testing
+ maxDuration: nil,
+ stopOnError: false,
+ randomizeLocation: false,
+ locationVariance: 0,
+ useDynamicMouseTracking: false,
+ cpsRandomizerConfig: cpsConfig
+ )
+
+ // Start concurrent operations
+ timerEngine.startAutomation(with: config)
+
+ // Allow concurrent operations to run
+ try await Task.sleep(nanoseconds: 300_000_000) // 0.3 seconds
+
+ // Verify timer is operating
+ XCTAssertEqual(timerEngine.automationState, .running)
+
+ // Stop operations
+ timerEngine.stopAutomation()
+ permissionManager.stopPermissionMonitoring()
+
+ // Verify clean shutdown
+ XCTAssertEqual(timerEngine.automationState, .idle)
+ }
+
+ func testTimerCallbackErrorHandling() async throws {
+ // Test that timer callbacks handle errors gracefully without crashing
+
+ // Create configuration with potentially problematic values
+ let cpsConfig = CPSRandomizer.Configuration(
+ enabled: false,
+ variancePercentage: 0.0,
+ distributionPattern: .uniform,
+ humannessLevel: .low,
+ minimumInterval: 0.01,
+ maximumInterval: 1.0,
+ patternBreakupFrequency: 0.0
+ )
+
+ let config = AutomationConfiguration(
+ location: CGPoint(x: -100, y: -100), // Invalid coordinates
+ clickType: .left,
+ clickInterval: 0.2, // 5 CPS
+ targetApplication: "NonExistentApp",
+ maxClicks: 2, // Limit for testing
+ maxDuration: nil,
+ stopOnError: false, // Continue despite errors
+ randomizeLocation: false,
+ locationVariance: 0,
+ useDynamicMouseTracking: false,
+ cpsRandomizerConfig: cpsConfig
+ )
+
+ // Start automation - should not crash despite invalid configuration
+ timerEngine.startAutomation(with: config)
+
+ // Allow timer to attempt clicks with invalid config
+ try await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds
+
+ // Should still be able to stop safely
+ timerEngine.stopAutomation()
+
+ // Verify clean shutdown
+ XCTAssertEqual(timerEngine.automationState, .idle)
+ }
+
+ func testHighFrequencyTimerStability() async throws {
+ // Test high-frequency timer operations for MainActor stability
+
+ // Configure for high frequency
+ let cpsConfig = CPSRandomizer.Configuration(
+ enabled: false,
+ variancePercentage: 0.0,
+ distributionPattern: .uniform,
+ humannessLevel: .low,
+ minimumInterval: 0.01,
+ maximumInterval: 1.0,
+ patternBreakupFrequency: 0.0
+ )
+
+ let config = AutomationConfiguration(
+ location: CGPoint(x: 200, y: 200),
+ clickType: .left,
+ clickInterval: 0.02, // 50 CPS
+ targetApplication: nil,
+ maxClicks: 5, // Limit for testing
+ maxDuration: nil,
+ stopOnError: false,
+ randomizeLocation: false,
+ locationVariance: 0,
+ useDynamicMouseTracking: false,
+ cpsRandomizerConfig: cpsConfig
+ )
+
+ // Start high-frequency automation
+ timerEngine.startAutomation(with: config)
+
+ // Run for short duration to test stability
+ try await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds
+
+ // Verify timer is still active and responsive
+ XCTAssertEqual(timerEngine.automationState, .running)
+
+ // Should be able to stop cleanly
+ timerEngine.stopAutomation()
+ XCTAssertEqual(timerEngine.automationState, .idle)
+ }
+}
\ No newline at end of file
diff --git a/Tests/ClickItTests/TimerConcurrencyValidationTest.swift b/Tests/ClickItTests/TimerConcurrencyValidationTest.swift
new file mode 100644
index 0000000..d91aa4c
--- /dev/null
+++ b/Tests/ClickItTests/TimerConcurrencyValidationTest.swift
@@ -0,0 +1,51 @@
+import XCTest
+@testable import ClickIt
+
+@MainActor
+final class TimerConcurrencyValidationTest: XCTestCase {
+
+ func testTimerAutomationEngineBuildsWithoutConcurrencyIssues() async throws {
+ // This test simply verifies that TimerAutomationEngine can be instantiated
+ // and started without concurrency crashes, validating our MainActor fixes
+
+ let timerEngine = TimerAutomationEngine()
+
+ // Create minimal test configuration
+ let cpsConfig = CPSRandomizer.Configuration(
+ enabled: false,
+ variancePercentage: 0.0,
+ distributionPattern: .uniform,
+ humannessLevel: .low,
+ minimumInterval: 0.01,
+ maximumInterval: 1.0,
+ patternBreakupFrequency: 0.0
+ )
+
+ let config = AutomationConfiguration(
+ location: CGPoint(x: 100, y: 100),
+ clickType: .left,
+ clickInterval: 0.5, // 2 CPS - slow for testing
+ targetApplication: nil,
+ maxClicks: 1, // Just one click
+ maxDuration: nil,
+ stopOnError: false,
+ randomizeLocation: false,
+ locationVariance: 0,
+ useDynamicMouseTracking: false,
+ cpsRandomizerConfig: cpsConfig
+ )
+
+ // Start automation - this should not crash due to concurrency issues
+ timerEngine.startAutomation(with: config)
+
+ // Brief wait to allow timer callback to execute safely
+ try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
+
+ // Stop automation
+ timerEngine.stopAutomation()
+
+ // Test passes if we reach here without crashes
+ XCTAssertEqual(timerEngine.automationState, .idle)
+ print("✅ TimerAutomationEngine concurrency fix validated successfully")
+ }
+}
\ No newline at end of file