/**
* Git Tools Tests
*/
import { jest } from '@jest/globals';
import {
gitStatusExplained,
gitBranchExplained,
gitCommitGuided,
setCommandRunner
} from '../src/tools.js';
describe('Git Tools', () => {
let mockRunner;
beforeEach(() => {
mockRunner = jest.fn();
setCommandRunner(mockRunner);
});
describe('gitStatusExplained', () => {
test('should show clean working tree when no changes', async () => {
mockRunner.mockResolvedValue({
success: true,
stdout: '## main',
stderr: ''
});
const result = await gitStatusExplained();
expect(result.content[0].text).toContain('Branch: main');
expect(result.content[0].text).toContain('Working tree clean');
});
test('should explain untracked files', async () => {
mockRunner.mockResolvedValue({
success: true,
stdout: '## main\n?? newfile.js',
stderr: ''
});
const result = await gitStatusExplained();
expect(result.content[0].text).toContain('newfile.js');
expect(result.content[0].text).toContain('Untracked');
expect(result.content[0].text).toContain('git add');
});
test('should explain modified files', async () => {
mockRunner.mockResolvedValue({
success: true,
stdout: '## main\n M modified.js',
stderr: ''
});
const result = await gitStatusExplained();
expect(result.content[0].text).toContain('modified.js');
expect(result.content[0].text).toContain('Modified');
});
test('should explain staged files', async () => {
mockRunner.mockResolvedValue({
success: true,
stdout: '## main\nM staged.js',
stderr: ''
});
const result = await gitStatusExplained();
expect(result.content[0].text).toContain('staged.js');
expect(result.content[0].text).toContain('Staged');
});
test('should explain merge conflicts', async () => {
mockRunner.mockResolvedValue({
success: true,
stdout: '## main\nUU conflicted.js',
stderr: ''
});
const result = await gitStatusExplained();
expect(result.content[0].text).toContain('conflicted.js');
expect(result.content[0].text).toContain('Conflict');
});
test('should handle not being in a git repository', async () => {
mockRunner.mockResolvedValue({
success: false,
error: 'fatal: not a git repository',
stdout: '',
stderr: ''
});
const result = await gitStatusExplained();
expect(result.content[0].text).toContain('Error');
expect(result.content[0].text).toContain('git init');
});
test('should handle multiple file statuses', async () => {
mockRunner.mockResolvedValue({
success: true,
stdout: '## feature/test\n?? new.js\nM staged.js\n M modified.js',
stderr: ''
});
const result = await gitStatusExplained();
expect(result.content[0].text).toContain('Branch: feature/test');
expect(result.content[0].text).toContain('new.js');
expect(result.content[0].text).toContain('staged.js');
expect(result.content[0].text).toContain('modified.js');
});
});
describe('gitBranchExplained', () => {
test('should list branches with workflow explanation', async () => {
mockRunner.mockResolvedValue({
success: true,
stdout: '* main\n develop\n feature/login',
stderr: ''
});
const result = await gitBranchExplained();
expect(result.content[0].text).toContain('BRANCHES:');
expect(result.content[0].text).toContain('main');
expect(result.content[0].text).toContain('develop');
expect(result.content[0].text).toContain('BRANCH WORKFLOW');
expect(result.content[0].text).toContain('git checkout -b');
});
test('should handle error when not in git repo', async () => {
mockRunner.mockResolvedValue({
success: false,
error: 'fatal: not a git repository',
stdout: '',
stderr: ''
});
const result = await gitBranchExplained();
expect(result.content[0].text).toContain('Error');
});
});
describe('gitCommitGuided', () => {
test('should reject commit message less than 10 characters', async () => {
const result = await gitCommitGuided({ message: 'short', stage_all: false });
expect(result.content[0].text).toContain('at least 10 characters');
expect(result.content[0].text).toContain('feat:');
expect(result.content[0].text).toContain('fix:');
});
test('should reject empty commit message', async () => {
const result = await gitCommitGuided({ message: '', stage_all: false });
expect(result.content[0].text).toContain('at least 10 characters');
});
test('should warn when nothing is staged', async () => {
mockRunner.mockResolvedValue({
success: true,
stdout: '',
stderr: ''
});
const result = await gitCommitGuided({ message: 'feat: add new feature', stage_all: false });
expect(result.content[0].text).toContain('Nothing staged to commit');
expect(result.content[0].text).toContain('git add');
});
test('should stage all and commit successfully', async () => {
mockRunner
.mockResolvedValueOnce({ success: true, stdout: '', stderr: '' }) // git add -A
.mockResolvedValueOnce({ success: true, stdout: 'file1.js\nfile2.js', stderr: '' }) // git diff --cached
.mockResolvedValueOnce({ success: true, stdout: '[main abc123] feat: add feature', stderr: '' }); // git commit
const result = await gitCommitGuided({ message: 'feat: add new feature', stage_all: true });
expect(result.content[0].text).toContain('Staged all changes');
expect(result.content[0].text).toContain('Committed successfully');
expect(result.content[0].text).toContain('git push');
expect(result.content[0].text).toContain('gh pr create');
});
test('should handle commit failure', async () => {
mockRunner
.mockResolvedValueOnce({ success: true, stdout: 'file.js', stderr: '' }) // git diff --cached
.mockResolvedValueOnce({ success: false, error: 'pre-commit hook failed', stderr: 'lint error' }); // git commit
const result = await gitCommitGuided({ message: 'feat: add feature', stage_all: false });
expect(result.content[0].text).toContain('Commit failed');
});
test('should handle staging failure', async () => {
mockRunner.mockResolvedValueOnce({
success: false,
error: 'Permission denied',
stderr: ''
});
const result = await gitCommitGuided({ message: 'feat: add feature', stage_all: true });
expect(result.content[0].text).toContain('Failed to stage');
});
});
});