1+ package main
2+
3+ import (
4+ "slices"
5+ "sync"
6+ "testing"
7+ "time"
8+ )
9+
10+ // TestMenuChangeDetection tests that the menu change detection logic works correctly
11+ // and prevents unnecessary menu rebuilds when PR data hasn't changed.
12+ func TestMenuChangeDetection (t * testing.T ) {
13+ // Create app with test data
14+ app := & App {
15+ mu : sync.RWMutex {},
16+ stateManager : NewPRStateManager (time .Now ()),
17+ hiddenOrgs : make (map [string ]bool ),
18+ seenOrgs : make (map [string ]bool ),
19+ blockedPRTimes : make (map [string ]time.Time ),
20+ browserRateLimiter : NewBrowserRateLimiter (30 * time .Second , 5 , defaultMaxBrowserOpensDay ),
21+ systrayInterface : & MockSystray {},
22+ incoming : []PR {
23+ {Repository : "org1/repo1" , Number : 1 , Title : "Fix bug" , URL : "https://github.com/org1/repo1/pull/1" , NeedsReview : true , UpdatedAt : time .Now ()},
24+ {Repository : "org2/repo2" , Number : 2 , Title : "Add feature" , URL : "https://github.com/org2/repo2/pull/2" , NeedsReview : false , UpdatedAt : time .Now ()},
25+ },
26+ outgoing : []PR {
27+ {Repository : "org3/repo3" , Number : 3 , Title : "Update docs" , URL : "https://github.com/org3/repo3/pull/3" , IsBlocked : true , UpdatedAt : time .Now ()},
28+ },
29+ }
30+
31+ t .Run ("same_titles_should_be_equal" , func (t * testing.T ) {
32+ // Generate titles twice with same data
33+ titles1 := app .generateMenuTitles ()
34+ titles2 := app .generateMenuTitles ()
35+
36+ // They should be equal
37+ if ! slices .Equal (titles1 , titles2 ) {
38+ t .Errorf ("Same PR data generated different titles:\n First: %v\n Second: %v" , titles1 , titles2 )
39+ }
40+ })
41+
42+ t .Run ("different_pr_count_changes_titles" , func (t * testing.T ) {
43+ // Generate initial titles
44+ initialTitles := app .generateMenuTitles ()
45+
46+ // Add a new PR
47+ app .incoming = append (app .incoming , PR {
48+ Repository : "org4/repo4" ,
49+ Number : 4 ,
50+ Title : "New PR" ,
51+ URL : "https://github.com/org4/repo4/pull/4" ,
52+ NeedsReview : true ,
53+ UpdatedAt : time .Now (),
54+ })
55+
56+ // Generate new titles
57+ newTitles := app .generateMenuTitles ()
58+
59+ // They should be different
60+ if slices .Equal (initialTitles , newTitles ) {
61+ t .Error ("Adding a PR didn't change the menu titles" )
62+ }
63+
64+ // The new titles should have more items
65+ if len (newTitles ) <= len (initialTitles ) {
66+ t .Errorf ("New titles should have more items: got %d, initial had %d" , len (newTitles ), len (initialTitles ))
67+ }
68+ })
69+
70+ t .Run ("pr_repository_change_updates_menu" , func (t * testing.T ) {
71+ // Generate initial titles
72+ initialTitles := app .generateMenuTitles ()
73+
74+ // Change a PR repository (this would be unusual but tests the title generation)
75+ app .incoming [0 ].Repository = "different-org/different-repo"
76+
77+ // Generate new titles
78+ newTitles := app .generateMenuTitles ()
79+
80+ // They should be different because menu shows "org/repo #number"
81+ if slices .Equal (initialTitles , newTitles ) {
82+ t .Error ("Changing a PR repository didn't change the menu titles" )
83+ }
84+ })
85+
86+ t .Run ("blocked_status_change_updates_menu" , func (t * testing.T ) {
87+ // Generate initial titles
88+ initialTitles := app .generateMenuTitles ()
89+
90+ // Change blocked status
91+ app .incoming [1 ].NeedsReview = true // Make it blocked
92+
93+ // Generate new titles
94+ newTitles := app .generateMenuTitles ()
95+
96+ // They should be different because the title prefix changes for blocked PRs
97+ if slices .Equal (initialTitles , newTitles ) {
98+ t .Error ("Changing PR blocked status didn't change the menu titles" )
99+ }
100+ })
101+ }
102+
103+ // TestFirstRunMenuRebuildBug tests the specific bug where the first scheduled update
104+ // after initial load would unnecessarily rebuild the menu.
105+ func TestFirstRunMenuRebuildBug (t * testing.T ) {
106+ // Create app simulating initial state
107+ app := & App {
108+ mu : sync.RWMutex {},
109+ stateManager : NewPRStateManager (time .Now ()),
110+ hiddenOrgs : make (map [string ]bool ),
111+ seenOrgs : make (map [string ]bool ),
112+ blockedPRTimes : make (map [string ]time.Time ),
113+ browserRateLimiter : NewBrowserRateLimiter (30 * time .Second , 5 , defaultMaxBrowserOpensDay ),
114+ menuInitialized : false ,
115+ systrayInterface : & MockSystray {},
116+ lastMenuTitles : nil , // This is nil on first run - the bug condition
117+ incoming : []PR {
118+ {Repository : "test/repo" , Number : 1 , Title : "Test PR" , URL : "https://github.com/test/repo/pull/1" },
119+ },
120+ }
121+
122+ // Simulate what happens during initial load
123+ // OLD BEHAVIOR: lastMenuTitles would remain nil
124+ // NEW BEHAVIOR: lastMenuTitles should be set after initial menu build
125+
126+ // Generate initial titles (simulating what rebuildMenu would show)
127+ initialTitles := app .generateMenuTitles ()
128+
129+ // This is the fix - store titles after initial build
130+ app .mu .Lock ()
131+ app .lastMenuTitles = initialTitles
132+ app .menuInitialized = true
133+ app .mu .Unlock ()
134+
135+ // Now simulate first scheduled update with same data
136+ // Generate current titles (should be same as initial)
137+ currentTitles := app .generateMenuTitles ()
138+
139+ // Get stored titles
140+ app .mu .RLock ()
141+ storedTitles := app .lastMenuTitles
142+ app .mu .RUnlock ()
143+
144+ // Test 1: Stored titles should not be nil/empty
145+ if len (storedTitles ) == 0 {
146+ t .Fatal ("BUG: lastMenuTitles not set after initial menu build" )
147+ }
148+
149+ // Test 2: Current and stored titles should be equal (no changes)
150+ if ! slices .Equal (currentTitles , storedTitles ) {
151+ t .Errorf ("BUG: Titles marked as different when they're the same:\n Current: %v\n Stored: %v" ,
152+ currentTitles , storedTitles )
153+ }
154+
155+ // Test 3: Verify the comparison result that updateMenu would make
156+ // In the bug, this would be comparing non-empty current titles with nil/empty stored titles
157+ // and would return false (different), triggering unnecessary rebuild
158+ shouldSkipRebuild := slices .Equal (currentTitles , storedTitles )
159+ if ! shouldSkipRebuild {
160+ t .Error ("BUG: Would rebuild menu even though PR data hasn't changed" )
161+ }
162+ }
163+
164+ // TestHiddenOrgChangesMenu tests that hiding/showing orgs updates menu titles
165+ func TestHiddenOrgChangesMenu (t * testing.T ) {
166+ app := & App {
167+ mu : sync.RWMutex {},
168+ stateManager : NewPRStateManager (time .Now ()),
169+ hiddenOrgs : make (map [string ]bool ),
170+ seenOrgs : make (map [string ]bool ),
171+ blockedPRTimes : make (map [string ]time.Time ),
172+ browserRateLimiter : NewBrowserRateLimiter (30 * time .Second , 5 , defaultMaxBrowserOpensDay ),
173+ systrayInterface : & MockSystray {},
174+ incoming : []PR {
175+ {Repository : "org1/repo1" , Number : 1 , Title : "PR 1" , URL : "https://github.com/org1/repo1/pull/1" },
176+ {Repository : "org2/repo2" , Number : 2 , Title : "PR 2" , URL : "https://github.com/org2/repo2/pull/2" },
177+ },
178+ }
179+
180+ // Generate initial titles
181+ initialTitles := app .generateMenuTitles ()
182+ initialCount := len (initialTitles )
183+
184+ // Hide org1
185+ app .hiddenOrgs ["org1" ] = true
186+
187+ // Generate new titles - should have fewer items
188+ newTitles := app .generateMenuTitles ()
189+
190+ // Titles should be different
191+ if slices .Equal (initialTitles , newTitles ) {
192+ t .Error ("Hiding an org didn't change menu titles" )
193+ }
194+
195+ // Should have fewer items (org1/repo1 should be hidden)
196+ if len (newTitles ) >= initialCount {
197+ t .Errorf ("Menu should have fewer items after hiding org: got %d, started with %d" ,
198+ len (newTitles ), initialCount )
199+ }
200+ }
0 commit comments