@@ -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,17 +243,12 @@ func TestIssueIDsExtraction(t *testing.T) {
243243 defer func () {
244244 issuesInBodyREs = originalRegex
245245 }()
246- << << << < HEAD
247-
248- // For testing, use a regex that matches any 3-letter prefix format
249- == == == =
250246
251247 // For testing, use a regex that matches team keys of 2-10 chars and issue numbers 1-5 digits
252- >> >> >> > 3 aa6f7157 (fix (linear - sync ): support variable - length team keys in issue regex (#3469 ))
253248 issuesInBodyREs = []* regexp.Regexp {
254249 regexp .MustCompile (`(?P<issue>\w{2,10}-\d{1,5})` ),
255250 }
256-
251+
257252 testCases := []struct {
258253 name string
259254 body string
@@ -321,7 +316,7 @@ func TestIssueIDsExtraction(t *testing.T) {
321316 expected : []string {"eng-12345" },
322317 },
323318 }
324-
319+
325320 for _ , tc := range testCases {
326321 t .Run (tc .name , func (t * testing.T ) {
327322 pr := LinearPullRequest {
@@ -330,15 +325,15 @@ func TestIssueIDsExtraction(t *testing.T) {
330325 HeadRefName : tc .headRefName ,
331326 },
332327 }
333-
328+
334329 result := pr .IssueIDs ()
335-
330+
336331 if len (result ) != len (tc .expected ) {
337332 t .Errorf ("Expected %d issues, got %d" , len (tc .expected ), len (result ))
338333 t .Errorf ("Expected: %v, Got: %v" , tc .expected , result )
339334 return
340335 }
341-
336+
342337 // Check all expected IDs are found (ignoring order)
343338 for _ , expectedID := range tc .expected {
344339 found := false
@@ -355,3 +350,90 @@ func TestIssueIDsExtraction(t *testing.T) {
355350 })
356351 }
357352}
353+
354+ func TestIsStableRelease (t * testing.T ) {
355+ testCases := []struct {
356+ version string
357+ expected bool
358+ }{
359+ // Stable releases
360+ {"v0.26.1" , true },
361+ {"v4.5.0" , true },
362+ {"v1.0.0" , true },
363+ {"0.26.1" , true }, // without v prefix
364+ {"v27.0.0" , true },
365+
366+ // Pre-releases
367+ {"v0.26.1-alpha.1" , false },
368+ {"v0.26.1-alpha.5" , false },
369+ {"v0.26.1-beta.1" , false },
370+ {"v0.26.1-rc.1" , false },
371+ {"v0.26.1-rc.4" , false },
372+ {"v0.26.1-dev.1" , false },
373+ {"v0.26.1-pre.1" , false },
374+ {"v0.26.1-next.1" , false },
375+ {"v4.5.0-beta.2" , false },
376+ {"0.27.0-alpha.1" , false }, // without v prefix
377+ }
378+
379+ for _ , tc := range testCases {
380+ t .Run (tc .version , func (t * testing.T ) {
381+ result := isStableRelease (tc .version )
382+ if result != tc .expected {
383+ t .Errorf ("isStableRelease(%q) = %v, want %v" , tc .version , result , tc .expected )
384+ }
385+ })
386+ }
387+ }
388+
389+ func TestStableReleaseCommentText (t * testing.T ) {
390+ // Test the comment text logic for different scenarios
391+ testCases := []struct {
392+ name string
393+ alreadyReleased bool
394+ isStable bool
395+ releaseTag string
396+ releaseDate string
397+ expectedContains string
398+ }{
399+ {
400+ name : "First release (pre-release)" ,
401+ alreadyReleased : false ,
402+ isStable : false ,
403+ releaseTag : "v0.27.0-alpha.1" ,
404+ releaseDate : "2025-01-15" ,
405+ expectedContains : "first released in" ,
406+ },
407+ {
408+ name : "First release (stable)" ,
409+ alreadyReleased : false ,
410+ isStable : true ,
411+ releaseTag : "v0.27.0" ,
412+ releaseDate : "2025-02-01" ,
413+ expectedContains : "first released in" ,
414+ },
415+ {
416+ name : "Stable release on already-released issue" ,
417+ alreadyReleased : true ,
418+ isStable : true ,
419+ releaseTag : "v0.27.0" ,
420+ releaseDate : "2025-02-01" ,
421+ expectedContains : "Now available in stable release" ,
422+ },
423+ }
424+
425+ for _ , tc := range testCases {
426+ t .Run (tc .name , func (t * testing.T ) {
427+ var releaseComment string
428+ if tc .alreadyReleased && tc .isStable {
429+ releaseComment = fmt .Sprintf ("Now available in stable release %v (released %v)" , tc .releaseTag , tc .releaseDate )
430+ } else {
431+ releaseComment = fmt .Sprintf ("This issue was first released in %v on %v" , tc .releaseTag , tc .releaseDate )
432+ }
433+
434+ if ! strings .Contains (releaseComment , tc .expectedContains ) {
435+ t .Errorf ("Comment %q does not contain expected text %q" , releaseComment , tc .expectedContains )
436+ }
437+ })
438+ }
439+ }
0 commit comments