@@ -2,6 +2,7 @@ package cmd
22
33import (
44 "encoding/json"
5+ "errors"
56 "os"
67 "path/filepath"
78 "testing"
@@ -137,7 +138,10 @@ func TestUnstack_API404_TreatedAsIdempotentSuccess(t *testing.T) {
137138 writeStackFile (t , gitDir , stack.Stack {
138139 ID : "99" ,
139140 Trunk : stack.BranchRef {Branch : "main" },
140- Branches : []stack.BranchRef {{Branch : "b1" }, {Branch : "b2" }},
141+ Branches : []stack.BranchRef {
142+ {Branch : "b1" , PullRequest : & stack.PullRequestRef {Number : 101 , Merged : true }},
143+ {Branch : "b2" , PullRequest : & stack.PullRequestRef {Number : 102 }},
144+ },
141145 })
142146
143147 cfg , outR , errR := config .NewTestConfig ()
@@ -170,7 +174,10 @@ func TestUnstack_API409_ShowsErrorAndStopsLocalDeletion(t *testing.T) {
170174 writeStackFile (t , gitDir , stack.Stack {
171175 ID : "99" ,
172176 Trunk : stack.BranchRef {Branch : "main" },
173- Branches : []stack.BranchRef {{Branch : "b1" }, {Branch : "b2" }},
177+ Branches : []stack.BranchRef {
178+ {Branch : "b1" , PullRequest : & stack.PullRequestRef {Number : 101 , Merged : true }},
179+ {Branch : "b2" , PullRequest : & stack.PullRequestRef {Number : 102 }},
180+ },
174181 })
175182
176183 cfg , outR , errR := config .NewTestConfig ()
@@ -226,3 +233,179 @@ func TestUnstack_RemovesCorrectStackByPointer(t *testing.T) {
226233 require .Len (t , sf .Stacks , 1 , "should remove exactly one stack" )
227234 assert .Equal (t , []string {"b1" , "b2" }, sf .Stacks [0 ].BranchNames (), "should keep the OTHER stack intact" )
228235}
236+
237+ func TestUnstack_PreflightBlocksDelete_WhenAllPRsIneligible (t * testing.T ) {
238+ gitDir := t .TempDir ()
239+ restore := git .SetOps (& git.MockOps {
240+ GitDirFn : func () (string , error ) { return gitDir , nil },
241+ CurrentBranchFn : func () (string , error ) { return "b1" , nil },
242+ })
243+ defer restore ()
244+
245+ writeStackFile (t , gitDir , stack.Stack {
246+ ID : "99" ,
247+ Trunk : stack.BranchRef {Branch : "main" },
248+ Branches : []stack.BranchRef {
249+ {Branch : "b1" , PullRequest : & stack.PullRequestRef {Number : 101 , Merged : true }},
250+ {Branch : "b2" , PullRequest : & stack.PullRequestRef {Number : 102 }},
251+ },
252+ })
253+
254+ deleteCalled := false
255+ cfg , outR , errR := config .NewTestConfig ()
256+ cfg .GitHubClientOverride = & github.MockClient {
257+ FindPRByNumberFn : func (number int ) (* github.PullRequest , error ) {
258+ switch number {
259+ case 101 :
260+ return & github.PullRequest {Number : 101 , State : "MERGED" }, nil
261+ case 102 :
262+ return & github.PullRequest {Number : 102 , State : "OPEN" , MergeQueueEntry : & github.MergeQueueEntry {ID : "MQE_1" }}, nil
263+ default :
264+ return nil , nil
265+ }
266+ },
267+ DeleteStackFn : func (stackID string ) error {
268+ deleteCalled = true
269+ return nil
270+ },
271+ }
272+
273+ err := runUnstack (cfg , & unstackOptions {})
274+ output := collectOutput (cfg , outR , errR )
275+
276+ assert .ErrorIs (t , err , ErrInvalidArgs )
277+ assert .False (t , deleteCalled , "DeleteStack should not be called when all PRs are ineligible" )
278+ assert .Contains (t , output , "Unstacking not allowed" )
279+ assert .NotContains (t , output , "Stack removed from local tracking" )
280+
281+ sf , loadErr := stack .Load (gitDir )
282+ require .NoError (t , loadErr )
283+ require .Len (t , sf .Stacks , 1 )
284+ }
285+
286+ func TestUnstack_PreflightAllowsDelete_WhenMixedEligibility (t * testing.T ) {
287+ gitDir := t .TempDir ()
288+ restore := git .SetOps (& git.MockOps {
289+ GitDirFn : func () (string , error ) { return gitDir , nil },
290+ CurrentBranchFn : func () (string , error ) { return "b1" , nil },
291+ })
292+ defer restore ()
293+
294+ writeStackFile (t , gitDir , stack.Stack {
295+ ID : "99" ,
296+ Trunk : stack.BranchRef {Branch : "main" },
297+ Branches : []stack.BranchRef {
298+ {Branch : "b1" , PullRequest : & stack.PullRequestRef {Number : 101 }},
299+ {Branch : "b2" , PullRequest : & stack.PullRequestRef {Number : 102 }},
300+ },
301+ })
302+
303+ deleteCalled := false
304+ cfg , outR , errR := config .NewTestConfig ()
305+ cfg .GitHubClientOverride = & github.MockClient {
306+ FindPRByNumberFn : func (number int ) (* github.PullRequest , error ) {
307+ switch number {
308+ case 101 :
309+ return & github.PullRequest {Number : 101 , State : "MERGED" }, nil
310+ case 102 :
311+ return & github.PullRequest {Number : 102 , State : "OPEN" }, nil
312+ default :
313+ return nil , nil
314+ }
315+ },
316+ DeleteStackFn : func (stackID string ) error {
317+ deleteCalled = true
318+ return nil
319+ },
320+ }
321+
322+ err := runUnstack (cfg , & unstackOptions {})
323+ output := collectOutput (cfg , outR , errR )
324+
325+ require .NoError (t , err )
326+ assert .True (t , deleteCalled , "DeleteStack should be called when at least one PR is eligible" )
327+ assert .Contains (t , output , "Stack deleted on GitHub" )
328+ assert .Contains (t , output , "Stack removed from local tracking" )
329+
330+ sf , loadErr := stack .Load (gitDir )
331+ require .NoError (t , loadErr )
332+ assert .Empty (t , sf .Stacks )
333+ }
334+
335+ func TestUnstack_PreflightLookupFailure_StopsDeletion (t * testing.T ) {
336+ gitDir := t .TempDir ()
337+ restore := git .SetOps (& git.MockOps {
338+ GitDirFn : func () (string , error ) { return gitDir , nil },
339+ CurrentBranchFn : func () (string , error ) { return "b1" , nil },
340+ })
341+ defer restore ()
342+
343+ writeStackFile (t , gitDir , stack.Stack {
344+ ID : "99" ,
345+ Trunk : stack.BranchRef {Branch : "main" },
346+ Branches : []stack.BranchRef {{Branch : "b1" , PullRequest : & stack.PullRequestRef {Number : 101 }}},
347+ })
348+
349+ deleteCalled := false
350+ cfg , outR , errR := config .NewTestConfig ()
351+ cfg .GitHubClientOverride = & github.MockClient {
352+ FindPRByNumberFn : func (number int ) (* github.PullRequest , error ) {
353+ return nil , errors .New ("graphql timeout" )
354+ },
355+ DeleteStackFn : func (stackID string ) error {
356+ deleteCalled = true
357+ return nil
358+ },
359+ }
360+
361+ err := runUnstack (cfg , & unstackOptions {})
362+ output := collectOutput (cfg , outR , errR )
363+
364+ assert .ErrorIs (t , err , ErrAPIFailure )
365+ assert .False (t , deleteCalled , "DeleteStack should not be called if preflight fails" )
366+ assert .Contains (t , output , "failed to check pull request states before unstack" )
367+ assert .NotContains (t , output , "Stack removed from local tracking" )
368+
369+ sf , loadErr := stack .Load (gitDir )
370+ require .NoError (t , loadErr )
371+ require .Len (t , sf .Stacks , 1 )
372+ }
373+
374+ func TestUnstack_API422_ShowsInformativeErrorAndStopsLocalDeletion (t * testing.T ) {
375+ gitDir := t .TempDir ()
376+ restore := git .SetOps (& git.MockOps {
377+ GitDirFn : func () (string , error ) { return gitDir , nil },
378+ CurrentBranchFn : func () (string , error ) { return "b1" , nil },
379+ })
380+ defer restore ()
381+
382+ writeStackFile (t , gitDir , stack.Stack {
383+ ID : "99" ,
384+ Trunk : stack.BranchRef {Branch : "main" },
385+ Branches : []stack.BranchRef {
386+ {Branch : "b1" , PullRequest : & stack.PullRequestRef {Number : 101 }},
387+ {Branch : "b2" , PullRequest : & stack.PullRequestRef {Number : 102 }},
388+ },
389+ })
390+
391+ cfg , outR , errR := config .NewTestConfig ()
392+ cfg .GitHubClientOverride = & github.MockClient {
393+ FindPRByNumberFn : func (number int ) (* github.PullRequest , error ) {
394+ return & github.PullRequest {Number : number , State : "OPEN" }, nil
395+ },
396+ DeleteStackFn : func (stackID string ) error {
397+ return & api.HTTPError {StatusCode : 422 , Message : "some pull requests cannot be removed from stack" }
398+ },
399+ }
400+ err := runUnstack (cfg , & unstackOptions {})
401+ output := collectOutput (cfg , outR , errR )
402+
403+ assert .ErrorIs (t , err , ErrAPIFailure )
404+ assert .Contains (t , output , "Cannot delete stack on GitHub" )
405+ assert .Contains (t , output , "cannot be removed" )
406+ assert .NotContains (t , output , "Stack removed from local tracking" )
407+
408+ sf , loadErr := stack .Load (gitDir )
409+ require .NoError (t , loadErr )
410+ require .Len (t , sf .Stacks , 1 )
411+ }
0 commit comments