@@ -6,6 +6,8 @@ import { ValidationRunner } from '../lib/ValidationRunner.js'
66import { CommitManager } from '../lib/CommitManager.js'
77import { MergeManager } from '../lib/MergeManager.js'
88import { IdentifierParser } from '../utils/IdentifierParser.js'
9+ import { ResourceCleanup } from '../lib/ResourceCleanup.js'
10+ import { ProcessManager } from '../lib/process/ProcessManager.js'
911import type { Issue , PullRequest } from '../types/index.js'
1012import type { GitWorktree } from '../types/worktree.js'
1113
@@ -16,6 +18,8 @@ vi.mock('../lib/ValidationRunner.js')
1618vi . mock ( '../lib/CommitManager.js' )
1719vi . mock ( '../lib/MergeManager.js' )
1820vi . mock ( '../utils/IdentifierParser.js' )
21+ vi . mock ( '../lib/ResourceCleanup.js' )
22+ vi . mock ( '../lib/process/ProcessManager.js' )
1923
2024// Mock the logger to prevent console output during tests
2125vi . mock ( '../utils/logger.js' , ( ) => ( {
@@ -36,6 +40,8 @@ describe('FinishCommand', () => {
3640 let mockCommitManager : CommitManager
3741 let mockMergeManager : MergeManager
3842 let mockIdentifierParser : IdentifierParser
43+ let mockResourceCleanup : ResourceCleanup
44+ let mockProcessManager : ProcessManager
3945
4046 beforeEach ( ( ) => {
4147 mockGitHubService = new GitHubService ( )
@@ -44,6 +50,12 @@ describe('FinishCommand', () => {
4450 mockCommitManager = new CommitManager ( )
4551 mockMergeManager = new MergeManager ( )
4652 mockIdentifierParser = new IdentifierParser ( mockGitWorktreeManager )
53+ mockProcessManager = new ProcessManager ( )
54+ mockResourceCleanup = new ResourceCleanup (
55+ mockGitWorktreeManager ,
56+ mockProcessManager ,
57+ undefined
58+ )
4759
4860 // Mock ValidationRunner.runValidations to always succeed by default
4961 vi . mocked ( mockValidationRunner . runValidations ) . mockResolvedValue ( {
@@ -69,6 +81,15 @@ describe('FinishCommand', () => {
6981 vi . mocked ( mockMergeManager . rebaseOnMain ) . mockResolvedValue ( undefined )
7082 vi . mocked ( mockMergeManager . performFastForwardMerge ) . mockResolvedValue ( undefined )
7183
84+ // Mock ResourceCleanup.cleanupWorktree to succeed by default
85+ vi . mocked ( mockResourceCleanup . cleanupWorktree ) . mockResolvedValue ( {
86+ identifier : 'test' ,
87+ success : true ,
88+ operations : [ ] ,
89+ errors : [ ] ,
90+ rollbackRequired : false ,
91+ } )
92+
7293 // Mock GitWorktreeManager specific finding methods (used by IdentifierParser and FinishCommand)
7394 vi . mocked ( mockGitWorktreeManager . findWorktreeForPR ) . mockResolvedValue ( null )
7495 vi . mocked ( mockGitWorktreeManager . findWorktreeForIssue ) . mockResolvedValue ( null )
@@ -80,7 +101,8 @@ describe('FinishCommand', () => {
80101 mockValidationRunner ,
81102 mockCommitManager ,
82103 mockMergeManager ,
83- mockIdentifierParser
104+ mockIdentifierParser ,
105+ mockResourceCleanup
84106 )
85107 } )
86108
@@ -124,6 +146,25 @@ describe('FinishCommand', () => {
124146 expect ( cmd [ 'mergeManager' ] ) . toBe ( customManager )
125147 } )
126148
149+ it ( 'should accept ResourceCleanup via constructor' , ( ) => {
150+ const customCleanup = new ResourceCleanup (
151+ mockGitWorktreeManager ,
152+ mockProcessManager ,
153+ undefined
154+ )
155+ const cmd = new FinishCommand (
156+ undefined ,
157+ undefined ,
158+ undefined ,
159+ undefined ,
160+ undefined ,
161+ undefined ,
162+ customCleanup
163+ )
164+
165+ expect ( cmd [ 'resourceCleanup' ] ) . toBe ( customCleanup )
166+ } )
167+
127168 it ( 'should create default instances when not provided' , ( ) => {
128169 const cmd = new FinishCommand ( )
129170
@@ -133,6 +174,7 @@ describe('FinishCommand', () => {
133174 expect ( cmd [ 'commitManager' ] ) . toBeInstanceOf ( CommitManager )
134175 expect ( cmd [ 'mergeManager' ] ) . toBeInstanceOf ( MergeManager )
135176 expect ( cmd [ 'identifierParser' ] ) . toBeInstanceOf ( IdentifierParser )
177+ expect ( cmd [ 'resourceCleanup' ] ) . toBeInstanceOf ( ResourceCleanup )
136178 } )
137179 } )
138180
@@ -2192,5 +2234,284 @@ describe('FinishCommand', () => {
21922234 expect ( logger . error ) . toHaveBeenCalledWith ( 'An unknown error occurred' )
21932235 } )
21942236 } )
2237+
2238+ describe ( 'post-merge cleanup integration' , ( ) => {
2239+ const mockIssue : Issue = {
2240+ number : 123 ,
2241+ title : 'Test issue' ,
2242+ body : 'Test body' ,
2243+ state : 'open' ,
2244+ labels : [ ] ,
2245+ assignees : [ ] ,
2246+ url : 'https://github.com/test/repo/issues/123' ,
2247+ }
2248+
2249+ const mockWorktree : GitWorktree = {
2250+ path : '/test/worktree' ,
2251+ branch : 'feat/issue-123' ,
2252+ commit : 'abc123' ,
2253+ isPR : false ,
2254+ issueNumber : 123 ,
2255+ }
2256+
2257+ beforeEach ( ( ) => {
2258+ // Mock successful issue fetch
2259+ vi . mocked ( mockGitHubService . fetchIssue ) . mockResolvedValue ( mockIssue )
2260+
2261+ // Mock successful worktree finding
2262+ vi . mocked ( mockGitWorktreeManager . findWorktreeForIssue ) . mockResolvedValue ( mockWorktree )
2263+
2264+ // Mock IdentifierParser to detect as issue
2265+ vi . mocked ( mockIdentifierParser . parseForPatternDetection ) . mockResolvedValue ( {
2266+ type : 'issue' ,
2267+ number : 123 ,
2268+ originalInput : '123' ,
2269+ } )
2270+ } )
2271+
2272+ it ( 'should call ResourceCleanup.cleanupWorktree after successful merge' , async ( ) => {
2273+ await command . execute ( {
2274+ identifier : '123' ,
2275+ options : { } ,
2276+ } )
2277+
2278+ // Verify cleanup was called
2279+ expect ( mockResourceCleanup . cleanupWorktree ) . toHaveBeenCalled ( )
2280+
2281+ // Verify cleanup was called after merge
2282+ const mergeCallOrder = vi . mocked ( mockMergeManager . performFastForwardMerge ) . mock . invocationCallOrder [ 0 ]
2283+ const cleanupCallOrder = vi . mocked ( mockResourceCleanup . cleanupWorktree ) . mock . invocationCallOrder [ 0 ]
2284+ expect ( cleanupCallOrder ) . toBeGreaterThan ( mergeCallOrder )
2285+ } )
2286+
2287+ it ( 'should pass correct ParsedInput to cleanupWorktree (without autoDetected field)' , async ( ) => {
2288+ await command . execute ( {
2289+ identifier : '123' ,
2290+ options : { } ,
2291+ } )
2292+
2293+ // Verify cleanupWorktree was called with ParsedInput (no autoDetected field)
2294+ expect ( mockResourceCleanup . cleanupWorktree ) . toHaveBeenCalledWith (
2295+ expect . objectContaining ( {
2296+ type : 'issue' ,
2297+ number : 123 ,
2298+ originalInput : '123' ,
2299+ } ) ,
2300+ expect . any ( Object )
2301+ )
2302+
2303+ // Verify autoDetected field is NOT present
2304+ const callArgs = vi . mocked ( mockResourceCleanup . cleanupWorktree ) . mock . calls [ 0 ]
2305+ expect ( callArgs [ 0 ] ) . not . toHaveProperty ( 'autoDetected' )
2306+ } )
2307+
2308+ it ( 'should pass correct cleanup options (deleteBranch: true, keepDatabase: false)' , async ( ) => {
2309+ await command . execute ( {
2310+ identifier : '123' ,
2311+ options : { } ,
2312+ } )
2313+
2314+ expect ( mockResourceCleanup . cleanupWorktree ) . toHaveBeenCalledWith (
2315+ expect . any ( Object ) ,
2316+ expect . objectContaining ( {
2317+ dryRun : false ,
2318+ deleteBranch : true ,
2319+ keepDatabase : false ,
2320+ force : false ,
2321+ } )
2322+ )
2323+ } )
2324+
2325+ it ( 'should pass dryRun flag from options to cleanup' , async ( ) => {
2326+ await command . execute ( {
2327+ identifier : '123' ,
2328+ options : { dryRun : true } ,
2329+ } )
2330+
2331+ expect ( mockResourceCleanup . cleanupWorktree ) . toHaveBeenCalledWith (
2332+ expect . any ( Object ) ,
2333+ expect . objectContaining ( {
2334+ dryRun : true ,
2335+ } )
2336+ )
2337+ } )
2338+
2339+ it ( 'should pass force flag from options to cleanup' , async ( ) => {
2340+ await command . execute ( {
2341+ identifier : '123' ,
2342+ options : { force : true } ,
2343+ } )
2344+
2345+ expect ( mockResourceCleanup . cleanupWorktree ) . toHaveBeenCalledWith (
2346+ expect . any ( Object ) ,
2347+ expect . objectContaining ( {
2348+ force : true ,
2349+ } )
2350+ )
2351+ } )
2352+
2353+ it ( 'should handle partial cleanup failures gracefully without throwing' , async ( ) => {
2354+ // Mock partial cleanup failure
2355+ vi . mocked ( mockResourceCleanup . cleanupWorktree ) . mockResolvedValue ( {
2356+ identifier : '123' ,
2357+ success : false ,
2358+ operations : [
2359+ { type : 'dev-server' , success : true , message : 'Dev server terminated' } ,
2360+ { type : 'worktree' , success : false , message : 'Failed to remove worktree' , error : 'Worktree is locked' } ,
2361+ ] ,
2362+ errors : [ new Error ( 'Worktree is locked' ) ] ,
2363+ rollbackRequired : false ,
2364+ } )
2365+
2366+ // Should not throw even though cleanup failed
2367+ await expect (
2368+ command . execute ( {
2369+ identifier : '123' ,
2370+ options : { } ,
2371+ } )
2372+ ) . resolves . not . toThrow ( )
2373+
2374+ // Verify cleanup was still attempted
2375+ expect ( mockResourceCleanup . cleanupWorktree ) . toHaveBeenCalled ( )
2376+ } )
2377+
2378+ it ( 'should handle cleanup exceptions gracefully without throwing' , async ( ) => {
2379+ // Mock cleanup throwing an error
2380+ vi . mocked ( mockResourceCleanup . cleanupWorktree ) . mockRejectedValue (
2381+ new Error ( 'Unexpected cleanup error' )
2382+ )
2383+
2384+ // Should not throw even though cleanup threw an error
2385+ await expect (
2386+ command . execute ( {
2387+ identifier : '123' ,
2388+ options : { } ,
2389+ } )
2390+ ) . resolves . not . toThrow ( )
2391+
2392+ // Verify cleanup was attempted
2393+ expect ( mockResourceCleanup . cleanupWorktree ) . toHaveBeenCalled ( )
2394+ } )
2395+
2396+ it ( 'should NOT execute cleanup if validation fails' , async ( ) => {
2397+ // Mock validation failure
2398+ vi . mocked ( mockValidationRunner . runValidations ) . mockRejectedValue (
2399+ new Error ( 'Validation failed' )
2400+ )
2401+
2402+ await expect (
2403+ command . execute ( {
2404+ identifier : '123' ,
2405+ options : { } ,
2406+ } )
2407+ ) . rejects . toThrow ( 'Validation failed' )
2408+
2409+ // Verify cleanup was NOT called
2410+ expect ( mockResourceCleanup . cleanupWorktree ) . not . toHaveBeenCalled ( )
2411+ } )
2412+
2413+ it ( 'should NOT execute cleanup if rebase fails' , async ( ) => {
2414+ // Mock rebase failure
2415+ vi . mocked ( mockMergeManager . rebaseOnMain ) . mockRejectedValue (
2416+ new Error ( 'Rebase failed' )
2417+ )
2418+
2419+ await expect (
2420+ command . execute ( {
2421+ identifier : '123' ,
2422+ options : { } ,
2423+ } )
2424+ ) . rejects . toThrow ( 'Rebase failed' )
2425+
2426+ // Verify cleanup was NOT called
2427+ expect ( mockResourceCleanup . cleanupWorktree ) . not . toHaveBeenCalled ( )
2428+ } )
2429+
2430+ it ( 'should NOT execute cleanup if merge fails' , async ( ) => {
2431+ // Mock merge failure
2432+ vi . mocked ( mockMergeManager . performFastForwardMerge ) . mockRejectedValue (
2433+ new Error ( 'Merge failed' )
2434+ )
2435+
2436+ await expect (
2437+ command . execute ( {
2438+ identifier : '123' ,
2439+ options : { } ,
2440+ } )
2441+ ) . rejects . toThrow ( 'Merge failed' )
2442+
2443+ // Verify cleanup was NOT called
2444+ expect ( mockResourceCleanup . cleanupWorktree ) . not . toHaveBeenCalled ( )
2445+ } )
2446+
2447+ it ( 'should work with PR identifiers' , async ( ) => {
2448+ const mockPR : PullRequest = {
2449+ number : 456 ,
2450+ title : 'Test PR' ,
2451+ body : 'Test body' ,
2452+ state : 'open' ,
2453+ branch : 'feat/test' ,
2454+ baseBranch : 'main' ,
2455+ url : 'https://github.com/test/repo/pull/456' ,
2456+ isDraft : false ,
2457+ }
2458+
2459+ const mockPRWorktree : GitWorktree = {
2460+ path : '/test/worktree/feat-test_pr_456' ,
2461+ branch : 'feat/test' ,
2462+ commit : 'def456' ,
2463+ isPR : true ,
2464+ prNumber : 456 ,
2465+ }
2466+
2467+ vi . mocked ( mockGitHubService . fetchPR ) . mockResolvedValue ( mockPR )
2468+ vi . mocked ( mockGitWorktreeManager . findWorktreeForPR ) . mockResolvedValue ( mockPRWorktree )
2469+
2470+ await command . execute ( {
2471+ identifier : 'pr/456' ,
2472+ options : { } ,
2473+ } )
2474+
2475+ // Verify cleanup was called with PR type
2476+ expect ( mockResourceCleanup . cleanupWorktree ) . toHaveBeenCalledWith (
2477+ expect . objectContaining ( {
2478+ type : 'pr' ,
2479+ number : 456 ,
2480+ } ) ,
2481+ expect . any ( Object )
2482+ )
2483+ } )
2484+
2485+ it ( 'should work with branch identifiers' , async ( ) => {
2486+ const mockBranchWorktree : GitWorktree = {
2487+ path : '/test/worktree/custom-branch' ,
2488+ branch : 'custom-branch' ,
2489+ commit : 'ghi789' ,
2490+ isPR : false ,
2491+ }
2492+
2493+ vi . mocked ( mockIdentifierParser . parseForPatternDetection ) . mockResolvedValue ( {
2494+ type : 'branch' ,
2495+ branchName : 'custom-branch' ,
2496+ originalInput : 'custom-branch' ,
2497+ } )
2498+
2499+ vi . mocked ( mockGitWorktreeManager . findWorktreeForBranch ) . mockResolvedValue ( mockBranchWorktree )
2500+
2501+ await command . execute ( {
2502+ identifier : 'custom-branch' ,
2503+ options : { } ,
2504+ } )
2505+
2506+ // Verify cleanup was called with branch type
2507+ expect ( mockResourceCleanup . cleanupWorktree ) . toHaveBeenCalledWith (
2508+ expect . objectContaining ( {
2509+ type : 'branch' ,
2510+ branchName : 'custom-branch' ,
2511+ } ) ,
2512+ expect . any ( Object )
2513+ )
2514+ } )
2515+ } )
21952516 } )
21962517} )
0 commit comments