Code audit: release signing, notarization, and test coverage#8
Code audit: release signing, notarization, and test coverage#8nathanialhenniges merged 20 commits intomainfrom
Conversation
- Update CLAUDE.md with Discord service, CI/CD, docs site, and current stack - Update README description and dark/light mode logo - Add Discord Rich Presence to homepage feature grid - Fix Discord support link to mrdwolf.net - Split docs sidebar into Guide and Developers sections
Implement a local WebSocket server (Network.framework NWListener) that broadcasts now-playing JSON to connected clients, and a React widget page on the docs site for use as an OBS browser source overlay. Swift: - WebSocketServerService with multi-client support, progress timer, and auto-retry on failure - Functional WebSocket settings view replacing WIP placeholder - AppDelegate wiring for music playback and artwork URL forwarding - Network server entitlement for App Sandbox - Missing AppConstants (Update, URLs, notifications) that blocked build - MusicPlaybackMonitor delegate updated to pass duration/elapsed Docs: - /widget page with WebSocket client, album art blur background, progress bar with requestAnimationFrame interpolation, auto-reconnect - Supports port, duration (auto-hide), and hideAlbumArt URL params Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rename sidebar label from "WebSocket" to "OBS Widget" in settings - Fix App Visibility card to be full width in detail pane - Add Discord and OBS Widget steps to onboarding wizard (4 steps) - Update welcome step with Discord/OBS feature highlights - Update README, features, usage, installation, development, and architecture docs to reflect OBS Widget naming and new onboarding - Fix pre-existing TwitchViewModelTests referencing missing ChannelValidationState enum - Fix double notification in WolfWaveApp WebSocket state callback - Update onboarding tests for 4-step navigation (142 tests pass) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Streamline doc comments across all files to be concise and professional, removing verbose multi-paragraph descriptions and redundant inline comments while keeping MARK headers. Add a WolfWave icon placeholder in the stream overlay widget when album artwork hasn't loaded yet. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap setReauthNeeded call in MainActor.run to eliminate the 'no async operations occur within await' compiler warning. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolve all outstanding compliance items: code signing, notarization, and credential security now pass. Add WW-17 WebSocket entitlements and Network.framework dependency. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove DispatchQueue.main.asyncAfter delay before opening settings on token expiration — the delayed dispatch could race with notification tap activation, causing duplicate window creation and a crash. Replace the disabled "Connect" button with an orange "Re-auth" button when reauthNeeded is true, giving users a clear action to re-authorize their expired Twitch session. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move Music Monitor and App Visibility into Advanced, rename Discord to Discord Integration, and fix locale-aware port number formatting (8,765 → 8765) by using Text(verbatim:). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Return false from applicationShouldHandleReopen to prevent AppKit's default window restoration from conflicting with manual openSettings(). Clear settingsWindow reference on close so restoreMenuOnlyIfNeeded correctly detects no visible windows remain. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bug fixes: - Fix force unwrap crash in DiscordRPCService socket handling - Fix unlimited reconnect loop in TwitchChatService network monitor - Add retry mechanism with exponential backoff for Twitch chat messages - Fix notification observer leak in TwitchViewModel - Replace debug print() calls with Log utility Features: - Add bot command cooldowns with global/per-user limits and mod bypass - Add in-app update banner in settings - Add log file writing and export button in Advanced settings - Add Apple Music permission detection and guidance - Add Copy Widget URL menu item to status bar - Add widget customization (theme, layout, colors, font) with WebSocket broadcast - Add Test Connection buttons for Twitch (token validation) and Discord (IPC check) Code quality: - Extract hardcoded values to AppConstants (Twitch, Widget, WebSocket) - Unify duplicate artwork fetching into shared ArtworkService singleton - Add Color(hex:) extension for hex color parsing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Expand the widget font picker from 4 built-in options to 16 fonts (4 built-in + 12 Google Fonts) with sectioned picker UI. All Google Fonts are preloaded in a single request on widget mount for instant switching. Also adds an "Open in browser" button for the widget URL, hides the widget when WebSocket disconnects, and fixes actor isolation warnings in Logger. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolve build errors from dev merge: - Remove duplicate URLs and Update enums in AppConstants - Add missing NSLock to BotCommandDispatcher - Fix duplicate onReauth and missing channelValidationState in TwitchSettingsView - Add missing state properties to AdvancedSettingsView (update checker) - Add missing now-playing state and helpers to MusicMonitorSettingsView - Fix hasClientID computed property assignment in DiscordSettingsView Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add missing network endpoints (GitHub API, Google Fonts CDN), document browser-side dependencies, and add comprehensive Section 8 covering Apple notarization requirements for self-signed DMG distribution. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… for untested core classes The release.yml was missing certificate import, code signing, notarization, and stapling steps — DMG releases would be unsigned and blocked by Gatekeeper. Added the full signing pipeline (cert import, app codesign, DMG sign, notarize, staple, keychain cleanup). COMPLIANCE.md claims are now accurate. Added 48 new tests across 6 test files covering CooldownManager, KeychainService, ArtworkService, PowerStateMonitor, MusicPlaybackMonitor, and TwitchDeviceAuth. Total test count: 190, all passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review infoConfiguration used: Organization UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (24)
WalkthroughThis pull request adds code signing and notarization to the release workflow, expands test coverage from 124 to 190 tests, updates compliance and feature documentation, improves thread-safety and cache handling in core services, refines UI state logic, and removes a legacy SwiftUI wrapper for Twitch authentication. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 20
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/wolfwave/Views/Twitch/TwitchDeviceAuthWindowController.swift (1)
84-106:⚠️ Potential issue | 🟡 MinorRemove unused
TwitchDeviceAuthWindowstruct or convert it to a proper View.The struct uses
@Stateon a non-View type, which violates SwiftUI design principles. The@Stateproperty wrapper only functions within View types and is designed to integrate with SwiftUI's view lifecycle; using it here will not work as intended. Additionally, this struct is never instantiated anywhere in the codebase, making it dead code. Either remove it entirely or convert it to aViewif it's meant to be used in SwiftUI contexts.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/Twitch/TwitchDeviceAuthWindowController.swift` around lines 84 - 106, The TwitchDeviceAuthWindow struct incorrectly uses `@State` (invalid outside a SwiftUI View) and appears unused; either delete the struct entirely to remove dead code or convert it into a proper SwiftUI View (rename to TwitchDeviceAuthWindowView) that defines retainedController as `@StateObject` or `@State` (depending on lifecycle) and exposes show()/dismiss() via bindings or view model; if converting, move the TwitchDeviceAuthWindowController construction into an ObservableObject/ViewModel (e.g., TwitchDeviceAuthWindowViewModel) and reference its methods from the View, otherwise remove TwitchDeviceAuthWindow, its retainedController property and show()/dismiss() functions (and any references to TwitchDeviceAuthWindowController from here) to eliminate the misuse of `@State`.README.md (1)
97-97:⚠️ Potential issue | 🟡 MinorUpdate the
make testcount to match current suite size.Line 97 still says
124 tests, but this PR’s test plan states 190. Keeping this synced avoids stale release docs.🛠️ Suggested doc fix
-| `make test` | Run unit tests (124 tests) | +| `make test` | Run unit tests (190 tests) |🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@README.md` at line 97, Update the README table entry for the `make test` row to reflect the current test suite size: replace the text "124 tests" with "190 tests" in the table cell that contains the `make test` description so the documentation matches the PR test plan.
🧹 Nitpick comments (14)
CHANGELOG.md (1)
25-25: Document the release signing/notarization pipeline in Unreleased notes.The PR’s main release-hardening work (Developer ID signing, notarization, stapling, keychain cleanup) is not reflected here. Add a concise bullet so changelog matches shipped CI/release behavior.
✏️ Proposed patch
### Changed - **Channel Validation**: Moved Twitch channel name validation from keystroke-triggered to connect-button-triggered. No more API calls while typing — validation happens when you click Connect. @@ - **Documentation**: Updated all docs for current features, added Legal section with Privacy Policy and Terms of Service. +- **Release Pipeline Security**: Updated GitHub Actions release workflow to import Developer ID certs, code sign app/DMG, notarize with `notarytool --wait`, staple notarization tickets, and clean up keychain artifacts.Also applies to: 46-47
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CHANGELOG.md` at line 25, The Unreleased section of CHANGELOG.md is missing a bullet documenting the release signing/notarization pipeline; add a concise bullet under the "Unreleased" header (near the existing "**CI Pipeline**" entry) that describes Developer ID signing, notarization, stapling and keychain cleanup being performed in the CI/release process so the changelog matches the shipped release behavior..github/workflows/release.yml (1)
179-192: Consider adding a timeout to notarization.The
notarytool submit --waitcommand blocks until notarization completes, but Apple's notarization service can occasionally experience delays. Adding--timeoutprevents indefinite hangs.💡 Suggested improvement
xcrun notarytool submit "builds/$DMG_NAME" \ --apple-id "$APPLE_ID" \ --team-id "$APPLE_TEAM_ID" \ --password "$APPLE_APP_PASSWORD" \ - --wait + --wait \ + --timeout 30m echo "✅ Notarization complete"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/release.yml around lines 179 - 192, Update the "Notarize DMG" step to pass a timeout to the blocking command: add a NOTARIZE_TIMEOUT environment variable (e.g., default to 3600) to the step and include --timeout "$NOTARIZE_TIMEOUT" on the xcrun notarytool submit command (keep the existing --wait). Reference the step name "Notarize DMG", the env vars DMG_NAME/APPLE_ID/APPLE_TEAM_ID/APPLE_APP_PASSWORD, and the xcrun notarytool submit invocation when making the change so the submit call cannot hang indefinitely.src/wolfwave/Views/Advanced/AdvancedSettingsView.swift (1)
173-176: PrefercardStyle()for the Diagnostics section to keep styling centralized.This avoids duplicating padding/background/corner-radius logic and keeps settings cards visually consistent.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/Advanced/AdvancedSettingsView.swift` around lines 173 - 176, Replace the explicit card styling on the Diagnostics settings block in AdvancedSettingsView (the view using .padding(AppConstants.SettingsUI.cardPadding), .background(Color(nsColor: .controlBackgroundColor)), .clipShape(RoundedRectangle(cornerRadius: AppConstants.SettingsUI.cardCornerRadius))) with the centralized .cardStyle() modifier; locate the Diagnostics section inside the AdvancedSettingsView struct and remove those three modifiers and apply .cardStyle() so the cardPadding, controlBackgroundColor and cardCornerRadius are managed by the single reusable cardStyle() modifier.src/wolfwave/Services/Twitch/Commands/CooldownManager.swift (1)
25-27: Consider adding eviction for stale per-user cooldown entries.
userCooldownscurrently only shrinks onreset(). Long-running channels with many unique users can accumulate stale keys indefinitely.Also applies to: 85-88
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Services/Twitch/Commands/CooldownManager.swift` around lines 25 - 27, userCooldowns currently never evicts entries which can grow unbounded; add a TTL-based eviction so stale per-user entries are removed: implement a private pruneStaleUserCooldowns(ttl: TimeInterval) that iterates userCooldowns and removes keys whose Date is older than now - ttl, call this prune from recordUse(for:trigger:) and isOnCooldown(for:trigger:) (or run it periodically via a DispatchSourceTimer if preferred), and ensure reset() still clears the map; reference the userCooldowns property and reset() when making the change.docs/app/widget/themes.ts (1)
246-253: Consider usingdefaultWidgetConfigconstants instead of hardcoded values.The comparison values
"#FFFFFF"and"#1A1A2E"duplicate the defaults defined indefaultWidgetConfig. If the defaults change, this logic could become inconsistent.♻️ Suggested improvement
// For Default theme, allow custom text/background color overrides if (config.theme === "Default") { - if (config.textColor && config.textColor !== "#FFFFFF") { + if (config.textColor && config.textColor !== defaultWidgetConfig.textColor) { resolved.textPrimary = config.textColor; resolved.textSecondary = config.textColor; resolved.progressFillBg = config.textColor; } - if (config.backgroundColor && config.backgroundColor !== "#1A1A2E") { + if (config.backgroundColor && config.backgroundColor !== defaultWidgetConfig.backgroundColor) { resolved.overlayBg = config.backgroundColor; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/app/widget/themes.ts` around lines 246 - 253, The conditionals in themes.ts compare config.textColor and config.backgroundColor to hardcoded "#FFFFFF" and "#1A1A2E"; change those comparisons to reference the defaults from defaultWidgetConfig (e.g., defaultWidgetConfig.textColor and defaultWidgetConfig.backgroundColor) so the checks remain correct if defaults change, and keep the rest of the logic assigning to resolved.textPrimary, resolved.textSecondary, resolved.progressFillBg and resolved.overlayBg unchanged.src/WolfWaveTests/ArtworkServiceTests.swift (1)
47-63: Consider marking network-dependent tests or using mocks.These tests call
fetchArtworkURLwhich performs actual network requests. Per the project's testing guidelines, tests should "avoid tests requiring AppDelegate, Keychain, or network." While these tests only verify that the completion handler is called, they may be flaky in CI environments without network access.Consider:
- Marking these as integration tests (separate test plan/scheme)
- Mocking the network layer to test completion handler behavior deterministically
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/WolfWaveTests/ArtworkServiceTests.swift` around lines 47 - 63, Tests testFetchWithEmptyTrackCallsCompletion and testFetchArtworkURLCallsCompletion call service.fetchArtworkURL which performs real network calls; to avoid flaky CI runs either (a) move these tests into an integration test target or mark them with a custom flag/annotation so they run only in integration schemes, or (b) inject/make the network client mockable and replace the real network layer in these tests with a stubbed/mock implementation that invokes the completion immediately (verify completion is called deterministically). Update the ArtworkService (or its initializer) to accept a NetworkClient protocol dependency and change these two tests to use a mock NetworkClient that calls the completion without performing real HTTP requests.src/WolfWaveTests/PowerStateMonitorTests.swift (1)
33-37: Tautological assertion provides no meaningful validation.The assertion
XCTAssertTrue(value == true || value == false)is always true for anyBoolvalue in Swift. Consider either removing this test or replacing it with a more meaningful assertion.♻️ Suggested alternatives
Option 1: Remove the test as
testIsReducedModePropertyAccessiblealready validates readability.Option 2: If you want to document the expected type, use a compile-time check:
func testIsReducedModeReturnsBool() { let value = PowerStateMonitor.shared.isReducedMode - // Value depends on system state but should be a valid boolean - XCTAssertTrue(value == true || value == false) + // Compile-time type check - Bool is the expected type + let _: Bool = value + // Test passes if property is accessible and returns Bool }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/WolfWaveTests/PowerStateMonitorTests.swift` around lines 33 - 37, The test testIsReducedModeReturnsBool is tautological because XCTAssertTrue(value == true || value == false) always holds for any Bool; either remove this test entirely or replace it with a meaningful check: e.g., rename to testIsReducedModePropertyAccessible and simply access PowerStateMonitor.shared.isReducedMode (or add a compile-time type assertion such as binding it to a Bool) to document readability without a runtime tautology; update or delete the test method accordingly and keep references to PowerStateMonitor.shared.isReducedMode and testIsReducedModeReturnsBool when making the change.src/wolfwave/Views/Onboarding/OnboardingOBSWidgetStepView.swift (1)
113-115: Prefer async/await overDispatchQueue.main.asyncAfterfor this delayed state reset.Use a cancellable
Task+Task.sleepfor the “Copied” timeout to align with the project’s Swift concurrency guideline.As per coding guidelines: "Use async/await concurrency instead of DispatchQueue for new async work in Swift."♻️ Proposed refactor
`@State` private var copiedURL = false +@State private var copyResetTask: Task<Void, Never>? @@ Button { NSPasteboard.general.clearContents() NSPasteboard.general.setString(widgetURL, forType: .string) copiedURL = true - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - copiedURL = false - } + copyResetTask?.cancel() + copyResetTask = Task { + try? await Task.sleep(for: .seconds(2)) + guard !Task.isCancelled else { return } + copiedURL = false + } } label: { @@ } + .onDisappear { + copyResetTask?.cancel() + } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/Onboarding/OnboardingOBSWidgetStepView.swift` around lines 113 - 115, Replace the DispatchQueue.main.asyncAfter usage in OnboardingOBSWidgetStepView that resets copiedURL with a cancellable Swift concurrency Task: add a state property (e.g., copyResetTask: Task<Void, Never>?) to the view, cancel any existing copyResetTask before creating a new Task that awaits Task.sleep(nanoseconds: 2_000_000_000) and then sets copiedURL = false on the main actor, assign the Task to copyResetTask so it can be cancelled if the user copies again or the view disappears, and cancel copyResetTask in the view's onDisappear to avoid stray work.src/wolfwave/Views/WebSocket/WebSocketSettingsView.swift (2)
471-490: StatusChip is duplicated across multiple files.This
StatusChipview is also defined inTwitchSettingsView.swift. Consider extracting to a shared component.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/WebSocket/WebSocketSettingsView.swift` around lines 471 - 490, The StatusChip view is duplicated; extract the StatusChip struct into a single reusable component (e.g., create a new StatusChip SwiftUI View file) and replace the duplicate definitions in WebSocketSettingsView (StatusChip) and TwitchSettingsView with references to the shared component; ensure the new StatusChip preserves the initializer signature (text: String, color: Color), accessibility (internal/public as needed), and styling so existing usage compiles, then delete the duplicated structs from both files.
327-356: Consider validating hex color input format.The text fields accept arbitrary strings for
widgetTextColorandwidgetBackgroundColor. Invalid hex values could cause issues in the widget rendering.💡 Suggested validation
Add input validation or a color picker to ensure only valid hex color codes are accepted:
private func isValidHexColor(_ hex: String) -> Bool { let pattern = "^#[0-9A-Fa-f]{6}$" return hex.range(of: pattern, options: .regularExpression) != nil }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/WebSocket/WebSocketSettingsView.swift` around lines 327 - 356, The TextField inputs for widgetTextColor and widgetBackgroundColor accept arbitrary strings which can produce invalid colors when used by Color(hex:); add a validation helper (e.g., isValidHexColor(_:) using the ^#[0-9A-Fa-f]{6}$ regex) and use it in the TextField handlers: validate onChange and onSubmit before calling broadcastWidgetConfig(), and if invalid either prevent the broadcast and show a simple inline error state (or revert to a safe default) and ensure Color(hex:) is always provided a validated value or a fallback (so references to widgetTextColor, widgetBackgroundColor, broadcastWidgetConfig(), and Color(hex:) are updated accordingly).src/wolfwave/Services/WebSocket/WebSocketServerService.swift (1)
71-81: setEnabled uses manual lock/unlock instead of withLock.For consistency with other services and to avoid potential unlock-before-use issues, consider using
withLock.♻️ Suggested refactor
func setEnabled(_ enabled: Bool) { - enabledLock.lock() - isEnabled = enabled - enabledLock.unlock() + enabledLock.withLock { isEnabled = enabled } if enabled { serverQueue.async { [weak self] in self?.startServer() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Services/WebSocket/WebSocketServerService.swift` around lines 71 - 81, Replace the manual enabledLock.lock()/unlock() in setEnabled(_:) with enabledLock.withLock { isEnabled = enabled } to match other services and avoid unlock-before-use issues; keep the existing serverQueue.async calls that dispatch startServer()/stopServer() (using [weak self]) unchanged so only the assignment is protected by enabledLock.withLock.src/wolfwave/Views/SettingsView.swift (1)
440-442: Reset settings array could benefit from extraction.The array of keys to reset is getting long. Consider extracting to a constant for maintainability.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/SettingsView.swift` around lines 440 - 442, Extract the long inline array of UserDefaults keys into a named constant and use that constant in the forEach call: create a descriptive constant (e.g., resettableUserDefaultKeys or AppConstants.UserDefaults.resetKeys) containing [AppConstants.UserDefaults.trackingEnabled, AppConstants.UserDefaults.currentSongCommandEnabled, AppConstants.UserDefaults.lastSongCommandEnabled, AppConstants.UserDefaults.dockVisibility, AppConstants.UserDefaults.websocketEnabled, AppConstants.UserDefaults.websocketURI, AppConstants.UserDefaults.websocketServerPort, AppConstants.UserDefaults.hasCompletedOnboarding, AppConstants.UserDefaults.discordPresenceEnabled] and replace the inline array passed to UserDefaults.standard.removeObject(forKey:) with that constant to improve readability and maintainability in SettingsView.swift.src/wolfwave/Services/Twitch/Commands/BotCommandDispatcher.swift (1)
120-142: Consider decoupling trigger-to-key mapping from hardcoded strings.The switch statement duplicates trigger strings that are already defined in
SongCommandandLastSongCommand. If triggers change, this mapping could become stale.💡 Suggested approach
Consider adding a
commandTypeproperty to theBotCommandprotocol or using the command instance directly to determine UserDefaults keys, rather than matching on trigger strings.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Services/Twitch/Commands/BotCommandDispatcher.swift` around lines 120 - 142, The cooldownValues(for:command:) function currently matches on hardcoded trigger strings; instead use the command's type or a new commandType property on the BotCommand protocol to pick the appropriate UserDefaults keys so mappings can't drift when triggers change. Update the BotCommand protocol to expose either a commandType enum or provide methods to return the relevant UserDefaults keys, then change cooldownValues(for:command:) to switch on command.commandType (or call those methods) and read AppConstants.UserDefaults.songCommandGlobalCooldown / songCommandUserCooldown or lastSongCommandGlobalCooldown / lastSongCommandUserCooldown accordingly; keep the default fallback to command.globalCooldown and command.userCooldown.src/wolfwave/WolfWaveApp.swift (1)
440-457: Consider extracting the magic number5.0to a constant.The normal music check interval (
5.0) on line 445 is hardcoded while other intervals useAppConstants. For consistency, consider adding a constant likeAppConstants.Music.defaultCheckInterval.Suggested improvement
In
AppConstants.swift, add to theMusicenum:/// Default polling interval for music playback state (seconds) static let defaultCheckInterval: TimeInterval = 5.0Then update line 445:
musicMonitor?.updateCheckInterval( - reduced ? AppConstants.PowerManagement.reducedMusicCheckInterval : 5.0 + reduced ? AppConstants.PowerManagement.reducedMusicCheckInterval : AppConstants.Music.defaultCheckInterval )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/WolfWaveApp.swift` around lines 440 - 457, Extract the hardcoded 5.0 in powerStateChanged into a named AppConstants value: add a TimeInterval constant like AppConstants.Music.defaultCheckInterval (e.g., 5.0) and then change the call to musicMonitor?.updateCheckInterval(...) in the powerStateChanged(_:) method to use that constant when not in reduced mode; reference the PowerStateMonitor.shared.isReducedMode check and the musicMonitor?.updateCheckInterval call to locate where to replace the magic number.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CHANGELOG.md`:
- Line 24: Update the "Unit Test Suite" changelog line to match the PR
objective: replace the incorrect "124+ unit tests across 8 test files" with the
corrected counts reflecting the addition of 48 tests across 6 files (total 190
unit tests across 14 test files); edit the line that begins with "**Unit Test
Suite**" accordingly so the numbers are consistent with the PR.
In `@COMPLIANCE.md`:
- Line 42: Add blank lines before and after each markdown table to satisfy
markdownlint MD058: locate the table rows beginning with the header string "|
Config | Current | Required |" (and the other identical table headers repeated
later) and ensure there is an empty line immediately above the header and an
empty line immediately below the table block so each table is separated by blank
lines from surrounding paragraphs or headings.
In `@docs/app/widget/page.tsx`:
- Around line 97-154: The catch block in handleMessage currently swallows all
errors (including JSON.parse failures); update it to log the caught error when
running in development so parsing/handling issues are visible—inside the catch
of handleMessage, capture the exception and call console.error or a dev-only
logger with a helpful message including the error and the raw event.data
(guarded by NODE_ENV !== "production" or a similar env/dev check); keep silent
behavior in production to avoid noisy logs.
In `@docs/content/docs/development.mdx`:
- Around line 153-154: Update the test-count sentence in
docs/content/docs/development.mdx that currently reads "make test runs 124
tests" so it reflects the new total of 190 tests; locate the phrase "make test
runs 124 tests" (the overall test-count line near the top of the test summary)
and change the numeric value to 190 so the documentation matches the table
additions (e.g., AppConstantsTests and WebSocketServerServiceTests).
In `@docs/content/docs/features.mdx`:
- Around line 84-88: The docs hardcode the WebSocket endpoint
"ws://localhost:8765" which conflicts with the generated widget URL and
configurable port; update the sentence in docs/content/docs/features.mdx to
describe the endpoint as dynamic (derived from the OBS Widget Settings or the
copied widget URL) rather than a fixed port — e.g., say the widget connects to
the WebSocket address shown in OBS Widget settings or the copied widget URL so
users use their configured port.
- Around line 101-104: Update the onboarding heading sentence (currently
referencing a "3‑step" process) to reflect the new four items in the list by
changing the wording to "4-step" (or otherwise saying "four-step") so the
heading matches the list; locate the heading text in
docs/content/docs/features.mdx near the onboarding section (just above the list
that now contains Twitch Connection, Discord Rich Presence, and OBS Stream
Widget) and replace "3-step" with "4-step" (or "four-step") to keep copy
consistent.
In `@docs/content/docs/installation.mdx`:
- Around line 69-79: Update the onboarding summary copy that currently reads
"3-step guided setup" to reflect the added OBS Widget step by changing the
phrase to "4-step guided setup" (or remove the numeric count and use "guided
setup" to avoid future drift); locate the string near the onboarding description
that references the guided setup and the "Enable OBS Stream Widget (Optional)"
section and replace the old phrasing accordingly.
In `@src/wolfwave/Core/Logger.swift`:
- Around line 159-165: exportLogFile() reads/initializes shared
logFileURL/_logFileURL without holding fileLock, causing a race with writers;
fix by acquiring fileLock before accessing or initializing
logFileURL/_logFileURL and keep the lock while you read/use fileHandle
(synchronizeFile) and determine the return URL, then unlock and return. In
short: in exportLogFile(), call fileLock.lock() before touching logFileURL or
_logFileURL, perform any lazy initialization and fileHandle?.synchronizeFile()
while locked, then check FileManager.default.fileExists(atPath:) on the computed
URL, unlock, and return the result.
In `@src/wolfwave/Monitors/MusicPlaybackMonitor.swift`:
- Around line 99-105: The updateCheckInterval(_:) method currently accepts any
TimeInterval and immediately reschedules the fallback timer; validate the
incoming interval first (in updateCheckInterval) and ignore or clamp
non-positive values to a safe minimum before assigning to currentCheckInterval,
cancelling the existing timer, and calling setupFallbackTimer(); reference the
updateCheckInterval function, currentCheckInterval property, isTracking guard,
timer variable, and setupFallbackTimer() when making this change.
- Around line 154-155: The deduplication key currently includes elapsed time
(built as combined = name + Constants.trackSeparator + artist + album +
String(duration) + String(elapsed>) which causes repeated "Now Playing" logs;
change the payload passed to handleTrackInfo so the dedup key excludes elapsed
(e.g., build a dedupKey using name, artist, album and duration or a persistent
track identifier) and pass the full payload if needed separately—update the
occurrences where combined is constructed and passed to handleTrackInfo
(including the other similar sites) to use the new dedupKey for dedup logic
while preserving elapsed in the payload if the handler needs it.
In `@src/wolfwave/Services/ArtworkService.swift`:
- Around line 32-36: The cache write is currently dispatched asynchronously on
cacheQueue so completion(highRes) can return before the cache is updated;
replace the DispatchQueue-based mutation with an NSLock (e.g., add a private let
cacheLock = NSLock() in ArtworkService) and perform the mutation while holding
the lock, ensuring you set the cache entry (the same code currently run inside
cacheQueue) before calling completion(highRes); update both the cache set site
around cacheQueue and the other occurrence (the block at the other cache set) to
use cacheLock.lock()/unlock() (or lock() + defer { unlock() }) so writes are
synchronous relative to the completion callback and thread-safe per the service
guideline.
In `@src/wolfwave/Services/Twitch/Commands/BotCommandDispatcher.swift`:
- Around line 77-107: The commands array is being iterated without
synchronization while register() mutates it under a lock, risking a data race;
protect all accesses by introducing/using an NSLock (e.g., commandsLock) in
BotCommandDispatcher and wrap the iteration in commandsLock.lock() and
commandsLock.unlock() (use defer { commandsLock.unlock() } immediately after
locking) so the for command in commands loop (and any reads of commands) hold
the same lock used by register() to mutate commands; ensure you reference and
reuse the existing lock symbol (or add one) so register() and the
message-processing path share the same NSLock.
In `@src/wolfwave/Services/Twitch/Commands/SongCommand.swift`:
- Around line 31-33: The truncation logic uses result.prefix(maxLen -
suffix.count) without guarding against negative lengths; compute let available =
maxLen - suffix.count and branch: if available > 0 return
String(result.prefix(available)) + suffix, otherwise return
String(suffix.prefix(maxLen)) (this protects the prefix call and handles cases
where maxLen is smaller than the suffix), adjusting the existing result/suffix
usage where prefix is currently called.
In `@src/wolfwave/Views/Advanced/AdvancedSettingsView.swift`:
- Around line 84-86: The Log.info/Log.error calls in AdvancedSettingsView
currently emit destination.path which leaks absolute user file paths; update the
export routine (e.g., the method handling export in AdvancedSettingsView) to
avoid logging full paths and instead log non-sensitive metadata such as
destination.lastPathComponent (filename), file size, or a success flag. Replace
references to destination.path in Log.info and any error messages with sanitized
data (e.g., destination.lastPathComponent or a masked path) and keep the error
text descriptive without including the absolute path.
In `@src/wolfwave/Views/Discord/DiscordSettingsView.swift`:
- Around line 29-33: The scheduled clear of testResultMessage can let earlier
delayed tasks overwrite newer results; introduce and use a cancellable handle
(e.g., a DispatchWorkItem or Swift concurrency Task stored as a property like
private var pendingClearTask) and, before scheduling a new clear in the test
flow (the places around isTesting and testResultMessage and the other occurrence
around lines 194-197), cancel any existing pendingClearTask and replace it with
the new one so only the latest scheduled clear runs.
In `@src/wolfwave/Views/MusicMonitor/MusicMonitorSettingsView.swift`:
- Around line 120-122: The permission check in checkMusicPermission() should not
call AEDeterminePermissionToAutomateTarget with askUserIfNeeded = true during
onAppear (avoid surprising prompts); change the automatic check to call
AEDeterminePermissionToAutomateTarget(..., askUserIfNeeded: false) and only call
it with askUserIfNeeded = true from an explicit user action (e.g., a "Request
Access" button handler). Also treat any non-noErr OSStatus result as
automation-unavailable (not just one error value): consider noErr (granted) vs
any other OSStatus (denied/unavailable) and handle errAEPrivilegeNotGranted /
errAEInteractionNotAllowed-style failures as unavailable, updating
checkMusicPermission() and the UI logic that reads its result accordingly.
In `@src/wolfwave/Views/Shared/UpdateBannerView.swift`:
- Around line 93-109: The handler in UpdateBannerView currently only updates
state when userInfo["isUpdateAvailable"] is true, so the banner isn't cleared
when updates become unavailable; change the logic to first read
userInfo["isUpdateAvailable"] as a Bool (e.g., let available =
userInfo["isUpdateAvailable"] as? Bool) and then branch: if available == true
set latestVersion, releaseURL (from "latestVersion" and "releaseURL") and set
isUpdateAvailable = true, isDismissed = false; else (available == false)
clear/reset state by setting isUpdateAvailable = false, isDismissed = false (and
optionaly clear latestVersion and releaseURL) so the banner is removed when no
update is available. Ensure you still safely unwrap "latestVersion" and
"releaseURL" when available is true.
In `@src/wolfwave/Views/Twitch/TwitchViewModel.swift`:
- Line 277: The code is registering for TwitchChatService.connectionStateChanged
but the project requires notification names to be centralized; move the
notification name constant from TwitchChatService
(TwitchChatService.connectionStateChanged) into AppConstants.Notifications
(e.g., add a static let for the same Notification.Name) and update this file to
use AppConstants.Notifications.<name> instead of
TwitchChatService.connectionStateChanged; also update any other usages and
imports so all observers/posters reference the centralized
AppConstants.Notifications entry.
In `@src/WolfWaveTests/KeychainServiceTests.swift`:
- Around line 35-81: These unit tests rely on the real Keychain
(KeychainService.deleteTwitchUsername/loadTwitchUsername/saveTwitchUsername/saveTwitchUsernameIfChanged/deleteTwitchChannelID)
and must be converted to either integration tests or to use a mocked keychain
boundary: extract a KeychainServiceProtocol (or wrapper) that declares
saveTwitchUsername, saveTwitchUsernameIfChanged, loadTwitchUsername,
deleteTwitchUsername, loadTwitchChannelID, deleteTwitchChannelID; update
production KeychainService to implement that protocol and make consuming code
accept the protocol via dependency injection; then rewrite these tests
(testSaveTwitchUsernameIfChangedSavesNew,
testSaveTwitchUsernameIfChangedSkipsSameValue,
testSaveTwitchUsernameIfChangedUpdatesOnChange, testLoadMissingKeyReturnsNil,
testDeleteNonexistentKeyDoesNotThrow) to use a lightweight in-memory/mock
implementation of the protocol (or move the current tests into an integration
test target that runs with keychain entitlements).
- Around line 15-25: Replace the force unwraps in testSaveFailedErrorDescription
and testInvalidDataErrorDescription: use XCTAssertNotNil followed immediately by
let desc = try XCTUnwrap(error.errorDescription) (or directly use let desc = try
XCTUnwrap(...)) and then assert on desc.contains(...) so the tests fail cleanly
instead of crashing; update references to
KeychainService.KeychainError.saveFailed(-25300) and
KeychainService.KeychainError.invalidData accordingly.
---
Outside diff comments:
In `@README.md`:
- Line 97: Update the README table entry for the `make test` row to reflect the
current test suite size: replace the text "124 tests" with "190 tests" in the
table cell that contains the `make test` description so the documentation
matches the PR test plan.
In `@src/wolfwave/Views/Twitch/TwitchDeviceAuthWindowController.swift`:
- Around line 84-106: The TwitchDeviceAuthWindow struct incorrectly uses `@State`
(invalid outside a SwiftUI View) and appears unused; either delete the struct
entirely to remove dead code or convert it into a proper SwiftUI View (rename to
TwitchDeviceAuthWindowView) that defines retainedController as `@StateObject` or
`@State` (depending on lifecycle) and exposes show()/dismiss() via bindings or
view model; if converting, move the TwitchDeviceAuthWindowController
construction into an ObservableObject/ViewModel (e.g.,
TwitchDeviceAuthWindowViewModel) and reference its methods from the View,
otherwise remove TwitchDeviceAuthWindow, its retainedController property and
show()/dismiss() functions (and any references to
TwitchDeviceAuthWindowController from here) to eliminate the misuse of `@State`.
---
Nitpick comments:
In @.github/workflows/release.yml:
- Around line 179-192: Update the "Notarize DMG" step to pass a timeout to the
blocking command: add a NOTARIZE_TIMEOUT environment variable (e.g., default to
3600) to the step and include --timeout "$NOTARIZE_TIMEOUT" on the xcrun
notarytool submit command (keep the existing --wait). Reference the step name
"Notarize DMG", the env vars DMG_NAME/APPLE_ID/APPLE_TEAM_ID/APPLE_APP_PASSWORD,
and the xcrun notarytool submit invocation when making the change so the submit
call cannot hang indefinitely.
In `@CHANGELOG.md`:
- Line 25: The Unreleased section of CHANGELOG.md is missing a bullet
documenting the release signing/notarization pipeline; add a concise bullet
under the "Unreleased" header (near the existing "**CI Pipeline**" entry) that
describes Developer ID signing, notarization, stapling and keychain cleanup
being performed in the CI/release process so the changelog matches the shipped
release behavior.
In `@docs/app/widget/themes.ts`:
- Around line 246-253: The conditionals in themes.ts compare config.textColor
and config.backgroundColor to hardcoded "#FFFFFF" and "#1A1A2E"; change those
comparisons to reference the defaults from defaultWidgetConfig (e.g.,
defaultWidgetConfig.textColor and defaultWidgetConfig.backgroundColor) so the
checks remain correct if defaults change, and keep the rest of the logic
assigning to resolved.textPrimary, resolved.textSecondary,
resolved.progressFillBg and resolved.overlayBg unchanged.
In `@src/wolfwave/Services/Twitch/Commands/BotCommandDispatcher.swift`:
- Around line 120-142: The cooldownValues(for:command:) function currently
matches on hardcoded trigger strings; instead use the command's type or a new
commandType property on the BotCommand protocol to pick the appropriate
UserDefaults keys so mappings can't drift when triggers change. Update the
BotCommand protocol to expose either a commandType enum or provide methods to
return the relevant UserDefaults keys, then change cooldownValues(for:command:)
to switch on command.commandType (or call those methods) and read
AppConstants.UserDefaults.songCommandGlobalCooldown / songCommandUserCooldown or
lastSongCommandGlobalCooldown / lastSongCommandUserCooldown accordingly; keep
the default fallback to command.globalCooldown and command.userCooldown.
In `@src/wolfwave/Services/Twitch/Commands/CooldownManager.swift`:
- Around line 25-27: userCooldowns currently never evicts entries which can grow
unbounded; add a TTL-based eviction so stale per-user entries are removed:
implement a private pruneStaleUserCooldowns(ttl: TimeInterval) that iterates
userCooldowns and removes keys whose Date is older than now - ttl, call this
prune from recordUse(for:trigger:) and isOnCooldown(for:trigger:) (or run it
periodically via a DispatchSourceTimer if preferred), and ensure reset() still
clears the map; reference the userCooldowns property and reset() when making the
change.
In `@src/wolfwave/Services/WebSocket/WebSocketServerService.swift`:
- Around line 71-81: Replace the manual enabledLock.lock()/unlock() in
setEnabled(_:) with enabledLock.withLock { isEnabled = enabled } to match other
services and avoid unlock-before-use issues; keep the existing serverQueue.async
calls that dispatch startServer()/stopServer() (using [weak self]) unchanged so
only the assignment is protected by enabledLock.withLock.
In `@src/wolfwave/Views/Advanced/AdvancedSettingsView.swift`:
- Around line 173-176: Replace the explicit card styling on the Diagnostics
settings block in AdvancedSettingsView (the view using
.padding(AppConstants.SettingsUI.cardPadding), .background(Color(nsColor:
.controlBackgroundColor)), .clipShape(RoundedRectangle(cornerRadius:
AppConstants.SettingsUI.cardCornerRadius))) with the centralized .cardStyle()
modifier; locate the Diagnostics section inside the AdvancedSettingsView struct
and remove those three modifiers and apply .cardStyle() so the cardPadding,
controlBackgroundColor and cardCornerRadius are managed by the single reusable
cardStyle() modifier.
In `@src/wolfwave/Views/Onboarding/OnboardingOBSWidgetStepView.swift`:
- Around line 113-115: Replace the DispatchQueue.main.asyncAfter usage in
OnboardingOBSWidgetStepView that resets copiedURL with a cancellable Swift
concurrency Task: add a state property (e.g., copyResetTask: Task<Void, Never>?)
to the view, cancel any existing copyResetTask before creating a new Task that
awaits Task.sleep(nanoseconds: 2_000_000_000) and then sets copiedURL = false on
the main actor, assign the Task to copyResetTask so it can be cancelled if the
user copies again or the view disappears, and cancel copyResetTask in the view's
onDisappear to avoid stray work.
In `@src/wolfwave/Views/SettingsView.swift`:
- Around line 440-442: Extract the long inline array of UserDefaults keys into a
named constant and use that constant in the forEach call: create a descriptive
constant (e.g., resettableUserDefaultKeys or
AppConstants.UserDefaults.resetKeys) containing
[AppConstants.UserDefaults.trackingEnabled,
AppConstants.UserDefaults.currentSongCommandEnabled,
AppConstants.UserDefaults.lastSongCommandEnabled,
AppConstants.UserDefaults.dockVisibility,
AppConstants.UserDefaults.websocketEnabled,
AppConstants.UserDefaults.websocketURI,
AppConstants.UserDefaults.websocketServerPort,
AppConstants.UserDefaults.hasCompletedOnboarding,
AppConstants.UserDefaults.discordPresenceEnabled] and replace the inline array
passed to UserDefaults.standard.removeObject(forKey:) with that constant to
improve readability and maintainability in SettingsView.swift.
In `@src/wolfwave/Views/WebSocket/WebSocketSettingsView.swift`:
- Around line 471-490: The StatusChip view is duplicated; extract the StatusChip
struct into a single reusable component (e.g., create a new StatusChip SwiftUI
View file) and replace the duplicate definitions in WebSocketSettingsView
(StatusChip) and TwitchSettingsView with references to the shared component;
ensure the new StatusChip preserves the initializer signature (text: String,
color: Color), accessibility (internal/public as needed), and styling so
existing usage compiles, then delete the duplicated structs from both files.
- Around line 327-356: The TextField inputs for widgetTextColor and
widgetBackgroundColor accept arbitrary strings which can produce invalid colors
when used by Color(hex:); add a validation helper (e.g., isValidHexColor(_:)
using the ^#[0-9A-Fa-f]{6}$ regex) and use it in the TextField handlers:
validate onChange and onSubmit before calling broadcastWidgetConfig(), and if
invalid either prevent the broadcast and show a simple inline error state (or
revert to a safe default) and ensure Color(hex:) is always provided a validated
value or a fallback (so references to widgetTextColor, widgetBackgroundColor,
broadcastWidgetConfig(), and Color(hex:) are updated accordingly).
In `@src/wolfwave/WolfWaveApp.swift`:
- Around line 440-457: Extract the hardcoded 5.0 in powerStateChanged into a
named AppConstants value: add a TimeInterval constant like
AppConstants.Music.defaultCheckInterval (e.g., 5.0) and then change the call to
musicMonitor?.updateCheckInterval(...) in the powerStateChanged(_:) method to
use that constant when not in reduced mode; reference the
PowerStateMonitor.shared.isReducedMode check and the
musicMonitor?.updateCheckInterval call to locate where to replace the magic
number.
In `@src/WolfWaveTests/ArtworkServiceTests.swift`:
- Around line 47-63: Tests testFetchWithEmptyTrackCallsCompletion and
testFetchArtworkURLCallsCompletion call service.fetchArtworkURL which performs
real network calls; to avoid flaky CI runs either (a) move these tests into an
integration test target or mark them with a custom flag/annotation so they run
only in integration schemes, or (b) inject/make the network client mockable and
replace the real network layer in these tests with a stubbed/mock implementation
that invokes the completion immediately (verify completion is called
deterministically). Update the ArtworkService (or its initializer) to accept a
NetworkClient protocol dependency and change these two tests to use a mock
NetworkClient that calls the completion without performing real HTTP requests.
In `@src/WolfWaveTests/PowerStateMonitorTests.swift`:
- Around line 33-37: The test testIsReducedModeReturnsBool is tautological
because XCTAssertTrue(value == true || value == false) always holds for any
Bool; either remove this test entirely or replace it with a meaningful check:
e.g., rename to testIsReducedModePropertyAccessible and simply access
PowerStateMonitor.shared.isReducedMode (or add a compile-time type assertion
such as binding it to a Bool) to document readability without a runtime
tautology; update or delete the test method accordingly and keep references to
PowerStateMonitor.shared.isReducedMode and testIsReducedModeReturnsBool when
making the change.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (59)
.github/workflows/release.yml.gitignoreCHANGELOG.mdCOMPLIANCE.mdREADME.mddocs/app/widget/layout.tsxdocs/app/widget/page.tsxdocs/app/widget/themes.tsdocs/content/docs/architecture.mdxdocs/content/docs/development.mdxdocs/content/docs/features.mdxdocs/content/docs/installation.mdxdocs/content/docs/usage.mdxsrc/WolfWaveTests/AppConstantsTests.swiftsrc/WolfWaveTests/ArtworkServiceTests.swiftsrc/WolfWaveTests/CooldownManagerTests.swiftsrc/WolfWaveTests/KeychainServiceTests.swiftsrc/WolfWaveTests/MusicPlaybackMonitorTests.swiftsrc/WolfWaveTests/OnboardingViewModelTests.swiftsrc/WolfWaveTests/PowerStateMonitorTests.swiftsrc/WolfWaveTests/TwitchDeviceAuthTests.swiftsrc/WolfWaveTests/TwitchViewModelTests.swiftsrc/WolfWaveTests/WebSocketServerServiceTests.swiftsrc/wolfwave.xcodeproj/project.pbxprojsrc/wolfwave/Core/AppConstants.swiftsrc/wolfwave/Core/Logger.swiftsrc/wolfwave/Core/PowerStateMonitor.swiftsrc/wolfwave/Monitors/MusicPlaybackMonitor.swiftsrc/wolfwave/Services/ArtworkService.swiftsrc/wolfwave/Services/Discord/DiscordRPCService.swiftsrc/wolfwave/Services/Twitch/Commands/BotCommand.swiftsrc/wolfwave/Services/Twitch/Commands/BotCommandDispatcher.swiftsrc/wolfwave/Services/Twitch/Commands/CooldownManager.swiftsrc/wolfwave/Services/Twitch/Commands/LastSongCommand.swiftsrc/wolfwave/Services/Twitch/Commands/SongCommand.swiftsrc/wolfwave/Services/Twitch/TwitchChatService.swiftsrc/wolfwave/Services/UpdateChecker/UpdateCheckerService.swiftsrc/wolfwave/Services/WebSocket/WebSocketServerService.swiftsrc/wolfwave/Views/Advanced/AdvancedSettingsView.swiftsrc/wolfwave/Views/AppVisibility/AppVisibilitySettingsView.swiftsrc/wolfwave/Views/Discord/DiscordSettingsView.swiftsrc/wolfwave/Views/MusicMonitor/MusicMonitorSettingsView.swiftsrc/wolfwave/Views/Onboarding/OnboardingDiscordStepView.swiftsrc/wolfwave/Views/Onboarding/OnboardingOBSWidgetStepView.swiftsrc/wolfwave/Views/Onboarding/OnboardingView.swiftsrc/wolfwave/Views/Onboarding/OnboardingViewModel.swiftsrc/wolfwave/Views/Onboarding/OnboardingWelcomeStepView.swiftsrc/wolfwave/Views/SettingsView.swiftsrc/wolfwave/Views/Shared/UpdateBannerView.swiftsrc/wolfwave/Views/Shared/ViewModifiers.swiftsrc/wolfwave/Views/Twitch/DeviceCodeView.swiftsrc/wolfwave/Views/Twitch/TwitchDeviceAuthDialog.swiftsrc/wolfwave/Views/Twitch/TwitchDeviceAuthWindowController.swiftsrc/wolfwave/Views/Twitch/TwitchSettingsView.swiftsrc/wolfwave/Views/Twitch/TwitchViewModel.swiftsrc/wolfwave/Views/WebSocket/WebSocketSettingsView.swiftsrc/wolfwave/WolfWaveApp.swiftsrc/wolfwave/wolfwave.dev.entitlementssrc/wolfwave/wolfwave.entitlements
💤 Files with no reviewable changes (1)
- src/WolfWaveTests/TwitchViewModelTests.swift
…code removal, and doc updates Core fixes: Logger exportLogFile() race condition, MusicPlaybackMonitor interval validation and dedup key, ArtworkService synchronous cache write, BotCommandDispatcher thread-safe iteration. View fixes: SongCommand/LastSongCommand negative truncation guard, AdvancedSettings path sanitization, Discord cancellable clear task, MusicMonitor silent permission check, UpdateBanner clears on unavailable, centralized Twitch notification name. Removed dead TwitchDeviceAuthWindow struct. Updated test assertions and doc test counts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
notarytool submit --wait), stapling, and keychain cleanup to the GitHub Actions release pipeline. DMG releases were previously unsigned and would be blocked by Gatekeeper.Test plan
make test— 190 tests, 0 failuresrelease.ymlsteps🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests