@@ -449,7 +449,155 @@ func TestUnstack_ByStackNumber(t *testing.T) {
449449 assert .Equal (t , []string {"b1" , "b2" }, sf .Stacks [0 ].BranchNames ())
450450}
451451
452- func TestUnstack_ByStackNumber_NotTrackedLocally (t * testing.T ) {
452+ func TestUnstack_ByStackNumber_RemoteOnly_Dissolved (t * testing.T ) {
453+ // A stack number that isn't tracked locally is unstacked directly on GitHub
454+ // (remote-first), leaving unrelated local tracking untouched.
455+ gitDir := t .TempDir ()
456+ restore := git .SetOps (& git.MockOps {
457+ GitDirFn : func () (string , error ) { return gitDir , nil },
458+ CurrentBranchFn : func () (string , error ) { return "b1" , nil },
459+ })
460+ defer restore ()
461+
462+ writeStackFile (t , gitDir , stack.Stack {
463+ ID : "42" ,
464+ Number : 42 ,
465+ Trunk : stack.BranchRef {Branch : "main" },
466+ Branches : []stack.BranchRef {{Branch : "b1" }, {Branch : "b2" }},
467+ })
468+
469+ var unstackedNumber int
470+ cfg , outR , errR := config .NewTestConfig ()
471+ cfg .GitHubClientOverride = & github.MockClient {
472+ UnstackFn : func (n int ) (* github.RemoteStack , bool , error ) {
473+ unstackedNumber = n
474+ return nil , true , nil // dissolved
475+ },
476+ }
477+ err := runUnstack (cfg , & unstackOptions {stackNumber : 999 })
478+ output := collectOutput (cfg , outR , errR )
479+
480+ require .NoError (t , err )
481+ assert .Equal (t , 999 , unstackedNumber , "should unstack the requested number on GitHub" )
482+ assert .Contains (t , output , "Stack removed on GitHub" )
483+ // No local tracking was touched.
484+ assert .NotContains (t , output , "Stack removed from local tracking" )
485+
486+ sf , err := stack .Load (gitDir )
487+ require .NoError (t , err )
488+ require .Len (t , sf .Stacks , 1 )
489+ assert .Equal (t , 42 , sf .Stacks [0 ].Number , "the unrelated local stack is left intact" )
490+ }
491+
492+ func TestUnstack_ByStackNumber_RemoteOnly_NotFound (t * testing.T ) {
493+ // With no local tracking to reconcile, a 404 means the targeted stack does
494+ // not exist on GitHub — a hard error, not an idempotent success.
495+ gitDir := t .TempDir ()
496+ restore := git .SetOps (& git.MockOps {
497+ GitDirFn : func () (string , error ) { return gitDir , nil },
498+ CurrentBranchFn : func () (string , error ) { return "b1" , nil },
499+ })
500+ defer restore ()
501+
502+ writeStackFile (t , gitDir , stack.Stack {
503+ ID : "42" ,
504+ Number : 42 ,
505+ Trunk : stack.BranchRef {Branch : "main" },
506+ Branches : []stack.BranchRef {{Branch : "b1" }, {Branch : "b2" }},
507+ })
508+
509+ cfg , outR , errR := config .NewTestConfig ()
510+ cfg .GitHubClientOverride = & github.MockClient {
511+ UnstackFn : func (int ) (* github.RemoteStack , bool , error ) {
512+ return nil , false , & api.HTTPError {StatusCode : 404 , Message : "Not Found" }
513+ },
514+ }
515+ err := runUnstack (cfg , & unstackOptions {stackNumber : 999 })
516+ output := collectOutput (cfg , outR , errR )
517+
518+ assert .ErrorIs (t , err , ErrNotInStack )
519+ assert .Contains (t , output , "stack #999 not found on GitHub" )
520+ assert .NotContains (t , output , "Stack removed" )
521+
522+ sf , err := stack .Load (gitDir )
523+ require .NoError (t , err )
524+ require .Len (t , sf .Stacks , 1 )
525+ }
526+
527+ func TestUnstack_ByStackNumber_RemoteOnly_Partial (t * testing.T ) {
528+ // Some PRs (queued for merge / auto-merge) remain stacked. There is no local
529+ // tracking to keep, so the command reports the outcome and succeeds.
530+ gitDir := t .TempDir ()
531+ restore := git .SetOps (& git.MockOps {
532+ GitDirFn : func () (string , error ) { return gitDir , nil },
533+ CurrentBranchFn : func () (string , error ) { return "b1" , nil },
534+ })
535+ defer restore ()
536+
537+ writeStackFile (t , gitDir , stack.Stack {
538+ ID : "42" ,
539+ Number : 42 ,
540+ Trunk : stack.BranchRef {Branch : "main" },
541+ Branches : []stack.BranchRef {{Branch : "b1" }, {Branch : "b2" }},
542+ })
543+
544+ cfg , outR , errR := config .NewTestConfig ()
545+ cfg .GitHubClientOverride = & github.MockClient {
546+ UnstackFn : func (int ) (* github.RemoteStack , bool , error ) {
547+ return & github.RemoteStack {ID : 555 , Number : 999 , PullRequests : []int {102 }}, false , nil
548+ },
549+ }
550+ err := runUnstack (cfg , & unstackOptions {stackNumber : 999 })
551+ output := collectOutput (cfg , outR , errR )
552+
553+ require .NoError (t , err )
554+ assert .Contains (t , output , "remain stacked on GitHub" )
555+ // No local tracking is involved, so no local-tracking messaging is shown.
556+ assert .NotContains (t , output , "local tracking is unchanged" )
557+ assert .NotContains (t , output , "Stack removed from local tracking" )
558+
559+ sf , err := stack .Load (gitDir )
560+ require .NoError (t , err )
561+ require .Len (t , sf .Stacks , 1 )
562+ }
563+
564+ func TestUnstack_ByStackNumber_RemoteOnly_AllLocked (t * testing.T ) {
565+ // Every PR is queued for merge or has auto-merge enabled; the server rejects
566+ // the unstack with a 422 and the command surfaces the error.
567+ gitDir := t .TempDir ()
568+ restore := git .SetOps (& git.MockOps {
569+ GitDirFn : func () (string , error ) { return gitDir , nil },
570+ CurrentBranchFn : func () (string , error ) { return "b1" , nil },
571+ })
572+ defer restore ()
573+
574+ writeStackFile (t , gitDir , stack.Stack {
575+ ID : "42" ,
576+ Number : 42 ,
577+ Trunk : stack.BranchRef {Branch : "main" },
578+ Branches : []stack.BranchRef {{Branch : "b1" }, {Branch : "b2" }},
579+ })
580+
581+ cfg , outR , errR := config .NewTestConfig ()
582+ cfg .GitHubClientOverride = & github.MockClient {
583+ UnstackFn : func (int ) (* github.RemoteStack , bool , error ) {
584+ return nil , false , & api.HTTPError {StatusCode : 422 , Message : "all pull requests are queued for merge or have auto-merge enabled" }
585+ },
586+ }
587+ err := runUnstack (cfg , & unstackOptions {stackNumber : 999 })
588+ output := collectOutput (cfg , outR , errR )
589+
590+ assert .ErrorIs (t , err , ErrInvalidArgs )
591+ assert .Contains (t , output , "Unstacking not allowed" )
592+
593+ sf , err := stack .Load (gitDir )
594+ require .NoError (t , err )
595+ require .Len (t , sf .Stacks , 1 )
596+ }
597+
598+ func TestUnstack_ByStackNumber_NotTracked_LocalFlag (t * testing.T ) {
599+ // --local never contacts GitHub. Targeting a number that isn't tracked
600+ // locally with --local is an error: there is nothing to remove locally.
453601 gitDir := t .TempDir ()
454602 restore := git .SetOps (& git.MockOps {
455603 GitDirFn : func () (string , error ) { return gitDir , nil },
@@ -472,11 +620,11 @@ func TestUnstack_ByStackNumber_NotTrackedLocally(t *testing.T) {
472620 return nil , true , nil
473621 },
474622 }
475- err := runUnstack (cfg , & unstackOptions {stackNumber : 999 })
623+ err := runUnstack (cfg , & unstackOptions {stackNumber : 999 , local : true })
476624 output := collectOutput (cfg , outR , errR )
477625
478626 assert .ErrorIs (t , err , ErrNotInStack )
479- assert .False (t , unstackCalled , "should not unstack when the number isn't tracked locally " )
627+ assert .False (t , unstackCalled , "--local must never contact GitHub " )
480628 assert .Contains (t , output , "stack #999 is not tracked locally" )
481629
482630 sf , err := stack .Load (gitDir )
0 commit comments