Skip to content

Grade-report fixes: test quality, security, performance#20

Merged
jtn0123 merged 6 commits into
masterfrom
grade-fixes-top8
May 17, 2026
Merged

Grade-report fixes: test quality, security, performance#20
jtn0123 merged 6 commits into
masterfrom
grade-fixes-top8

Conversation

@jtn0123
Copy link
Copy Markdown
Owner

@jtn0123 jtn0123 commented May 16, 2026

Summary

Executes the highest-leverage items from the 2026-05-16 codebase audit (.claude/grade-report.md). Six focused commits:

  • D1 + D2 — test quality. The core TranscriptionPipelineTests caught every error into XCTAssertTrue(true) — a broken pipeline still passed. They now inject a stub speech service and assert the transcript, correction outcome, and error propagation. All ~52 XCTAssertTrue(true) tautologies across the suite are replaced with concrete assertions.
  • E1 — model integrity. ModelIntegrity no longer trusts the first download of a shipped model unconditionally — it verifies against a known-good SHA-256 table (hard fail on mismatch), keeping trust-on-first-use only for user-added models. The hash table ships empty with a documented placeholder; real release hashes must be populated before the pin is effective.
  • E3 — daemon least-privilege. The MLX/Parakeet Python daemon no longer inherits the full parent environment — it gets a minimal allowlist.
  • G1 — waveform timers. The 8 waveform renderers each ran an always-on 30fps timer even off-screen; a new FrameTimer helper starts/stops the subscription on appear/disappear.
  • C2 — @AppDefault. Constrained to Equatable so update() compares directly instead of forcing a redundant async write every body pass.
  • B5 — save path. Retention cleanup moved off the transcription-save critical path into a detached Task.

Not included

  • B3 (deleteRecord rebuild) — swapped for B5: B3's fix needs a design call (incremental decrement vs. the current self-correcting rebuild) and the "quadratic" case has no actual caller.
  • A2 (views bypass ViewModels) — deferred. It's a genuine 7-file ViewModel refactor and belongs in its own PR.

Test plan

  • swift build + swift build --build-tests clean
  • swiftlint --strict clean
  • Full suite: 2779 tests, 0 failures, 37 skipped

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Enhanced model integrity verification system with support for pinned model validation.
  • Performance & Stability

    • Optimized data saving with asynchronous cleanup operations.
    • Improved waveform animation reliability through frame-based timing.
    • Reduced Python daemon environment exposure for better security.
    • Fixed shimmer animation stopping when views disappear.
  • Tests

    • Expanded test coverage with meaningful behavioral assertions.

Review Change Stack

jtn0123 and others added 6 commits May 16, 2026 16:41
AppDefault's update() compared values by casting to AnyHashable and
returned false for any non-AnyHashable value — forcing a redundant
DispatchQueue.main.async write on every body pass. Constraining
Value: Equatable lets it compare directly; every @AppDefault value
type is already Equatable. (grade-report C2)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
saveTranscription awaited cleanupExpiredRecordsQuietly() — a full
predicate fetch + delete — before returning, so every save (and the
UI waiting on it) paid that cost. Cleanup now runs in a detached
Task, matching how initial cleanup already runs. (grade-report B5)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The MLX/Parakeet daemon subprocess inherited the full parent
environment. It now receives only an allowlist — PATH, HOME,
PYTHONUNBUFFERED, plus locale, temp, cache, and venv/HuggingFace
variables the daemon genuinely needs — for least privilege.
(grade-report E3)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ModelIntegrity trusted the first download of any model unconditionally.
It now verifies app-shipped models against a known-good SHA-256 table
(hard fail on mismatch, even on first download), mirroring how the
bundled uv binary is verified; user-added models keep trust-on-first-
use. The hash table ships empty with a documented placeholder — real
release hashes must be populated before the pin takes effect.
(grade-report E1)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each of the 8 waveform renderers installed a 30fps Timer.publish that
autoconnected on view init and ran forever — even off-screen or idle,
churning the main thread (the Visuals grid alone = ~240 updates/sec).
A new FrameTimer helper makes the timer subscription start on
onAppear and fully cancel on onDisappear; on-screen visual behavior
is unchanged. (grade-report G1)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The core TranscriptionPipeline tests caught every error into
XCTAssertTrue(true), so a broken pipeline still passed. They now
inject a stub speech service and assert the returned transcript,
the correction outcome, and error propagation. Separately, all ~52
XCTAssertTrue(true) tautologies across the suite are replaced with
concrete assertions (or removed where reaching the line is itself
the assertion). (grade-report D1, D2)

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

coderabbitai Bot commented May 16, 2026

📝 Walkthrough

Walkthrough

This PR implements three major themes: hardening daemon subprocess isolation via environment allowlist, extending model integrity verification with optional pinned-hash support alongside trust-on-first-use, refactoring waveform animations to a centralized FrameTimer component, and systematically upgrading 20+ test files from placeholder assertions to meaningful behavior validation.

Changes

Core Application Logic Changes

Layer / File(s) Summary
Daemon Environment Allowlist Hardening
Sources/Managers/MLDaemonManager+Process.swift
startProcess now constructs a hardened environment via daemonEnvironment() that always sets PATH, HOME, PYTHONUNBUFFERED and conditionally includes only allowlisted optional keys (locale, venv, HuggingFace cache vars) from parent environment instead of merging the full environment.
Model Integrity Dual-Mode Verification System
Sources/Utilities/DiskMutationSerializer.swift, Sources/Services/MLXModelManager+Downloads.swift, Sources/Services/ModelManager.swift
ModelIntegrity expanded with knownHashes table and knownHash(for:) lookup. verify(at:modelIdentifier:) and quietVerify(at:modelIdentifier:) now accept optional identifier: when pinned, computes SHA-256 and hard-fails pinnedMismatch on hash difference; otherwise falls back to trust-on-first-use sidecar validation. MLXModelManager and ModelManager updated to pass repo/model identifier at call sites.
Async Persistence and Property Wrapper Constraints
Sources/Stores/DataManager.swift, Sources/Utilities/AppDefault.swift
DataManager.saveTranscription() triggers retention cleanup via detached async Task instead of blocking on await. AppDefault<Value> now constrains Value: Equatable and updates equality detection from AnyHashable-based to compile-time != comparison.

Waveform Animation Refactoring

Layer / File(s) Summary
FrameTimer Component Foundation
Sources/Views/Components/Waveform/FrameTimer.swift
New FrameTimer class provides stoppable per-frame timer: stores interval, exposes PassthroughSubject<Date, Never> publisher, guards against duplicate starts via start(), and fully cancels underlying timer on stop().
Waveform View Migration to FrameTimer
Sources/Views/Components/Waveform/ClassicWaveformView.swift, ConstellationWaveformView.swift, DialWaveformView.swift, HaloWaveformView.swift, HeartbeatPulseView.swift, NeonWaveformView.swift, SpectrumWaveformView.swift, StreamWaveformView.swift
Nine waveform views unified to use @State FrameTimer started/stopped in onAppear/onDisappear, replacing inline Timer.publish(...).autoconnect() with optional isViewActive gates. Frame updates driven by frameTimer.publisher instead of guarded timer subscriptions.
Shimmer Animation Lifecycle Control
Sources/Views/Components/Waveform/WaveformContainer.swift
ProcessingShimmerView now resets phase to 0 in zero-duration animation on .onDisappear, halting off-screen shimmer re-renders.

Comprehensive Test Suite Enhancements

Layer / File(s) Summary
AppDelegate Lifecycle and Menu Behavior Tests
Tests/AppDelegate/AppDelegateMenuTests.swift, AppDelegateNotificationsTests.swift, AppDelegateRecordingWindowTests.swift
Menu action tests (showHistory, showDashboard) upgraded to assert statusItem non-mutation. Notification observer tests refactored with async helper awaitNotificationProcessed() to validate pressAndHoldConfiguration reconfiguration, statusItem preservation across multiple notifications, observer deallocation cleanup, and payload-based configuration updates. Window tests assert non-creation/non-mutation of recordingWindow and ChromelessWindow responder behavior.
Audio Recording and Transcription Service Tests
Tests/AudioEngineRecorderTests.swift, Tests/SpeechToTextServiceTests.swift, Tests/TranscriptionPipelineTests.swift
Removed compile-time protocol conformance placeholders. Added test-only StubSpeechToTextService to validate transcription pipeline end-to-end: raw transcript passthrough when correction disabled, .skipped outcome when forced off, provider error propagation, and stricter "model required" error message matching for .local provider.
Error Classification and Recovery Tests
Tests/ErrorPresenterExpandedTests.swift, Tests/Integration/ErrorPropagationLayerTests.swift, Tests/Integration/ErrorRecoveryIntegrationTests.swift
Upgraded error tests to assert absence of unintended retry notifications via inverted expectations, pattern-match error cases with associated value validation (e.g., .modelNotFound(let model) checked for "large"), and use explicit guard case with XCTFail on mismatch instead of placeholder success assertions.
View Component State and Utility Assertion Tests
Tests/Views/Components/InkRippleViewTests.swift, Tests/Views/Dashboard/DashboardTranscriptsViewTests.swift, Tests/Views/Dashboard/MLXModelManagementViewTests.swift, Tests/Views/Dashboard/UsageDashboardViewTests.swift, Tests/Views/Transcription/TranscriptionRecordRowTests.swift, Tests/Views/WelcomeViewTests.swift, Tests/Waveform/ParticleSystemTests.swift, Tests/Utilities/LoggerTests.swift, Tests/Utilities/ResourceLocatorTests.swift, Tests/Utilities/WindowTitlesTests.swift, Tests/WindowControllerTests.swift
Systematically replaced "does not crash" placeholders with behavioral assertions: view body evaluation validates state retention (audioLevel, isActive), container accessor idempotency, model refresh registry consistency, store reset/rebuild snapshot state, duration formatting, resource URL properties, architecture-specific flags, and logger non-throw behavior.

Sequence Diagram(s)

sequenceDiagram
  participant MLXModelManager
  participant ModelIntegrity
  participant Disk
  MLXModelManager->>ModelIntegrity: verify(at: fileURL, modelIdentifier: "repo")
  ModelIntegrity->>ModelIntegrity: knownHash(for: "repo")
  alt Pinned Hash Found
    ModelIntegrity->>Disk: Read file, compute SHA-256
    ModelIntegrity->>ModelIntegrity: Compare computed vs. pinned
    alt Mismatch
      ModelIntegrity-->>MLXModelManager: throw pinnedMismatch error
    else Match
      ModelIntegrity->>Disk: Update sidecar (optional)
      ModelIntegrity-->>MLXModelManager: Success
    end
  else No Pinned Hash
    ModelIntegrity->>Disk: Read sidecar hash (if exists)
    alt Sidecar Exists and Matches
      ModelIntegrity-->>MLXModelManager: Success (TOFU verified)
    else Sidecar Exists but Mismatches
      ModelIntegrity-->>MLXModelManager: throw mismatch error
    else No Sidecar
      ModelIntegrity->>Disk: Write computed hash to sidecar
      ModelIntegrity-->>MLXModelManager: Success (first use recorded)
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • jtn0123/AudioWhisper#16: Extended the ModelIntegrity sidecar verification system introduced in this PR with pinned-hash validation for app-shipped models.
  • jtn0123/AudioWhisper#17: Redesigned waveform container and component rendering while this PR refactors the underlying animation tick mechanism to use centralized FrameTimer.

Poem

🐰 With daemon walls drawn tight and hashes pinned just so,
The waveforms now all tick in sync—no scattered timers' flow,
Each test now speaks the truth it sought, no "true" without a claim,
A rabbit hops through cleaner code and celebrates the same! 🏃✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main theme of the pull request: addressing grade-report findings across three key areas (test quality, security, performance).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch grade-fixes-top8

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.

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

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

See analysis details on SonarQube Cloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Managers/MLDaemonManager`+Process.swift:
- Around line 87-98: The environment pass-through array passThroughIfSet in
MLDaemonManager+Process.swift currently omits SSL certificate overrides; update
passThroughIfSet to include "SSL_CERT_FILE", "SSL_CERT_DIR",
"REQUESTS_CA_BUNDLE", and "CURL_CA_BUNDLE" so the for loop that reads
parent[...] and writes env[...] passes these through to the Python daemon;
ensure the new keys are added alongside the existing variables used by the env
population logic (passThroughIfSet, parent, env) so corporate/custom CA settings
are preserved.

In `@Sources/Stores/DataManager.swift`:
- Line 169: The call launches an unstructured Task which inherits the current
actor/context; replace Task { await cleanupExpiredRecordsQuietly() } with a
detached background task so the cleanup doesn't inherit MainActor/foreground
priority. Locate the invocation in DataManager (the
cleanupExpiredRecordsQuietly() call) and change it to use Task.detached { await
cleanupExpiredRecordsQuietly() } so the work runs off the main actor with
detached/default priority.

In `@Sources/Views/Components/Waveform/FrameTimer.swift`:
- Around line 15-38: Add MainActor isolation to the FrameTimer utility by
annotating the FrameTimer type with `@MainActor` so its mutable state (interval,
cancellable, publisher) and methods start() / stop() are executed on the main
actor; update any call sites if needed to await or hop to the main actor when
constructing or invoking FrameTimer, and ensure references to publisher,
cancellable, start(), stop(), and init(interval:) remain consistent after the
annotation.

In `@Tests/AppDelegate/AppDelegateNotificationsTests.swift`:
- Around line 24-30: awaitNotificationProcessed currently posts the notification
then uses a fixed delay to assume processing, which doesn't actually synchronize
with observers; replace the fixed-delay approach by creating an XCTest
expectation bound to the notification (e.g. using expectation(forNotification:
name, object: object, handler: nil) or
NotificationCenter.addObserver(forName:object:queue:using:)-backed expectation),
then post the notification and wait(for: [expectation], timeout:), removing the
DispatchQueue.main.asyncAfter-based fulfill so the test truly waits for the
observer invoked by awaitNotificationProcessed.

In `@Tests/Utilities/ResourceLocatorTests.swift`:
- Around line 35-49: The test
testURLForResourceWithDevRelativePathResolvesExistingFile is nondeterministic
because it silently allows url == nil; update it to assert deterministically by
checking the dev fixture file existence directly: call
ResourceLocator.url(forResource:withExtension:devRelativePath:) as before, then
compute the expectedDevPath ("Sources/parakeet_transcribe_pcm.py") and use
FileManager.default.fileExists(atPath:) to decide assertions—if the file exists
assert url is non-nil, lastPathComponent equals "parakeet_transcribe_pcm.py",
and fileExists(atPath: url.path) is true; otherwise assert url is nil. This
ensures failures occur when the fixture is present but resolution returns nil.

In `@Tests/Views/Transcription/TranscriptionRecordRowTests.swift`:
- Around line 126-131: The test in TranscriptionRecordRowTests.swift uses a
hardcoded English suffix ("s") to assert sub-minute formatting; change it to a
locale-agnostic check: assert formattedDuration is non-nil, does not use
colon-based time (no ":"), and that the numeric portion — parsed with
NumberFormatter to respect the current locale — is approximately equal to the
record.duration (or less than 60s). Locate the assertion around
formattedDuration and record in the test, replace the hasSuffix("s") check with
parsing via NumberFormatter and a numeric comparison to ensure the output
represents seconds in a locale-safe way.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b9a60b2c-f287-47a3-9822-16a5ea9d4e48

📥 Commits

Reviewing files that changed from the base of the PR and between b254122 and 093505b.

📒 Files selected for processing (36)
  • Sources/Managers/MLDaemonManager+Process.swift
  • Sources/Services/MLXModelManager+Downloads.swift
  • Sources/Services/ModelManager.swift
  • Sources/Stores/DataManager.swift
  • Sources/Utilities/AppDefault.swift
  • Sources/Utilities/DiskMutationSerializer.swift
  • Sources/Views/Components/Waveform/ClassicWaveformView.swift
  • Sources/Views/Components/Waveform/ConstellationWaveformView.swift
  • Sources/Views/Components/Waveform/DialWaveformView.swift
  • Sources/Views/Components/Waveform/FrameTimer.swift
  • Sources/Views/Components/Waveform/HaloWaveformView.swift
  • Sources/Views/Components/Waveform/HeartbeatPulseView.swift
  • Sources/Views/Components/Waveform/NeonWaveformView.swift
  • Sources/Views/Components/Waveform/SpectrumWaveformView.swift
  • Sources/Views/Components/Waveform/StreamWaveformView.swift
  • Sources/Views/Components/Waveform/WaveformContainer.swift
  • Tests/AppDelegate/AppDelegateMenuTests.swift
  • Tests/AppDelegate/AppDelegateNotificationsTests.swift
  • Tests/AppDelegate/AppDelegateRecordingWindowTests.swift
  • Tests/AudioEngineRecorderTests.swift
  • Tests/ErrorPresenterExpandedTests.swift
  • Tests/Integration/ErrorPropagationLayerTests.swift
  • Tests/Integration/ErrorRecoveryIntegrationTests.swift
  • Tests/SpeechToTextServiceTests.swift
  • Tests/TranscriptionPipelineTests.swift
  • Tests/Utilities/LoggerTests.swift
  • Tests/Utilities/ResourceLocatorTests.swift
  • Tests/Utilities/WindowTitlesTests.swift
  • Tests/Views/Components/InkRippleViewTests.swift
  • Tests/Views/Dashboard/DashboardTranscriptsViewTests.swift
  • Tests/Views/Dashboard/MLXModelManagementViewTests.swift
  • Tests/Views/Dashboard/UsageDashboardViewTests.swift
  • Tests/Views/Transcription/TranscriptionRecordRowTests.swift
  • Tests/Views/WelcomeViewTests.swift
  • Tests/Waveform/ParticleSystemTests.swift
  • Tests/WindowControllerTests.swift
💤 Files with no reviewable changes (2)
  • Tests/AudioEngineRecorderTests.swift
  • Tests/Utilities/WindowTitlesTests.swift

Comment on lines +87 to +98
let passThroughIfSet = [
"LANG", "LC_ALL", "LC_CTYPE", "TMPDIR",
"AUDIOWHISPER_APP_SUPPORT_DIR",
"VIRTUAL_ENV", "PYTHONPATH",
"UV_CACHE_DIR", "UV_PYTHON",
"HF_HOME", "HF_HUB_CACHE", "HUGGINGFACE_HUB_CACHE", "XDG_CACHE_HOME"
]
for key in passThroughIfSet {
if let value = parent[key], !value.isEmpty {
env[key] = value
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider adding SSL certificate environment variables for corporate network compatibility.

The allowlist doesn't include SSL certificate override variables (SSL_CERT_FILE, SSL_CERT_DIR, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE). In corporate environments with custom CA certificates or MITM proxies, the Python daemon may fail SSL verification when downloading HuggingFace models, since Python's requests/urllib3 check these variables.

This is an edge case—most users rely on system certificates which will work—but corporate users could hit hard-to-diagnose download failures.

🔧 Proposed fix to add SSL certificate pass-through
         let passThroughIfSet = [
             "LANG", "LC_ALL", "LC_CTYPE", "TMPDIR",
             "AUDIOWHISPER_APP_SUPPORT_DIR",
             "VIRTUAL_ENV", "PYTHONPATH",
             "UV_CACHE_DIR", "UV_PYTHON",
-            "HF_HOME", "HF_HUB_CACHE", "HUGGINGFACE_HUB_CACHE", "XDG_CACHE_HOME"
+            "HF_HOME", "HF_HUB_CACHE", "HUGGINGFACE_HUB_CACHE", "XDG_CACHE_HOME",
+            "SSL_CERT_FILE", "SSL_CERT_DIR", "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE"
         ]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Managers/MLDaemonManager`+Process.swift around lines 87 - 98, The
environment pass-through array passThroughIfSet in MLDaemonManager+Process.swift
currently omits SSL certificate overrides; update passThroughIfSet to include
"SSL_CERT_FILE", "SSL_CERT_DIR", "REQUESTS_CA_BUNDLE", and "CURL_CA_BUNDLE" so
the for loop that reads parent[...] and writes env[...] passes these through to
the Python daemon; ensure the new keys are added alongside the existing
variables used by the env population logic (passThroughIfSet, parent, env) so
corporate/custom CA settings are preserved.


// Retention cleanup runs off the save critical path — the caller
// (and the UI) shouldn't wait on a full predicate fetch + delete.
Task { await cleanupExpiredRecordsQuietly() }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider using Task.detached instead of Task for background cleanup.

The PR description mentions a "detached Task," but the code uses unstructured Task { }, which inherits the MainActor context and current task priority. For background cleanup work that shouldn't compete with UI operations, Task.detached { } would be more semantically accurate and would default to a lower priority.

♻️ Suggested change
-            Task { await cleanupExpiredRecordsQuietly() }
+            Task.detached { await self.cleanupExpiredRecordsQuietly() }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Task { await cleanupExpiredRecordsQuietly() }
Task.detached { await self.cleanupExpiredRecordsQuietly() }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Stores/DataManager.swift` at line 169, The call launches an
unstructured Task which inherits the current actor/context; replace Task { await
cleanupExpiredRecordsQuietly() } with a detached background task so the cleanup
doesn't inherit MainActor/foreground priority. Locate the invocation in
DataManager (the cleanupExpiredRecordsQuietly() call) and change it to use
Task.detached { await cleanupExpiredRecordsQuietly() } so the work runs off the
main actor with detached/default priority.

Comment on lines +15 to +38
final class FrameTimer {
/// Per-frame tick stream. Emits only between `start()` and `stop()`.
let publisher = PassthroughSubject<Date, Never>()

private let interval: TimeInterval
private var cancellable: AnyCancellable?

init(interval: TimeInterval) {
self.interval = interval
}

/// Begin emitting ticks. No-op if already running.
func start() {
guard cancellable == nil else { return }
cancellable = Timer.publish(every: interval, on: .main, in: .common)
.autoconnect()
.sink { [publisher] date in publisher.send(date) }
}

/// Stop emitting ticks and cancel the underlying timer.
func stop() {
cancellable?.cancel()
cancellable = nil
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# Find all usages of FrameTimer
rg "FrameTimer" --type swift -B 2 -A 2

Repository: jtn0123/AudioWhisper

Length of output: 4861


🏁 Script executed:

# Check file structure to understand if this is a UI component
fd -e swift Sources/Views/Components/Waveform/ | head -20

Repository: jtn0123/AudioWhisper

Length of output: 506


🏁 Script executed:

# Look for similar timer patterns in the codebase with `@MainActor`
rg "`@MainActor`" --type swift -A 3 -B 1

Repository: jtn0123/AudioWhisper

Length of output: 50377


🏁 Script executed:

# Check closure retention patterns in the codebase
rg "sink.*\[.*\].*in" --type swift | head -20

Repository: jtn0123/AudioWhisper

Length of output: 365


Consider adding @MainActor isolation to FrameTimer for consistency with similar utility classes.

While not strictly required by the guideline (which targets UI components), marking FrameTimer with @MainActor aligns with the pattern used by similar helper classes in the codebase like MicTestCapture and LivePreviewSampler, and makes explicit that the timer's mutable state is main-actor-isolated.

Suggested change
+@MainActor
 final class FrameTimer {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Views/Components/Waveform/FrameTimer.swift` around lines 15 - 38, Add
MainActor isolation to the FrameTimer utility by annotating the FrameTimer type
with `@MainActor` so its mutable state (interval, cancellable, publisher) and
methods start() / stop() are executed on the main actor; update any call sites
if needed to await or hop to the main actor when constructing or invoking
FrameTimer, and ensure references to publisher, cancellable, start(), stop(),
and init(interval:) remain consistent after the annotation.

Comment on lines +24 to 30
/// Posts `.pressAndHoldSettingsChanged` and waits for the observer to run.
private func awaitNotificationProcessed(name: Notification.Name, object: Any?) {
NotificationCenter.default.post(name: name, object: object)
let processed = expectation(description: "Notification processed")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { processed.fulfill() }
wait(for: [processed], timeout: 1.0)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

awaitNotificationProcessed does not synchronize with the posted notification.

Line 26 posts before any expectation is registered, and Lines 27-29 always fulfill via fixed delay. This can pass even when observer wiring/regression exists and adds timing flakiness.

🔧 Proposed fix
 private func awaitNotificationProcessed(name: Notification.Name, object: Any?) {
-    NotificationCenter.default.post(name: name, object: object)
-    let processed = expectation(description: "Notification processed")
-    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { processed.fulfill() }
-    wait(for: [processed], timeout: 1.0)
+    let posted = expectation(forNotification: name, object: nil) { _ in true }
+    NotificationCenter.default.post(name: name, object: object)
+    wait(for: [posted], timeout: 1.0)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/AppDelegate/AppDelegateNotificationsTests.swift` around lines 24 - 30,
awaitNotificationProcessed currently posts the notification then uses a fixed
delay to assume processing, which doesn't actually synchronize with observers;
replace the fixed-delay approach by creating an XCTest expectation bound to the
notification (e.g. using expectation(forNotification: name, object: object,
handler: nil) or
NotificationCenter.addObserver(forName:object:queue:using:)-backed expectation),
then post the notification and wait(for: [expectation], timeout:), removing the
DispatchQueue.main.asyncAfter-based fulfill so the test truly waits for the
observer invoked by awaitNotificationProcessed.

Comment on lines +35 to 49
func testURLForResourceWithDevRelativePathResolvesExistingFile() {
// The dev fallback resolves a file that exists relative to the cwd.
// Sources/parakeet_transcribe_pcm.py ships with the package.
let url = ResourceLocator.url(
forResource: "parakeet_transcribe_pcm",
withExtension: "py",
devRelativePath: "Sources/parakeet_transcribe_pcm.py"
)
if let url = url {
XCTAssertEqual(url.lastPathComponent, "parakeet_transcribe_pcm.py")
XCTAssertTrue(FileManager.default.fileExists(atPath: url.path))
}
// When run outside the package root the dev path is absent and the
// result is nil; either way no other resolution mode applies here.
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the dev-path resolution test deterministic.

On Line 43, this test silently passes when url == nil, which can hide regressions even when the file exists in the current run environment. Assert success when the fixture file exists, and only allow nil when it truly does not exist.

Suggested test tightening
     func testURLForResourceWithDevRelativePathResolvesExistingFile() {
-        // The dev fallback resolves a file that exists relative to the cwd.
-        // Sources/parakeet_transcribe_pcm.py ships with the package.
+        // The dev fallback should resolve when the fixture exists in cwd.
         let url = ResourceLocator.url(
             forResource: "parakeet_transcribe_pcm",
             withExtension: "py",
             devRelativePath: "Sources/parakeet_transcribe_pcm.py"
         )
-        if let url = url {
-            XCTAssertEqual(url.lastPathComponent, "parakeet_transcribe_pcm.py")
-            XCTAssertTrue(FileManager.default.fileExists(atPath: url.path))
-        }
-        // When run outside the package root the dev path is absent and the
-        // result is nil; either way no other resolution mode applies here.
+        let expected = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
+            .appendingPathComponent("Sources/parakeet_transcribe_pcm.py")
+
+        if FileManager.default.fileExists(atPath: expected.path) {
+            XCTAssertNotNil(url)
+            XCTAssertEqual(url?.lastPathComponent, "parakeet_transcribe_pcm.py")
+            XCTAssertEqual(url?.standardizedFileURL, expected.standardizedFileURL)
+        } else {
+            XCTAssertNil(url)
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func testURLForResourceWithDevRelativePathResolvesExistingFile() {
// The dev fallback resolves a file that exists relative to the cwd.
// Sources/parakeet_transcribe_pcm.py ships with the package.
let url = ResourceLocator.url(
forResource: "parakeet_transcribe_pcm",
withExtension: "py",
devRelativePath: "Sources/parakeet_transcribe_pcm.py"
)
if let url = url {
XCTAssertEqual(url.lastPathComponent, "parakeet_transcribe_pcm.py")
XCTAssertTrue(FileManager.default.fileExists(atPath: url.path))
}
// When run outside the package root the dev path is absent and the
// result is nil; either way no other resolution mode applies here.
}
func testURLForResourceWithDevRelativePathResolvesExistingFile() {
// The dev fallback should resolve when the fixture exists in cwd.
let url = ResourceLocator.url(
forResource: "parakeet_transcribe_pcm",
withExtension: "py",
devRelativePath: "Sources/parakeet_transcribe_pcm.py"
)
let expected = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
.appendingPathComponent("Sources/parakeet_transcribe_pcm.py")
if FileManager.default.fileExists(atPath: expected.path) {
XCTAssertNotNil(url)
XCTAssertEqual(url?.lastPathComponent, "parakeet_transcribe_pcm.py")
XCTAssertEqual(url?.standardizedFileURL, expected.standardizedFileURL)
} else {
XCTAssertNil(url)
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/Utilities/ResourceLocatorTests.swift` around lines 35 - 49, The test
testURLForResourceWithDevRelativePathResolvesExistingFile is nondeterministic
because it silently allows url == nil; update it to assert deterministically by
checking the dev fixture file existence directly: call
ResourceLocator.url(forResource:withExtension:devRelativePath:) as before, then
compute the expectedDevPath ("Sources/parakeet_transcribe_pcm.py") and use
FileManager.default.fileExists(atPath:) to decide assertions—if the file exists
assert url is non-nil, lastPathComponent equals "parakeet_transcribe_pcm.py",
and fileExists(atPath: url.path) is true; otherwise assert url is nil. This
ensures failures occur when the fixture is present but resolution returns nil.

Comment on lines +126 to +131
// A non-nil 0.0 duration is under a minute, so it is formatted as a
// seconds string (locale-formatted number followed by "s").
let formatted = record.formattedDuration
XCTAssertNotNil(formatted)
XCTAssertTrue(formatted?.hasSuffix("s") ?? false,
"Sub-minute duration should be formatted in seconds, got \(formatted ?? "nil")")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid locale-coupled suffix assertions in duration formatting tests.

Line 130 assumes English output ("s" suffix). This can fail on non-English locales even when formatting is correct. Prefer a locale-agnostic behavioral assertion.

Example locale-agnostic adjustment
         let formatted = record.formattedDuration
         XCTAssertNotNil(formatted)
-        XCTAssertTrue(formatted?.hasSuffix("s") ?? false,
-                      "Sub-minute duration should be formatted in seconds, got \(formatted ?? "nil")")
+        let oneMinuteRecord = TranscriptionRecord(
+            text: "Test",
+            provider: .parakeet,
+            duration: 60.0
+        )
+        XCTAssertNotEqual(formatted, oneMinuteRecord.formattedDuration,
+                          "Sub-minute and minute durations should format differently")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/Views/Transcription/TranscriptionRecordRowTests.swift` around lines 126
- 131, The test in TranscriptionRecordRowTests.swift uses a hardcoded English
suffix ("s") to assert sub-minute formatting; change it to a locale-agnostic
check: assert formattedDuration is non-nil, does not use colon-based time (no
":"), and that the numeric portion — parsed with NumberFormatter to respect the
current locale — is approximately equal to the record.duration (or less than
60s). Locate the assertion around formattedDuration and record in the test,
replace the hasSuffix("s") check with parsing via NumberFormatter and a numeric
comparison to ensure the output represents seconds in a locale-safe way.

@jtn0123 jtn0123 merged commit d24a40b into master May 17, 2026
8 of 9 checks passed
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