@@ -24,6 +24,11 @@ vi.mock('../lib/GitWorktreeManager.js')
2424vi . mock ( '../lib/EnvironmentManager.js' )
2525vi . mock ( '../lib/ClaudeContextManager.js' )
2626
27+ // Mock branchExists utility
28+ vi . mock ( '../utils/git.js' , ( ) => ( {
29+ branchExists : vi . fn ( ) . mockResolvedValue ( false ) ,
30+ } ) )
31+
2732// Mock the logger to prevent console output during tests
2833vi . mock ( '../utils/logger.js' , ( ) => ( {
2934 logger : {
@@ -533,5 +538,195 @@ describe('StartCommand', () => {
533538 ) . not . toHaveBeenCalled ( )
534539 } )
535540 } )
541+
542+ describe ( 'GitHub state validation' , ( ) => {
543+ it ( 'should call validateIssueState for issues' , async ( ) => {
544+ const mockIssue = {
545+ number : 123 ,
546+ title : 'Test Issue' ,
547+ body : 'Issue body' ,
548+ state : 'open' as const ,
549+ labels : [ ] ,
550+ assignees : [ ] ,
551+ url : 'https://github.com/test/repo/issues/123' ,
552+ }
553+
554+ vi . mocked ( mockGitHubService . detectInputType ) . mockResolvedValue ( {
555+ type : 'issue' ,
556+ number : 123 ,
557+ rawInput : '123' ,
558+ } )
559+ vi . mocked ( mockGitHubService . fetchIssue ) . mockResolvedValue ( mockIssue )
560+ vi . mocked ( mockGitHubService . validateIssueState ) . mockResolvedValue ( )
561+
562+ await command . execute ( {
563+ identifier : '123' ,
564+ options : { } ,
565+ } )
566+
567+ expect ( mockGitHubService . fetchIssue ) . toHaveBeenCalledWith ( 123 )
568+ expect ( mockGitHubService . validateIssueState ) . toHaveBeenCalledWith ( mockIssue )
569+ } )
570+
571+ it ( 'should call validatePRState for PRs' , async ( ) => {
572+ const mockPR = {
573+ number : 456 ,
574+ title : 'Test PR' ,
575+ body : 'PR body' ,
576+ state : 'open' as const ,
577+ branch : 'feature-branch' ,
578+ baseBranch : 'main' ,
579+ url : 'https://github.com/test/repo/pull/456' ,
580+ isDraft : false ,
581+ }
582+
583+ vi . mocked ( mockGitHubService . fetchPR ) . mockResolvedValue ( mockPR )
584+ vi . mocked ( mockGitHubService . validatePRState ) . mockResolvedValue ( )
585+
586+ await command . execute ( {
587+ identifier : 'pr-456' ,
588+ options : { } ,
589+ } )
590+
591+ expect ( mockGitHubService . fetchPR ) . toHaveBeenCalledWith ( 456 )
592+ expect ( mockGitHubService . validatePRState ) . toHaveBeenCalledWith ( mockPR )
593+ } )
594+
595+ it ( 'should throw when validateIssueState rejects' , async ( ) => {
596+ vi . mocked ( mockGitHubService . detectInputType ) . mockResolvedValue ( {
597+ type : 'issue' ,
598+ number : 123 ,
599+ rawInput : '123' ,
600+ } )
601+ vi . mocked ( mockGitHubService . fetchIssue ) . mockResolvedValue ( {
602+ number : 123 ,
603+ title : 'Closed Issue' ,
604+ body : '' ,
605+ state : 'closed' ,
606+ labels : [ ] ,
607+ assignees : [ ] ,
608+ url : 'https://github.com/test/repo/issues/123' ,
609+ } )
610+ vi . mocked ( mockGitHubService . validateIssueState ) . mockRejectedValue (
611+ new Error ( 'User cancelled due to closed issue' )
612+ )
613+
614+ await expect (
615+ command . execute ( {
616+ identifier : '123' ,
617+ options : { } ,
618+ } )
619+ ) . rejects . toThrow ( 'User cancelled due to closed issue' )
620+ } )
621+
622+ it ( 'should throw when validatePRState rejects' , async ( ) => {
623+ const mockPR = {
624+ number : 456 ,
625+ title : 'Merged PR' ,
626+ body : '' ,
627+ state : 'closed' as const ,
628+ branch : 'feature' ,
629+ baseBranch : 'main' ,
630+ url : 'https://github.com/test/repo/pull/456' ,
631+ isDraft : false ,
632+ }
633+
634+ vi . mocked ( mockGitHubService . fetchPR ) . mockResolvedValue ( mockPR )
635+ vi . mocked ( mockGitHubService . validatePRState ) . mockRejectedValue (
636+ new Error ( 'User cancelled due to merged PR' )
637+ )
638+
639+ await expect (
640+ command . execute ( {
641+ identifier : 'pr/456' ,
642+ options : { } ,
643+ } )
644+ ) . rejects . toThrow ( 'User cancelled due to merged PR' )
645+ } )
646+ } )
647+
648+ describe ( 'branch existence checking' , ( ) => {
649+ it ( 'should check if branch exists before creation' , async ( ) => {
650+ const { branchExists } = await import ( '../utils/git.js' )
651+
652+ vi . mocked ( branchExists ) . mockResolvedValue ( true )
653+
654+ await expect (
655+ command . execute ( {
656+ identifier : 'existing-branch' ,
657+ options : { } ,
658+ } )
659+ ) . rejects . toThrow ( "Branch 'existing-branch' already exists" )
660+
661+ expect ( branchExists ) . toHaveBeenCalledWith ( 'existing-branch' )
662+ } )
663+
664+ it ( 'should not throw when branch does not exist' , async ( ) => {
665+ const { branchExists } = await import ( '../utils/git.js' )
666+
667+ vi . mocked ( branchExists ) . mockResolvedValue ( false )
668+
669+ await expect (
670+ command . execute ( {
671+ identifier : 'new-branch' ,
672+ options : { } ,
673+ } )
674+ ) . resolves . not . toThrow ( )
675+
676+ expect ( branchExists ) . toHaveBeenCalledWith ( 'new-branch' )
677+ } )
678+
679+ it ( 'should not check branch existence for PRs' , async ( ) => {
680+ const mockPR = {
681+ number : 123 ,
682+ title : 'Test PR' ,
683+ body : '' ,
684+ state : 'open' as const ,
685+ branch : 'feature-branch' ,
686+ baseBranch : 'main' ,
687+ url : 'https://github.com/test/repo/pull/123' ,
688+ isDraft : false ,
689+ }
690+
691+ vi . mocked ( mockGitHubService . fetchPR ) . mockResolvedValue ( mockPR )
692+ vi . mocked ( mockGitHubService . validatePRState ) . mockResolvedValue ( )
693+
694+ await command . execute ( {
695+ identifier : 'pr/123' ,
696+ options : { } ,
697+ } )
698+
699+ // branchExists should not be called for PRs in validateInput
700+ // (it might be called in HatchboxManager but that's a different check)
701+ } )
702+
703+ it ( 'should not check branch existence for issues in validateInput' , async ( ) => {
704+ const mockIssue = {
705+ number : 123 ,
706+ title : 'Test Issue' ,
707+ body : '' ,
708+ state : 'open' as const ,
709+ labels : [ ] ,
710+ assignees : [ ] ,
711+ url : 'https://github.com/test/repo/issues/123' ,
712+ }
713+
714+ vi . mocked ( mockGitHubService . detectInputType ) . mockResolvedValue ( {
715+ type : 'issue' ,
716+ number : 123 ,
717+ rawInput : '123' ,
718+ } )
719+ vi . mocked ( mockGitHubService . fetchIssue ) . mockResolvedValue ( mockIssue )
720+ vi . mocked ( mockGitHubService . validateIssueState ) . mockResolvedValue ( )
721+
722+ await command . execute ( {
723+ identifier : '123' ,
724+ options : { } ,
725+ } )
726+
727+ // branchExists is only called for branch-type inputs in validateInput
728+ // Issues get their branch checked in HatchboxManager.createWorktree
729+ } )
730+ } )
536731 } )
537732} )
0 commit comments