@@ -32,6 +32,13 @@ vi.mock('../lib/providers/NeonProvider.js')
3232vi . mock ( '../lib/EnvironmentManager.js' )
3333vi . mock ( '../utils/env.js' )
3434
35+ // Mock package-manager utilities
36+ vi . mock ( '../utils/package-manager.js' , ( ) => ( {
37+ installDependencies : vi . fn ( ) . mockResolvedValue ( undefined ) ,
38+ detectPackageManager : vi . fn ( ) . mockResolvedValue ( 'pnpm' ) ,
39+ runScript : vi . fn ( ) . mockResolvedValue ( undefined ) ,
40+ } ) )
41+
3542// Mock git utils module for pushBranchToRemote and findMainWorktreePath
3643vi . mock ( '../utils/git.js' , async ( ) => {
3744 const actual = await vi . importActual < typeof import ( '../utils/git.js' ) > ( '../utils/git.js' )
@@ -2701,6 +2708,222 @@ describe('FinishCommand', () => {
27012708 } )
27022709 } )
27032710
2711+ describe ( 'post-merge dependency installation' , ( ) => {
2712+ const mockIssue : Issue = {
2713+ number : 123 ,
2714+ title : 'Test issue' ,
2715+ body : 'Test body' ,
2716+ state : 'open' ,
2717+ labels : [ ] ,
2718+ assignees : [ ] ,
2719+ url : 'https://github.com/test/repo/issues/123' ,
2720+ }
2721+
2722+ const mockWorktree : GitWorktree = {
2723+ path : '/test/worktree' ,
2724+ branch : 'feat/issue-123' ,
2725+ commit : 'abc123' ,
2726+ isPR : false ,
2727+ issueNumber : 123 ,
2728+ }
2729+
2730+ beforeEach ( async ( ) => {
2731+ // Import the mocked functions
2732+ const { installDependencies } = await import ( '../utils/package-manager.js' )
2733+ const { findMainWorktreePath } = await import ( '../utils/git.js' )
2734+
2735+ // Reset mocks
2736+ vi . mocked ( installDependencies ) . mockClear ( )
2737+ vi . mocked ( findMainWorktreePath ) . mockClear ( )
2738+
2739+ // Setup default mocks
2740+ vi . mocked ( installDependencies ) . mockResolvedValue ( undefined )
2741+ vi . mocked ( findMainWorktreePath ) . mockResolvedValue ( '/test/main' )
2742+
2743+ // Mock successful issue fetch
2744+ vi . mocked ( mockGitHubService . fetchIssue ) . mockResolvedValue ( mockIssue )
2745+
2746+ // Mock successful worktree finding
2747+ vi . mocked ( mockGitWorktreeManager . findWorktreeForIssue ) . mockResolvedValue ( mockWorktree )
2748+
2749+ // Mock IdentifierParser to detect as issue
2750+ vi . mocked ( mockIdentifierParser . parseForPatternDetection ) . mockResolvedValue ( {
2751+ type : 'issue' ,
2752+ number : 123 ,
2753+ originalInput : '123' ,
2754+ } )
2755+ } )
2756+
2757+ it ( 'should install dependencies in main worktree after merge' , async ( ) => {
2758+ const { installDependencies } = await import ( '../utils/package-manager.js' )
2759+ const { findMainWorktreePath } = await import ( '../utils/git.js' )
2760+
2761+ await command . execute ( {
2762+ identifier : '123' ,
2763+ options : { } ,
2764+ } )
2765+
2766+ // Verify findMainWorktreePath was called
2767+ expect ( findMainWorktreePath ) . toHaveBeenCalledWith ( mockWorktree . path )
2768+
2769+ // Verify installDependencies was called with main worktree path and frozen=true
2770+ expect ( installDependencies ) . toHaveBeenCalledWith ( '/test/main' , true )
2771+ } )
2772+
2773+ it ( 'should install dependencies before running post-merge build' , async ( ) => {
2774+ const { installDependencies } = await import ( '../utils/package-manager.js' )
2775+ const executionOrder : string [ ] = [ ]
2776+
2777+ vi . mocked ( installDependencies ) . mockImplementation ( async ( ) => {
2778+ executionOrder . push ( 'install' )
2779+ } )
2780+
2781+ vi . mocked ( mockBuildRunner . runBuild ) . mockImplementation ( async ( ) => {
2782+ executionOrder . push ( 'build' )
2783+ return {
2784+ success : true ,
2785+ skipped : false ,
2786+ }
2787+ } )
2788+
2789+ await command . execute ( {
2790+ identifier : '123' ,
2791+ options : { } ,
2792+ } )
2793+
2794+ // Verify install happens before build
2795+ const installIndex = executionOrder . indexOf ( 'install' )
2796+ const buildIndex = executionOrder . indexOf ( 'build' )
2797+
2798+ expect ( installIndex ) . toBeGreaterThanOrEqual ( 0 )
2799+ expect ( buildIndex ) . toBeGreaterThanOrEqual ( 0 )
2800+ expect ( installIndex ) . toBeLessThan ( buildIndex )
2801+ } )
2802+
2803+ it ( 'should skip dependency installation in dry-run mode' , async ( ) => {
2804+ const { installDependencies } = await import ( '../utils/package-manager.js' )
2805+
2806+ await command . execute ( {
2807+ identifier : '123' ,
2808+ options : {
2809+ dryRun : true ,
2810+ } ,
2811+ } )
2812+
2813+ // Verify installDependencies was NOT called in dry-run mode
2814+ expect ( installDependencies ) . not . toHaveBeenCalled ( )
2815+ } )
2816+
2817+ it ( 'should fail finish command if dependency installation fails' , async ( ) => {
2818+ const { installDependencies } = await import ( '../utils/package-manager.js' )
2819+
2820+ // Mock installation failure
2821+ vi . mocked ( installDependencies ) . mockRejectedValue (
2822+ new Error ( 'Failed to install dependencies: Lockfile is out of date' )
2823+ )
2824+
2825+ await expect (
2826+ command . execute ( {
2827+ identifier : '123' ,
2828+ options : { } ,
2829+ } )
2830+ ) . rejects . toThrow ( 'Failed to install dependencies' )
2831+
2832+ // Verify cleanup was NOT called (error should prevent it)
2833+ expect ( mockResourceCleanup . cleanupWorktree ) . not . toHaveBeenCalled ( )
2834+ } )
2835+
2836+ it ( 'should not install dependencies for PR workflow (open PR)' , async ( ) => {
2837+ const { installDependencies } = await import ( '../utils/package-manager.js' )
2838+
2839+ const mockPR : PullRequest = {
2840+ number : 456 ,
2841+ title : 'Test PR' ,
2842+ body : 'Test body' ,
2843+ state : 'open' ,
2844+ headRef : 'feat/pr-456' ,
2845+ baseRef : 'main' ,
2846+ url : 'https://github.com/test/repo/pull/456' ,
2847+ labels : [ ] ,
2848+ assignees : [ ] ,
2849+ draft : false ,
2850+ mergeable : true ,
2851+ }
2852+
2853+ // Mock PR detection
2854+ vi . mocked ( mockIdentifierParser . parseForPatternDetection ) . mockResolvedValue ( {
2855+ type : 'pr' ,
2856+ number : 456 ,
2857+ originalInput : '456' ,
2858+ } )
2859+
2860+ vi . mocked ( mockGitHubService . fetchPR ) . mockResolvedValue ( mockPR )
2861+
2862+ const prWorktree : GitWorktree = {
2863+ path : '/test/worktree-pr' ,
2864+ branch : 'feat/pr-456' ,
2865+ commit : 'def456' ,
2866+ isPR : true ,
2867+ prNumber : 456 ,
2868+ }
2869+
2870+ vi . mocked ( mockGitWorktreeManager . findWorktreeForPR ) . mockResolvedValue ( prWorktree )
2871+
2872+ await command . execute ( {
2873+ identifier : '456' ,
2874+ options : { } ,
2875+ } )
2876+
2877+ // PRs don't merge to main, so no installation should occur
2878+ expect ( installDependencies ) . not . toHaveBeenCalled ( )
2879+ } )
2880+
2881+ it ( 'should not install dependencies for PR workflow (closed PR)' , async ( ) => {
2882+ const { installDependencies } = await import ( '../utils/package-manager.js' )
2883+
2884+ const mockPR : PullRequest = {
2885+ number : 456 ,
2886+ title : 'Test PR' ,
2887+ body : 'Test body' ,
2888+ state : 'closed' ,
2889+ headRef : 'feat/pr-456' ,
2890+ baseRef : 'main' ,
2891+ url : 'https://github.com/test/repo/pull/456' ,
2892+ labels : [ ] ,
2893+ assignees : [ ] ,
2894+ draft : false ,
2895+ mergeable : false ,
2896+ }
2897+
2898+ // Mock PR detection
2899+ vi . mocked ( mockIdentifierParser . parseForPatternDetection ) . mockResolvedValue ( {
2900+ type : 'pr' ,
2901+ number : 456 ,
2902+ originalInput : '456' ,
2903+ } )
2904+
2905+ vi . mocked ( mockGitHubService . fetchPR ) . mockResolvedValue ( mockPR )
2906+
2907+ const prWorktree : GitWorktree = {
2908+ path : '/test/worktree-pr' ,
2909+ branch : 'feat/pr-456' ,
2910+ commit : 'def456' ,
2911+ isPR : true ,
2912+ prNumber : 456 ,
2913+ }
2914+
2915+ vi . mocked ( mockGitWorktreeManager . findWorktreeForPR ) . mockResolvedValue ( prWorktree )
2916+
2917+ await command . execute ( {
2918+ identifier : '456' ,
2919+ options : { } ,
2920+ } )
2921+
2922+ // Closed PRs go to cleanup only, no installation
2923+ expect ( installDependencies ) . not . toHaveBeenCalled ( )
2924+ } )
2925+ } )
2926+
27042927 describe ( 'post-merge build verification' , ( ) => {
27052928 const mockIssue : Issue = {
27062929 number : 123 ,
0 commit comments