Skip to content

Commit 3128944

Browse files
committed
Finish now calls cleanup.
Fixes #63
1 parent 03cd4c0 commit 3128944

File tree

2 files changed

+416
-2
lines changed

2 files changed

+416
-2
lines changed

src/commands/finish.test.ts

Lines changed: 322 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { ValidationRunner } from '../lib/ValidationRunner.js'
66
import { CommitManager } from '../lib/CommitManager.js'
77
import { MergeManager } from '../lib/MergeManager.js'
88
import { IdentifierParser } from '../utils/IdentifierParser.js'
9+
import { ResourceCleanup } from '../lib/ResourceCleanup.js'
10+
import { ProcessManager } from '../lib/process/ProcessManager.js'
911
import type { Issue, PullRequest } from '../types/index.js'
1012
import type { GitWorktree } from '../types/worktree.js'
1113

@@ -16,6 +18,8 @@ vi.mock('../lib/ValidationRunner.js')
1618
vi.mock('../lib/CommitManager.js')
1719
vi.mock('../lib/MergeManager.js')
1820
vi.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
2125
vi.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

Comments
 (0)