Skip to content

Commit f243780

Browse files
committed
Fix finish command to install dependencies after merge Fixes #85
1 parent 470a112 commit f243780

File tree

2 files changed

+234
-1
lines changed

2 files changed

+234
-1
lines changed

src/commands/finish.test.ts

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ vi.mock('../lib/providers/NeonProvider.js')
3232
vi.mock('../lib/EnvironmentManager.js')
3333
vi.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
3643
vi.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,

src/commands/finish.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { EnvironmentManager } from '../lib/EnvironmentManager.js'
1414
import { CLIIsolationManager } from '../lib/CLIIsolationManager.js'
1515
import { findMainWorktreePath } from '../utils/git.js'
1616
import { loadEnvIntoProcess } from '../utils/env.js'
17+
import { installDependencies } from '../utils/package-manager.js'
1718
import type { FinishOptions, GitWorktree, CommitOptions, MergeOptions, PullRequest } from '../types/index.js'
1819
import type { ResourceCleanupOptions, CleanupResult } from '../types/cleanup.js'
1920
import type { ParsedInput } from './start.js'
@@ -500,7 +501,16 @@ export class FinishCommand {
500501
await this.mergeManager.performFastForwardMerge(worktree.branch, worktree.path, mergeOptions)
501502
logger.success('Fast-forward merge completed successfully')
502503

503-
// Step 5.5: Run post-merge build verification (CLI projects only)
504+
// Step 5.5: Install dependencies in main worktree
505+
if (options.dryRun) {
506+
logger.info('[DRY RUN] Would install dependencies in main worktree')
507+
} else {
508+
logger.info('Installing dependencies in main worktree...')
509+
const mainWorktreePath = await findMainWorktreePath(worktree.path)
510+
await installDependencies(mainWorktreePath, true)
511+
}
512+
513+
// Step 5.6: Run post-merge build verification (CLI projects only)
504514
if (!options.skipBuild) {
505515
await this.runPostMergeBuild(worktree.path, options)
506516
} else {

0 commit comments

Comments
 (0)