@@ -3,13 +3,15 @@ import { FinishCommand } from './finish.js'
33import { GitHubService } from '../lib/GitHubService.js'
44import { GitWorktreeManager } from '../lib/GitWorktreeManager.js'
55import { ValidationRunner } from '../lib/ValidationRunner.js'
6+ import { CommitManager } from '../lib/CommitManager.js'
67import type { Issue , PullRequest } from '../types/index.js'
78import type { GitWorktree } from '../types/worktree.js'
89
910// Mock dependencies
1011vi . mock ( '../lib/GitHubService.js' )
1112vi . mock ( '../lib/GitWorktreeManager.js' )
1213vi . mock ( '../lib/ValidationRunner.js' )
14+ vi . mock ( '../lib/CommitManager.js' )
1315
1416// Mock the logger to prevent console output during tests
1517vi . mock ( '../utils/logger.js' , ( ) => ( {
@@ -27,11 +29,13 @@ describe('FinishCommand', () => {
2729 let mockGitHubService : GitHubService
2830 let mockGitWorktreeManager : GitWorktreeManager
2931 let mockValidationRunner : ValidationRunner
32+ let mockCommitManager : CommitManager
3033
3134 beforeEach ( ( ) => {
3235 mockGitHubService = new GitHubService ( )
3336 mockGitWorktreeManager = new GitWorktreeManager ( )
3437 mockValidationRunner = new ValidationRunner ( )
38+ mockCommitManager = new CommitManager ( )
3539
3640 // Mock ValidationRunner.runValidations to always succeed by default
3741 vi . mocked ( mockValidationRunner . runValidations ) . mockResolvedValue ( {
@@ -40,10 +44,24 @@ describe('FinishCommand', () => {
4044 totalDuration : 0 ,
4145 } )
4246
47+ // Mock CommitManager.detectUncommittedChanges to return no changes by default
48+ vi . mocked ( mockCommitManager . detectUncommittedChanges ) . mockResolvedValue ( {
49+ hasUncommittedChanges : false ,
50+ unstagedFiles : [ ] ,
51+ stagedFiles : [ ] ,
52+ currentBranch : 'main' ,
53+ isAheadOfRemote : false ,
54+ isBehindRemote : false ,
55+ } )
56+
57+ // Mock CommitManager.commitChanges to succeed by default
58+ vi . mocked ( mockCommitManager . commitChanges ) . mockResolvedValue ( undefined )
59+
4360 command = new FinishCommand (
4461 mockGitHubService ,
4562 mockGitWorktreeManager ,
46- mockValidationRunner
63+ mockValidationRunner ,
64+ mockCommitManager
4765 )
4866 } )
4967
@@ -1277,6 +1295,136 @@ describe('FinishCommand', () => {
12771295 } )
12781296 } )
12791297
1298+ describe ( 'workflow execution order' , ( ) => {
1299+ it ( 'should run validation BEFORE detecting and committing changes' , async ( ) => {
1300+ // Test the correct workflow order: validate → detect → commit
1301+ const executionOrder : string [ ] = [ ]
1302+
1303+ // Mock ValidationRunner to track execution order
1304+ vi . mocked ( mockValidationRunner . runValidations ) . mockImplementation ( async ( ) => {
1305+ executionOrder . push ( 'validation' )
1306+ return {
1307+ success : true ,
1308+ steps : [ ] ,
1309+ totalDuration : 0 ,
1310+ }
1311+ } )
1312+
1313+ // Mock CommitManager to track execution order
1314+ vi . mocked ( mockCommitManager . detectUncommittedChanges ) . mockImplementation ( async ( ) => {
1315+ executionOrder . push ( 'commit-detect' )
1316+ return {
1317+ hasUncommittedChanges : true ,
1318+ unstagedFiles : [ 'src/test.ts' ] ,
1319+ stagedFiles : [ ] ,
1320+ currentBranch : 'feat/issue-123' ,
1321+ isAheadOfRemote : false ,
1322+ isBehindRemote : false ,
1323+ }
1324+ } )
1325+
1326+ vi . mocked ( mockCommitManager . commitChanges ) . mockImplementation ( async ( ) => {
1327+ executionOrder . push ( 'commit-execute' )
1328+ } )
1329+
1330+ vi . mocked ( mockGitHubService . detectInputType ) . mockResolvedValue ( {
1331+ type : 'issue' ,
1332+ number : 123 ,
1333+ rawInput : '123' ,
1334+ } )
1335+ vi . mocked ( mockGitHubService . fetchIssue ) . mockResolvedValue ( {
1336+ number : 123 ,
1337+ title : 'Test Issue' ,
1338+ state : 'open' ,
1339+ body : '' ,
1340+ labels : [ ] ,
1341+ assignees : [ ] ,
1342+ url : 'https://github.com/test/repo/issues/123' ,
1343+ } as Issue )
1344+ vi . mocked ( mockGitWorktreeManager . findWorktreesByIdentifier ) . mockResolvedValue ( [
1345+ { path : '/test/issue-123' , branch : 'feat/issue-123' , commit : 'abc123' , bare : false } ,
1346+ ] as GitWorktree [ ] )
1347+
1348+ // This should succeed with the correct order
1349+ await expect (
1350+ command . execute ( {
1351+ identifier : '123' ,
1352+ options : { } ,
1353+ } )
1354+ ) . resolves . not . toThrow ( )
1355+
1356+ // ✅ CORRECT: The implementation should follow this order
1357+ expect ( executionOrder ) . toEqual ( [
1358+ 'validation' , // ✅ First: Ensure code quality
1359+ 'commit-detect' , // ✅ Second: Check if there are changes to commit
1360+ 'commit-execute' // ✅ Third: Only commit if validation passed
1361+ ] )
1362+ } )
1363+
1364+ it ( 'should NOT commit if validation fails' , async ( ) => {
1365+ // Test that validation failure prevents committing
1366+ const executionOrder : string [ ] = [ ]
1367+
1368+ // Mock ValidationRunner to simulate failure
1369+ vi . mocked ( mockValidationRunner . runValidations ) . mockImplementation ( async ( ) => {
1370+ executionOrder . push ( 'validation' )
1371+ throw new Error ( 'Validation failed: TypeScript errors found' )
1372+ } )
1373+
1374+ // Mock CommitManager - these should NOT be called if validation fails
1375+ vi . mocked ( mockCommitManager . detectUncommittedChanges ) . mockImplementation ( async ( ) => {
1376+ executionOrder . push ( 'commit-detect' )
1377+ return {
1378+ hasUncommittedChanges : true ,
1379+ unstagedFiles : [ 'src/test.ts' ] ,
1380+ stagedFiles : [ ] ,
1381+ currentBranch : 'feat/issue-123' ,
1382+ isAheadOfRemote : false ,
1383+ isBehindRemote : false ,
1384+ }
1385+ } )
1386+
1387+ vi . mocked ( mockCommitManager . commitChanges ) . mockImplementation ( async ( ) => {
1388+ executionOrder . push ( 'commit-execute' )
1389+ } )
1390+
1391+ vi . mocked ( mockGitHubService . detectInputType ) . mockResolvedValue ( {
1392+ type : 'issue' ,
1393+ number : 123 ,
1394+ rawInput : '123' ,
1395+ } )
1396+ vi . mocked ( mockGitHubService . fetchIssue ) . mockResolvedValue ( {
1397+ number : 123 ,
1398+ title : 'Test Issue' ,
1399+ state : 'open' ,
1400+ body : '' ,
1401+ labels : [ ] ,
1402+ assignees : [ ] ,
1403+ url : 'https://github.com/test/repo/issues/123' ,
1404+ } as Issue )
1405+ vi . mocked ( mockGitWorktreeManager . findWorktreesByIdentifier ) . mockResolvedValue ( [
1406+ { path : '/test/issue-123' , branch : 'feat/issue-123' , commit : 'abc123' , bare : false } ,
1407+ ] as GitWorktree [ ] )
1408+
1409+ // This should fail at validation step
1410+ await expect (
1411+ command . execute ( {
1412+ identifier : '123' ,
1413+ options : { } ,
1414+ } )
1415+ ) . rejects . toThrow ( 'Validation failed: TypeScript errors found' )
1416+
1417+ // ✅ CORRECT: Validation fails, so we never detect or commit changes
1418+ expect ( executionOrder ) . toEqual ( [
1419+ 'validation' // ✅ Validation fails, workflow stops here
1420+ ] )
1421+
1422+ // Verify CommitManager methods were never called
1423+ expect ( mockCommitManager . detectUncommittedChanges ) . not . toHaveBeenCalled ( )
1424+ expect ( mockCommitManager . commitChanges ) . not . toHaveBeenCalled ( )
1425+ } )
1426+ } )
1427+
12801428 describe ( 'dependency injection' , ( ) => {
12811429 it ( 'should accept GitHubService via constructor' , ( ) => {
12821430 const customService = new GitHubService ( )
@@ -1296,6 +1444,12 @@ describe('FinishCommand', () => {
12961444 expect ( cmd ) . toBeDefined ( )
12971445 } )
12981446
1447+ it ( 'should accept CommitManager via constructor' , ( ) => {
1448+ const customCommitManager = new CommitManager ( )
1449+ const cmd = new FinishCommand ( undefined , undefined , undefined , customCommitManager )
1450+ expect ( cmd ) . toBeDefined ( )
1451+ } )
1452+
12991453 it ( 'should create default instances when not provided' , ( ) => {
13001454 const cmd = new FinishCommand ( )
13011455 expect ( cmd ) . toBeDefined ( )
0 commit comments