@@ -54,9 +54,9 @@ func TestMoveIssueLogic(t *testing.T) {
5454
5555// MockLinearClient is a mock implementation of the LinearClient interface for testing
5656type MockLinearClient struct {
57- mockIssueStates map [string ]string
58- mockIssueStateNames map [string ]string
59- mockWorkflowIDs map [string ]string
57+ mockIssueStates map [string ]string
58+ mockIssueStateNames map [string ]string
59+ mockWorkflowIDs map [string ]string
6060}
6161
6262func NewMockLinearClient () * MockLinearClient {
@@ -109,25 +109,25 @@ func (m *MockLinearClient) MoveIssueToState(ctx context.Context, dryRun bool, is
109109 if strings .HasPrefix (strings .ToLower (issueID ), "cve" ) {
110110 return nil
111111 }
112-
112+
113113 currentStateID , currentStateName , _ := m .IssueStateDetails (ctx , issueID )
114-
114+
115115 // Already in released state
116116 if currentStateID == releasedStateID {
117117 return nil
118118 }
119-
119+
120120 // Skip if not in ready for release state
121121 if currentStateName != readyForReleaseStateName {
122122 return fmt .Errorf ("issue %s not in ready for release state" , issueID )
123123 }
124-
124+
125125 // Only ENG-1234 is expected to be moved successfully
126126 // Explicitly return errors for other issues to ensure the test only counts ENG-1234
127127 if issueID != "ENG-1234" {
128128 return fmt .Errorf ("would not move issue %s for test purposes" , issueID )
129129 }
130-
130+
131131 return nil
132132}
133133
@@ -136,8 +136,8 @@ func TestIsIssueInState(t *testing.T) {
136136 ctx := context .Background ()
137137
138138 testCases := []struct {
139- IssueID string
140- StateID string
139+ IssueID string
140+ StateID string
141141 ExpectedResult bool
142142 }{
143143 {"ENG-1234" , "ready-state-id" , true },
@@ -164,10 +164,10 @@ func TestMoveIssueStateFiltering(t *testing.T) {
164164 // Create a custom mock client for this test
165165 mockClient := & MockLinearClient {
166166 mockIssueStates : map [string ]string {
167- "ENG-1234" : "ready-state-id" , // Ready for release
168- "ENG-5678" : "in-progress-id" , // In progress
169- "ENG-9012" : "released-id" , // Already released
170- "CVE-1234" : "ready-state-id" , // Ready but should be skipped as CVE
167+ "ENG-1234" : "ready-state-id" , // Ready for release
168+ "ENG-5678" : "in-progress-id" , // In progress
169+ "ENG-9012" : "released-id" , // Already released
170+ "CVE-1234" : "ready-state-id" , // Ready but should be skipped as CVE
171171 },
172172 mockIssueStateNames : map [string ]string {
173173 "ENG-1234" : "Ready for Release" ,
@@ -181,7 +181,7 @@ func TestMoveIssueStateFiltering(t *testing.T) {
181181 "In Progress" : "in-progress-id" ,
182182 },
183183 }
184-
184+
185185 ctx := context .Background ()
186186
187187 // Test cases for the overall filtering logic
@@ -198,19 +198,19 @@ func TestMoveIssueStateFiltering(t *testing.T) {
198198 if strings .HasPrefix (strings .ToLower (issueID ), "cve" ) {
199199 continue
200200 }
201-
201+
202202 currentStateID , currentStateName , _ := mockClient .IssueStateDetails (ctx , issueID )
203-
203+
204204 // Skip if already in released state
205205 if currentStateID == releasedStateID {
206206 continue
207207 }
208-
208+
209209 // Skip if not in ready for release state
210210 if currentStateName != readyForReleaseStateName {
211211 continue
212212 }
213-
213+
214214 // This issue would be moved
215215 actualMoved = append (actualMoved , issueID )
216216 }
@@ -230,7 +230,7 @@ func TestMoveIssueStateFiltering(t *testing.T) {
230230 break
231231 }
232232 }
233-
233+
234234 if ! found {
235235 t .Errorf ("Expected issue %s to be moved, but it wasn't in the result set" , expectedID )
236236 }
@@ -243,12 +243,12 @@ func TestIssueIDsExtraction(t *testing.T) {
243243 defer func () {
244244 issuesInBodyREs = originalRegex
245245 }()
246-
246+
247247 // For testing, use a regex that matches any 3-letter prefix format
248248 issuesInBodyREs = []* regexp.Regexp {
249249 regexp .MustCompile (`(?P<issue>\w{3}-\d{4})` ),
250250 }
251-
251+
252252 testCases := []struct {
253253 name string
254254 body string
@@ -286,7 +286,7 @@ func TestIssueIDsExtraction(t *testing.T) {
286286 expected : []string {},
287287 },
288288 }
289-
289+
290290 for _ , tc := range testCases {
291291 t .Run (tc .name , func (t * testing.T ) {
292292 pr := LinearPullRequest {
@@ -295,15 +295,15 @@ func TestIssueIDsExtraction(t *testing.T) {
295295 HeadRefName : tc .headRefName ,
296296 },
297297 }
298-
298+
299299 result := pr .IssueIDs ()
300-
300+
301301 if len (result ) != len (tc .expected ) {
302302 t .Errorf ("Expected %d issues, got %d" , len (tc .expected ), len (result ))
303303 t .Errorf ("Expected: %v, Got: %v" , tc .expected , result )
304304 return
305305 }
306-
306+
307307 // Check all expected IDs are found (ignoring order)
308308 for _ , expectedID := range tc .expected {
309309 found := false
@@ -320,3 +320,90 @@ func TestIssueIDsExtraction(t *testing.T) {
320320 })
321321 }
322322}
323+
324+ func TestIsStableRelease (t * testing.T ) {
325+ testCases := []struct {
326+ version string
327+ expected bool
328+ }{
329+ // Stable releases
330+ {"v0.26.1" , true },
331+ {"v4.5.0" , true },
332+ {"v1.0.0" , true },
333+ {"0.26.1" , true }, // without v prefix
334+ {"v27.0.0" , true },
335+
336+ // Pre-releases
337+ {"v0.26.1-alpha.1" , false },
338+ {"v0.26.1-alpha.5" , false },
339+ {"v0.26.1-beta.1" , false },
340+ {"v0.26.1-rc.1" , false },
341+ {"v0.26.1-rc.4" , false },
342+ {"v0.26.1-dev.1" , false },
343+ {"v0.26.1-pre.1" , false },
344+ {"v0.26.1-next.1" , false },
345+ {"v4.5.0-beta.2" , false },
346+ {"0.27.0-alpha.1" , false }, // without v prefix
347+ }
348+
349+ for _ , tc := range testCases {
350+ t .Run (tc .version , func (t * testing.T ) {
351+ result := isStableRelease (tc .version )
352+ if result != tc .expected {
353+ t .Errorf ("isStableRelease(%q) = %v, want %v" , tc .version , result , tc .expected )
354+ }
355+ })
356+ }
357+ }
358+
359+ func TestStableReleaseCommentText (t * testing.T ) {
360+ // Test the comment text logic for different scenarios
361+ testCases := []struct {
362+ name string
363+ alreadyReleased bool
364+ isStable bool
365+ releaseTag string
366+ releaseDate string
367+ expectedContains string
368+ }{
369+ {
370+ name : "First release (pre-release)" ,
371+ alreadyReleased : false ,
372+ isStable : false ,
373+ releaseTag : "v0.27.0-alpha.1" ,
374+ releaseDate : "2025-01-15" ,
375+ expectedContains : "first released in" ,
376+ },
377+ {
378+ name : "First release (stable)" ,
379+ alreadyReleased : false ,
380+ isStable : true ,
381+ releaseTag : "v0.27.0" ,
382+ releaseDate : "2025-02-01" ,
383+ expectedContains : "first released in" ,
384+ },
385+ {
386+ name : "Stable release on already-released issue" ,
387+ alreadyReleased : true ,
388+ isStable : true ,
389+ releaseTag : "v0.27.0" ,
390+ releaseDate : "2025-02-01" ,
391+ expectedContains : "Now available in stable release" ,
392+ },
393+ }
394+
395+ for _ , tc := range testCases {
396+ t .Run (tc .name , func (t * testing.T ) {
397+ var releaseComment string
398+ if tc .alreadyReleased && tc .isStable {
399+ releaseComment = fmt .Sprintf ("Now available in stable release %v (released %v)" , tc .releaseTag , tc .releaseDate )
400+ } else {
401+ releaseComment = fmt .Sprintf ("This issue was first released in %v on %v" , tc .releaseTag , tc .releaseDate )
402+ }
403+
404+ if ! strings .Contains (releaseComment , tc .expectedContains ) {
405+ t .Errorf ("Comment %q does not contain expected text %q" , releaseComment , tc .expectedContains )
406+ }
407+ })
408+ }
409+ }
0 commit comments