Skip to content

Commit 3aa24aa

Browse files
committed
mutex everything
1 parent a9bca8a commit 3aa24aa

File tree

5 files changed

+35
-10
lines changed

5 files changed

+35
-10
lines changed

cmd/goose/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ type App struct {
9191
updateInterval time.Duration
9292
consecutiveFailures int
9393
mu sync.RWMutex
94+
menuMutex sync.Mutex // Mutex to prevent concurrent menu rebuilds
9495
enableAutoBrowser bool
9596
hideStaleIncoming bool
9697
hasPerformedInitialDiscovery bool // Track if we've done the first poll to distinguish from real state changes

cmd/goose/main_test.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"os"
7+
"runtime"
78
"sync"
89
"testing"
910
"time"
@@ -196,8 +197,15 @@ func TestTrayIconRestoredAfterNetworkRecovery(t *testing.T) {
196197
}
197198
app.setTrayTitle()
198199
initialTitle := mock.title
199-
if initialTitle != "🪿 1" {
200-
t.Errorf("Expected initial tray title '🪿 1', got %q", initialTitle)
200+
201+
// Expected title varies by platform
202+
expectedTitle := "1/1" // Linux format
203+
if runtime.GOOS == "darwin" {
204+
expectedTitle = "1" // macOS format
205+
}
206+
207+
if initialTitle != expectedTitle {
208+
t.Errorf("Expected initial tray title %q, got %q", expectedTitle, initialTitle)
201209
}
202210

203211
// Simulate network failure - updatePRs would set warning icon and return early
@@ -213,8 +221,8 @@ func TestTrayIconRestoredAfterNetworkRecovery(t *testing.T) {
213221
// With our fix, setTrayTitle() is now called after successful fetch
214222
app.setTrayTitle()
215223
recoveredTitle := mock.title
216-
if recoveredTitle != "🪿 1" {
217-
t.Errorf("Expected tray title to be restored to '🪿 1' after recovery, got %q", recoveredTitle)
224+
if recoveredTitle != expectedTitle {
225+
t.Errorf("Expected tray title to be restored to %q after recovery, got %q", expectedTitle, recoveredTitle)
218226
}
219227
}
220228

@@ -447,15 +455,19 @@ func TestSoundPlaybackDuringTransitions(t *testing.T) {
447455
// Actual sound playback is verified through integration testing.
448456

449457
// Set initial state
458+
app.mu.Lock()
450459
app.incoming = tt.initialIncoming
451460
app.outgoing = tt.initialOutgoing
461+
app.mu.Unlock()
452462

453463
// Run first check to establish baseline
454464
app.checkForNewlyBlockedPRs(ctx)
455465

456466
// Update to new state
467+
app.mu.Lock()
457468
app.incoming = tt.updatedIncoming
458469
app.outgoing = tt.updatedOutgoing
470+
app.mu.Unlock()
459471

460472
// Run check again to detect transitions
461473
app.checkForNewlyBlockedPRs(ctx)
@@ -465,6 +477,7 @@ func TestSoundPlaybackDuringTransitions(t *testing.T) {
465477
if len(tt.expectedSounds) > 0 {
466478
// Check that blocked PRs are tracked in previousBlockedPRs
467479
blocked := 0
480+
app.mu.RLock()
468481
for _, pr := range app.incoming {
469482
if pr.NeedsReview && app.previousBlockedPRs[pr.URL] {
470483
blocked++
@@ -475,6 +488,7 @@ func TestSoundPlaybackDuringTransitions(t *testing.T) {
475488
blocked++
476489
}
477490
}
491+
app.mu.RUnlock()
478492
if blocked == 0 {
479493
t.Errorf("%s: expected blocked PRs to be tracked in previousBlockedPRs", tt.description)
480494
}

cmd/goose/notifications.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,10 @@ func (app *App) processNotifications(ctx context.Context) {
9090
}
9191
}
9292

93-
// Update menu if we sent notifications
94-
if len(toNotify) > 0 {
95-
slog.Debug("[NOTIFY] Updating menu after notifications")
96-
app.updateMenu(ctx)
97-
}
9893
}()
9994

100-
// Update menu if we sent notifications
95+
// Update menu immediately after sending notifications
96+
// This needs to happen in the main thread to show the party popper emoji
10197
if len(toNotify) > 0 {
10298
slog.Info("[FLOW] Updating menu after sending notifications", "notified_count", len(toNotify))
10399
app.updateMenu(ctx)

cmd/goose/systray_interface.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"log/slog"
5+
"sync"
56

67
"github.com/energye/systray"
78
)
@@ -54,15 +55,20 @@ func (*RealSystray) Quit() {
5455

5556
// MockSystray implements SystrayInterface for testing.
5657
type MockSystray struct {
58+
mu sync.Mutex
5759
title string
5860
menuItems []string
5961
}
6062

6163
func (m *MockSystray) ResetMenu() {
64+
m.mu.Lock()
65+
defer m.mu.Unlock()
6266
m.menuItems = nil
6367
}
6468

6569
func (m *MockSystray) AddMenuItem(title, tooltip string) MenuItem {
70+
m.mu.Lock()
71+
defer m.mu.Unlock()
6672
m.menuItems = append(m.menuItems, title)
6773
// Return a MockMenuItem that won't panic when methods are called
6874
return &MockMenuItem{
@@ -72,10 +78,14 @@ func (m *MockSystray) AddMenuItem(title, tooltip string) MenuItem {
7278
}
7379

7480
func (m *MockSystray) AddSeparator() {
81+
m.mu.Lock()
82+
defer m.mu.Unlock()
7583
m.menuItems = append(m.menuItems, "---")
7684
}
7785

7886
func (m *MockSystray) SetTitle(title string) {
87+
m.mu.Lock()
88+
defer m.mu.Unlock()
7989
m.title = title
8090
}
8191

cmd/goose/ui.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,10 @@ func (app *App) generatePRSectionTitles(prs []PR, sectionTitle string, hiddenOrg
555555

556556
// rebuildMenu completely rebuilds the menu from scratch.
557557
func (app *App) rebuildMenu(ctx context.Context) {
558+
// Prevent concurrent menu rebuilds
559+
app.menuMutex.Lock()
560+
defer app.menuMutex.Unlock()
561+
558562
// Rebuild entire menu
559563
slog.Info("[MENU] Starting rebuildMenu", "os", runtime.GOOS)
560564

0 commit comments

Comments
 (0)