projectRoot = '/home/michael/dev/michaelschiemer'; $this->gitTools = new GitTools($this->projectRoot); }); describe('git_status', function () { it('returns git status with staged, unstaged, and untracked files', function () { $result = $this->gitTools->gitStatus(); expect($result)->toHaveKeys(['staged', 'unstaged', 'untracked', 'clean', 'summary']); expect($result['staged'])->toBeArray(); expect($result['unstaged'])->toBeArray(); expect($result['untracked'])->toBeArray(); expect($result['summary'])->toHaveKeys(['staged_count', 'unstaged_count', 'untracked_count']); expect($result['clean'])->toBeIn([true, false]); }); it('marks repository as clean when no changes exist', function () { // This test assumes a clean repository state // In real scenarios, you might need to setup a clean git state first $result = $this->gitTools->gitStatus(); if ($result['clean']) { expect($result['staged'])->toBeEmpty(); expect($result['unstaged'])->toBeEmpty(); expect($result['untracked'])->toBeEmpty(); } }); }); describe('git_diff', function () { it('returns diff for unstaged changes by default', function () { $result = $this->gitTools->gitDiff(staged: false); expect($result)->toHaveKeys(['diff', 'staged', 'file', 'has_changes']); expect($result['staged'])->toBeFalse(); }); it('returns diff for staged changes when staged flag is true', function () { $result = $this->gitTools->gitDiff(staged: true); expect($result)->toHaveKeys(['diff', 'staged', 'file', 'has_changes']); expect($result['staged'])->toBeTrue(); }); it('can get diff for specific file', function () { $result = $this->gitTools->gitDiff(staged: false, file: 'README.md'); expect($result)->toHaveKey('file'); expect($result['file'])->toBe('README.md'); }); }); describe('git_log', function () { it('returns recent commit history', function () { $result = $this->gitTools->gitLog(limit: 5); expect($result)->toHaveKeys(['commits', 'count', 'limit']); expect($result['commits'])->toBeArray(); expect($result['limit'])->toBe(5); if (! empty($result['commits'])) { $commit = $result['commits'][0]; expect($commit)->toHaveKeys(['hash', 'short_hash', 'author', 'email', 'date', 'message']); expect(strlen($commit['hash']))->toBe(40); // Full SHA-1 hash expect(strlen($commit['short_hash']))->toBe(7); } }); it('respects the limit parameter', function () { $result = $this->gitTools->gitLog(limit: 3); expect($result['count'])->toBeLessThanOrEqual(3); expect($result['limit'])->toBe(3); }); }); describe('git_branch_info', function () { it('returns current branch information', function () { $result = $this->gitTools->gitBranchInfo(); expect($result)->toHaveKeys(['current_branch', 'local_branches', 'remote_branches', 'total_local', 'total_remote']); expect($result['current_branch'])->toBeString(); expect($result['local_branches'])->toBeArray(); expect($result['remote_branches'])->toBeArray(); }); it('includes branch counts', function () { $result = $this->gitTools->gitBranchInfo(); expect($result['total_local'])->toBeInt(); expect($result['total_remote'])->toBeInt(); expect($result['total_local'])->toBe(count($result['local_branches'])); expect($result['total_remote'])->toBe(count($result['remote_branches'])); }); }); describe('git_changed_files', function () { it('returns all changed files with their status', function () { $result = $this->gitTools->gitChangedFiles(); expect($result)->toHaveKeys(['changes', 'total', 'by_status']); expect($result['changes'])->toBeArray(); expect($result['total'])->toBeInt(); expect($result['by_status'])->toBeArray(); }); it('includes staged flag for each change', function () { $result = $this->gitTools->gitChangedFiles(); foreach ($result['changes'] as $change) { expect($change)->toHaveKeys(['file', 'status', 'staged']); expect($change['staged'])->toBeIn([true, false]); } }); }); describe('git_stash_list', function () { it('returns list of stashed changes', function () { $result = $this->gitTools->gitStashList(); expect($result)->toHaveKeys(['stashes', 'count']); expect($result['stashes'])->toBeArray(); expect($result['count'])->toBe(count($result['stashes'])); }); it('parses stash entries correctly', function () { $result = $this->gitTools->gitStashList(); foreach ($result['stashes'] as $stash) { expect($stash)->toHaveKeys(['ref', 'index', 'message']); expect($stash['ref'])->toContain('stash@{'); expect($stash['index'])->toBeInt(); expect($stash['message'])->toBeString(); } }); }); describe('git_remote_info', function () { it('returns configured remotes', function () { $result = $this->gitTools->gitRemoteInfo(); expect($result)->toHaveKeys(['remotes', 'count']); expect($result['remotes'])->toBeArray(); expect($result['count'])->toBe(count($result['remotes'])); }); it('includes fetch and push URLs for each remote', function () { $result = $this->gitTools->gitRemoteInfo(); foreach ($result['remotes'] as $remote) { expect($remote)->toHaveKey('name'); expect($remote['name'])->toBeString(); } }); }); describe('git_conflict_status', function () { it('checks for merge/rebase conflicts', function () { $result = $this->gitTools->gitConflictStatus(); expect($result)->toHaveKeys(['has_conflicts', 'state', 'conflicted_files', 'count']); expect($result['has_conflicts'])->toBeIn([true, false]); expect($result['state'])->toBeIn(['normal', 'merging', 'rebasing', 'cherry-picking']); expect($result['conflicted_files'])->toBeArray(); expect($result['count'])->toBe(count($result['conflicted_files'])); }); it('returns normal state when no conflicts exist', function () { $result = $this->gitTools->gitConflictStatus(); // Assuming clean repository state if (! $result['has_conflicts']) { expect($result['state'])->toBe('normal'); expect($result['conflicted_files'])->toBeEmpty(); expect($result['count'])->toBe(0); } }); }); describe('git_generate_commit_message', function () { it('generates conventional commit message from staged changes', function () { // This test requires staged changes $result = $this->gitTools->generateCommitMessage(); // If no staged changes, should return error if (isset($result['error'])) { expect($result['error'])->toContain('No staged changes'); expect($result)->toHaveKey('suggestion'); } else { // If staged changes exist expect($result)->toHaveKeys([ 'suggested_message', 'type', 'scope', 'description', 'changes_summary', 'conventional_commit', ]); expect($result['conventional_commit'])->toBeTrue(); expect($result['type'])->toBeIn(['feat', 'fix', 'docs', 'test', 'chore', 'refactor']); } }); }); describe('error handling', function () { it('handles git command failures gracefully', function () { // Create GitTools with invalid project root $invalidGitTools = new GitTools('/invalid/path/that/does/not/exist'); $result = $invalidGitTools->gitStatus(); // Should either work (if git falls back to current dir) or return error expect($result)->toBeArray(); }); }); describe('private helper methods', function () { it('correctly analyzes diff output', function () { // Test through generateCommitMessage which uses analyzeDiff $result = $this->gitTools->generateCommitMessage(); if (isset($result['changes_summary'])) { $summary = $result['changes_summary']; expect($summary)->toHaveKeys(['files', 'file_count', 'additions', 'deletions', 'net_change']); expect($summary['file_count'])->toBe(count($summary['files'])); expect($summary['net_change'])->toBe($summary['additions'] - $summary['deletions']); } }); }); });