Skip to content

Bug-hunt v2: fix 41 bugs (fuzzing + dogfood pass)#24

Merged
jtn0123 merged 6 commits into
masterfrom
bug-hunt-2-fixes
May 20, 2026
Merged

Bug-hunt v2: fix 41 bugs (fuzzing + dogfood pass)#24
jtn0123 merged 6 commits into
masterfrom
bug-hunt-2-fixes

Conversation

@jtn0123
Copy link
Copy Markdown
Owner

@jtn0123 jtn0123 commented May 20, 2026

Summary

Second multi-agent bug hunt against AudioWhisper (after PR #22 fixed 44). This round was a fuzzing + dogfood pass: 5 hunter agents → 41 raw candidates → 4 validators → 41 confirmed actionable bugs → 5 Opus workers in isolated worktrees → 5 Opus validators reviewed each commit.

Final review: 37 SOLID / 4 WEAK / 0 BROKEN. Full test suite green: 2805 tests, 0 failures, 37 skipped.

Bugs fixed by severity

CRITICAL (1)

  • C1 Closing Welcome window via red dot silently broke recording forever

HIGH (22)

  • H1 Hotkey during processing → second-recording attempt + misleading toast
  • H2 Same flaw on push-and-hold path
  • H3 Toggling Immediate Recording mid-record stranded the recorder
  • H4 No sleep / engine-config observer → silent truncation on lid close
  • H5 MicrophoneVolumeManager unsynchronized → mic stuck at 100%
  • H6 Concurrent daemon spawn during restart → orphan subprocess
  • H7 applicationWillTerminate fire-and-forgot daemon shutdown
  • H8 uv sync pipe deadlock on first launch (>64KiB stdout)
  • H9 Daemon stdin write inside actor → mutual deadlock
  • H10 KeyboardEventHandler retain cycle (NSEvent closures captured self strongly)
  • H11 NotificationCoordinator deinit skipped async-stream + handler tasks
  • H12 RecordingViewModel had no deinit → 3 notification loops survived dropped VM
  • H13 Search-while-pagination silently dropped keystrokes
  • H14 Delete-record reload skipped during pagination
  • H15 "Request Access" was a silent no-op once mic was denied
  • H16 Verify MLX failure left "Checking model (offline)…" forever
  • H17 Deleting selected MLX model swapped to a possibly-undownloaded fallback; two screens disagreed
  • H18 Categories Reset had no confirmation → silently wiped custom prompts
  • H19 CategoryStore crashed on launch if categories.json had duplicate ids
  • H20 Unbounded Levenshtein in safeMerge → multi-second UI freeze on long transcripts
  • H21 KeychainService collapsed all errors into .itemNotFound (Time Machine restore looked like "no key set")
  • H22 storedTargetApp never cleared → stale paste target after app switch

MEDIUM (14)

  • M1 MLX download progress bar hardcoded to 0% → indeterminate spinner
  • M2 "Open Recordings Folder" opened system temp dir → renamed to "Open App Data Folder" pointing at ~/Library/Application Support/AudioWhisper
  • M3 AccessibilityPermissionManager abandoned completion handler on supersession → UI stuck on "requesting"
  • M4 Category editor saved empty/whitespace identifier
  • M5 Category icon field accepted any string; invalid SF Symbols rendered blank
  • M6 MLDaemon transcribe/correct had no input size cap (now 1 MiB)
  • M7 MLX system-prompt file: no size cap + silent encoding fallback (now 64 KiB cap + utf16/lossy-utf8 fallbacks with warning logs)
  • M8 MLDaemon pipes leaked on proc.run() throw
  • M9 MLXModelManager pipes leaked on download spawn throw
  • M10 PressAndHold watchdog Timer never invalidated when deinit-while-pressed
  • M11 MicTestCapture spawned a MainActor Task per audio buffer (~47/s) → 60Hz throttle
  • M12 Any "parakeet" in an error message → "Configure Python" misleading recovery → tightened
  • M13 Error-recovery affordances used English substring matching (partial: added NSError-domain matcher; substring stays as fallback)
  • M14 showRecordingWindowForProcessing silently swallowed when audioRecorder was nil

LOW (4)

  • L1 HF --// reversal was lossy for repos with -- in slug → forward escaped-to-repo lookup map first
  • L2 MLXModelManager.deleteModel used unvalidated repo string → strict org/name validator
  • L3 AppCategoryManager.userMappings grew unbounded → 200-entry cap with deterministic eviction
  • L4 readabilityHandler cleared after MainActor.run → stale "Downloading…" string after success → moved clear inside the MainActor block, before removeValue

Documented follow-ups (4 WEAK items — functionally correct, not blocking)

  • H4 posts .recordingStartFailed on route-change interrupt; the recording was already in progress, not failing to start. A dedicated .recordingInterrupted notification would be more accurate.
  • H5 uses @MainActor on MicrophoneVolumeManager. Functionally correct, but pins Core Audio HAL property setters to the main thread. An actor would be a tighter fit.
  • H6 uses a 10ms polling spinlock to await isStarting. Functionally correct (cancellation isn't needed in practice), but a stored Task reference would be cleaner.
  • M2 rename of "Open Recordings Folder" → "Open App Data Folder" reframes the bug rather than restoring a recordings-folder feature that doesn't exist (audio files are ephemeral). This is the right product call but worth noting as scope-reframe rather than a literal fix.

Methodology

  • 5 hunters (concurrency, input/fuzzing, state-machine, resource-leak, UI/UX) — 42 raw candidates, 2 cross-hunter duplicates → 40 unique
  • 4 validators read the actual code paths → 38 confirmed + 2 partial + 3 rejected (B6 JSON-RPC id, X6 cleanup race, U8a button-affordance) → 41 actionable
  • 5 Opus workers in isolated git worktrees, each owning ~6-10 bugs
  • 5 Opus reviewers compared each commit against the original bug spec
  • Cherry-picked clean (no conflicts) into bug-hunt-2-fixes from master @ ad33346
  • Full swift test sequential: 2805 tests, 0 failures, 37 skipped (~63s)
  • swift build clean

Test plan

  • swift build clean
  • swift test (sequential) — 2805 passed, 0 failed, 37 skipped
  • Independent code review per worker commit
  • CI: build-and-test, lint, CodeQL, SonarCloud (run by GitHub Actions)
  • Manual smoke: deploy to /Applications, hit hotkey, transcribe, verify recording UI

🤖 Generated with Claude Code

jtn0123 and others added 5 commits May 19, 2026 20:26
- H1: Ignore hotkey press while transcription is still processing (no
  more misleading "recordingStartFailed" toast).
- H2: Ignore press-and-hold key-down during transcription processing.
- H3: Always stop active recorder on hotkey press regardless of the
  immediateRecording toggle — prevents stranded recorder when the
  user flips the setting mid-recording.
- H4: Observe NSWorkspace.willSleepNotification and
  AVAudioEngineConfigurationChange in AudioEngineRecorder so sleep /
  route changes commit (or surface) the recording instead of silently
  truncating it. Observers torn down on stop/cancel/deinit.
- H5: Make MicrophoneVolumeManager @mainactor so concurrent
  boost/restore from AudioEngineRecorder no longer race on
  originalVolume / isVolumeBoosted (mic-stuck-at-100% bug).
- M10: PressAndHoldKeyMonitor.stopWatchdog now invalidates the Timer
  synchronously (capturing it into a local) and marshals to the main
  RunLoop only for the actual invalidate call. Prior async-Task
  approach lost the timer reference when called from deinit.
- M11: MicTestCapture throttles the per-buffer MainActor Task to ~60Hz
  so audio callbacks can't pile up an unbounded queue while main
  stalls.
- M14: Surface a user-visible alert + log when
  showRecordingWindowForProcessing can't build the recording window
  (audioRecorder/ModelContainer nil), instead of silently calling the
  completion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- H6: add isStarting guard so queued ensureDaemonRunning callers wait
  for an in-flight startProcess instead of launching a duplicate daemon
- H7: convert applicationWillTerminate's fire-and-forget shutdown into
  applicationShouldTerminate with .terminateLater so the Python daemon
  is fully shut down before the app exits
- H8: drain stdout/stderr concurrently in UvBootstrap.runProcess BEFORE
  waitUntilExit so a child writing >64KiB can't deadlock on a full pipe
- H9: move the synchronous stdin write off the actor via Task.detached
  so a blocked write can't hold actor isolation against handle(line:)
- M6: cap each MLDaemon JSON-RPC payload at 1 MiB and reject oversize
  inputs early instead of DoSing the Python side
- M7: enforce a 64 KiB size cap on the MLX system-prompt file and add
  utf16 + lossy-utf8 fallbacks with warnings logged to Console.app
- M8: clean up pipes/readabilityHandler in MLDaemon startProcess catch
  block when proc.run() throws
- M9: clear readability handlers and close file handles in the two
  MLXModelManager download-spawn catch blocks
- L1: reverse HF cache directory names via an explicit known-repos
  table so repo names containing "--" round-trip losslessly
- L4: stop listening (readabilityHandler = nil) inside MainActor.run
  BEFORE clearing progress so a late callback can't restore a stale
  "Downloading..." string

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- C1: Welcome window red-close button now sets hasCompletedWelcome so
  the next hotkey isn't silently dropped by WindowController's guard.
- H15: When microphone state is .denied, "Request Access" now routes
  to System Settings → Privacy → Microphone (macOS won't re-prompt).
- H16: Verify MLX failure now always overwrites the in-progress
  "Checking model…" string with a concrete error (including stderr
  detail when available).
- H17: Both MLX screens use a shared helper, nextSelectionAfterDeletion,
  that prefers an already-downloaded model and falls back to "no MLX
  model installed" — no more swapping to an undownloaded fallback or
  the two screens disagreeing.
- H18: Categories "Reset" now opens an NSAlert confirmation describing
  that custom categories will be deleted, matching the existing
  TranscriptionHistoryView pattern.
- M1: MLX download progress bar in the correction picker is now
  indeterminate (no fake 0% determinate bar) since Python downloads
  don't report fractional progress; MLXModelManagementView already
  used an indeterminate spinner via UnifiedModelRow.
- M2: "Open Recordings Folder" was opening the system temp directory.
  Renamed to "Open App Data Folder" and pointed at the AudioWhisper
  Application Support folder, which actually holds persistent data.
- M3: AccessibilityPermissionManager now invokes completion(false) when
  a polling chain is superseded so the caller's UI moves out of
  .requesting instead of being stuck.
- M4: Category editor now rejects an empty/whitespace identifier with
  a clear validation error before checking for duplicates.
- M5: Category editor rejects non-SF-Symbol icon strings with a clear
  validation error so saved categories don't render as blank glyphs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- H10: Add [weak self] to KeyboardEventHandler NSEvent monitor closures so deinit fires and monitors are removed
- H11: Cancel `tasks` (async streams) and `handlerTasks` (in-flight handlers) in NotificationCoordinator.deinit
- H12: Add deinit to RecordingViewModel that cancels notificationTasks when the SwiftUI view tree drops the VM without onDisappear
- H13: Replace `guard !isLoading` drop pattern in TranscriptionHistoryViewModel with cancel-and-retry plus 200ms debounce so the latest search wins
- H14: Same fix as H13 covers the delete-record reload path
- H22: Clear WindowController.storedTargetApp in hideWindow and after restoreFocusToPreviousApp so stale NSRunningApplication doesn't survive across sessions
- L3: Cap AppCategoryManager.userMappings at 200 entries with deterministic alphabetical eviction; also enforced on load to trim oversized existing storage

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- H19: CategoryStore no longer crashes on duplicate ids in categories.json; deduplicates in-memory and heals on disk
- H20: SemanticCorrectionService.safeMerge caps Levenshtein at 4000 chars and falls back to length-ratio heuristic for long transcripts
- H21: KeychainError gains osStatusError(OSStatus) so non-success/non-not-found statuses surface real OSStatus instead of collapsing to itemNotFound
- L2: MLXModelManager.deleteModel validates repo identifier (rejects "..", leading "/", null bytes, control chars, non-org/name shapes)
- M12: TranscriptionError no longer routes generic "parakeet"/"python" messages to pythonConfigurationError unless config keywords are present
- M13: Adds structural NSError domain/code matcher to TranscriptionError.from(error:) and ErrorPresenter.showError(_:Error) so localized macOS systems still get correct recovery affordances; English substring matcher preserved as fallback

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

Warning

Rate limit exceeded

@jtn0123 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 26 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8e426cf8-8021-4f89-84d6-315eb154a396

📥 Commits

Reviewing files that changed from the base of the PR and between ad33346 and dba7fca.

📒 Files selected for processing (41)
  • Sources/App/AppDelegate+Hotkeys.swift
  • Sources/App/AppDelegate+Lifecycle.swift
  • Sources/App/AppDelegate+Notifications.swift
  • Sources/App/AppDelegate+RecordingWindow.swift
  • Sources/App/AppDelegate.swift
  • Sources/Managers/AccessibilityPermissionManager.swift
  • Sources/Managers/AppCategoryManager.swift
  • Sources/Managers/KeyboardEventHandler.swift
  • Sources/Managers/MLDaemonManager+Process.swift
  • Sources/Managers/MLDaemonManager.swift
  • Sources/Managers/MicrophoneVolumeManager.swift
  • Sources/Managers/PermissionManager.swift
  • Sources/Managers/PressAndHoldKeyMonitor.swift
  • Sources/Managers/Windows/WelcomeWindow.swift
  • Sources/Managers/Windows/WindowController.swift
  • Sources/Models/TranscriptionError.swift
  • Sources/Services/Audio/AudioEngineRecorder+Interruptions.swift
  • Sources/Services/Audio/AudioEngineRecorder.swift
  • Sources/Services/KeychainService.swift
  • Sources/Services/MLXCorrectionService.swift
  • Sources/Services/MLXModelManager+Downloads.swift
  • Sources/Services/MLXModelManager.swift
  • Sources/Services/SemanticCorrectionService.swift
  • Sources/Services/UvBootstrap+Process.swift
  • Sources/Stores/CategoryStore.swift
  • Sources/Utilities/ErrorPresenter.swift
  • Sources/Utilities/NotificationCoordinator.swift
  • Sources/Utilities/NotificationNames.swift
  • Sources/ViewModels/RecordingViewModel.swift
  • Sources/ViewModels/TranscriptionHistoryViewModel.swift
  • Sources/Views/Components/Waveform/MicTestCapture.swift
  • Sources/Views/Dashboard/CategoryEditorSheet.swift
  • Sources/Views/Dashboard/DashboardCategoriesView.swift
  • Sources/Views/Dashboard/DashboardCorrection+ModelPicker.swift
  • Sources/Views/Dashboard/DashboardCorrection+Verify.swift
  • Sources/Views/Dashboard/DashboardPreferencesView.swift
  • Sources/Views/Dashboard/MLXModelManagementView.swift
  • Tests/CategoryStoreTests.swift
  • Tests/MLXModelManagerTests.swift
  • Tests/TranscriptionErrorTests+Structural.swift
  • Tests/TranscriptionErrorTests.swift
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bug-hunt-2-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- Break two long alert strings (AppDelegate+RecordingWindow.swift,
  DashboardCategoriesView.swift)
- Replace String(data:encoding:) with String(bytes:encoding:) in
  UvBootstrap+Process.swift and MLXCorrectionService.swift (drops
  intentional lossy String(decoding:as:) — log warns instead)
- Refactor TranscriptionError.from(errorMessage:) into per-domain
  helpers (cyclomatic complexity 16 → ≤15) and drop the literal TODO
  marker in the M13 doc comment
- Fold single-if for-loops in MLXModelManager and CategoryStore into
  where-clauses (for_where)
- Extract H4 interruption observers + downsampleForDisplay into
  AudioEngineRecorder+Interruptions.swift (type body length 305 → ≤250)
- Split M13 structural-matcher tests into TranscriptionErrorTests+Structural.swift

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
41.1% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@jtn0123 jtn0123 merged commit da5e242 into master May 20, 2026
8 of 9 checks passed
@jtn0123 jtn0123 deleted the bug-hunt-2-fixes branch May 20, 2026 04:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant