UNPKG

aiwg

Version:

Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.

518 lines (436 loc) 19.2 kB
/** * Test suite for GitWorkflowOrchestrator * * Tests git workflow automation including branch management, commits, * merges, and PR operations with Conventional Commits support. * * Requirements: * - UC-008: Git Workflow Orchestration * - NFR-GIT-001: Git operations <5s * - NFR-GIT-002: Conflict detection accuracy >90% * - NFR-GIT-003: Commit message generation accuracy >85% */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { promises as fs } from 'fs'; import path from 'path'; import os from 'os'; import type { GitWorkflowOrchestrator, GitConfig, BranchOptions, CommitOptions, MergeOptions, PROptions } from '../../../src/git/git-workflow-orchestrator.ts'; // Mock exec to avoid actual git commands vi.mock('child_process', () => ({ exec: vi.fn((cmd: string, opts: any, callback: Function) => { // Add small delay to ensure duration > 0 setTimeout(() => { // Extract the cwd from options if available const cwd = typeof opts === 'object' && opts?.cwd ? opts.cwd : ''; // Simulate errors for non-existent paths if (cwd.includes('/non/existent') || cwd.includes('non-existent')) { callback(new Error('fatal: not a git repository (or any of the parent directories): .git'), null); return; } // Simulate errors for specific commands if (cmd.includes('non-existent') || cmd.includes('non-existent-branch')) { callback(new Error('error: pathspec \'non-existent\' did not match any file(s) known to git'), null); return; } // Simulate successful git commands if (cmd.includes('git status')) { callback(null, { stdout: '## main\n', stderr: '' }); } else if (cmd.includes('git branch --show-current')) { callback(null, { stdout: 'main\n', stderr: '' }); } else if (cmd.includes('git diff --cached --name-only')) { callback(null, { stdout: 'src/index.ts\n', stderr: '' }); } else if (cmd.includes('git checkout')) { callback(null, { stdout: 'Switched to branch \'main\'\n', stderr: '' }); } else if (cmd.includes('git branch -d') || cmd.includes('git branch -D')) { callback(null, { stdout: 'Deleted branch feature-branch\n', stderr: '' }); } else { callback(null, { stdout: '', stderr: '' }); } }, 1); // 1ms delay to ensure duration > 0 }) })); describe('GitWorkflowOrchestrator', () => { let orchestrator: GitWorkflowOrchestrator; let testRepo: string; beforeEach(async () => { // Create temp git repository for testing testRepo = path.join(os.tmpdir(), `git-test-${Date.now()}`); await fs.mkdir(testRepo, { recursive: true }); // Initialize config const config: GitConfig = { repoPath: testRepo, branchStrategy: 'github-flow', defaultBranch: 'main', remote: 'origin', conventionalCommits: true, autoGenerateMessages: true }; // Dynamic import const { GitWorkflowOrchestrator: Orchestrator } = await import( '../../../src/git/git-workflow-orchestrator.js' ); orchestrator = new Orchestrator(config); }); afterEach(async () => { // Cleanup test repository await fs.rm(testRepo, { recursive: true, force: true }); }); describe('Configuration', () => { it('should initialize with default configuration', async () => { const { GitWorkflowOrchestrator: Orchestrator } = await import( '../../../src/git/git-workflow-orchestrator.js' ); const config: GitConfig = { repoPath: testRepo }; const orch = new Orchestrator(config); expect(orch).toBeDefined(); }); it('should accept custom branch strategies', async () => { const { GitWorkflowOrchestrator: Orchestrator } = await import( '../../../src/git/git-workflow-orchestrator.js' ); const strategies: Array<'gitflow' | 'github-flow' | 'trunk-based'> = [ 'gitflow', 'github-flow', 'trunk-based' ]; // Test all strategies in single test strategies.forEach(strategy => { const config: GitConfig = { repoPath: testRepo, branchStrategy: strategy }; const orch = new Orchestrator(config); expect(orch, `Failed to create orchestrator with strategy: ${strategy}`).toBeDefined(); }); }); it('should default to conventional commits enabled', async () => { const { GitWorkflowOrchestrator: Orchestrator } = await import( '../../../src/git/git-workflow-orchestrator.js' ); const config: GitConfig = { repoPath: testRepo }; const orch = new Orchestrator(config); expect(orch).toBeDefined(); }); }); describe('Git Status Operations (NFR-GIT-001)', () => { it('should get repository status in <5s with all properties (NFR-GIT-001)', async () => { const startTime = Date.now(); try { const status = await orchestrator.getStatus(); // All status property checks in single test expect(status.branch).toBeDefined(); expect(status.staged).toBeInstanceOf(Array); expect(status.unstaged).toBeInstanceOf(Array); expect(status.untracked).toBeInstanceOf(Array); expect(status.conflicts).toBeInstanceOf(Array); expect(typeof status.ahead).toBe('number'); expect(typeof status.behind).toBe('number'); } catch (error) { // Expected in test environment expect(error).toBeDefined(); } const duration = Date.now() - startTime; expect(duration).toBeLessThan(5000); }, 10000); }); describe('Branch Operations', () => { describe('Branch Creation', () => { it('should create branches with all strategies in <5s (NFR-GIT-001)', async () => { const { GitWorkflowOrchestrator: Orchestrator } = await import( '../../../src/git/git-workflow-orchestrator.js' ); const testCases = [ { strategy: 'github-flow' as const, options: { name: 'add-feature' } }, { strategy: 'gitflow' as const, options: { name: 'user-authentication', type: 'feature' as const } }, { strategy: 'trunk-based' as const, options: { name: 'quick-fix' } }, { strategy: 'github-flow' as const, options: { name: 'new-feature', baseBranch: 'develop' } }, { strategy: 'github-flow' as const, options: { name: 'critical-bug-fix', type: 'hotfix' as const } } ]; for (const testCase of testCases) { const config: GitConfig = { repoPath: testRepo, branchStrategy: testCase.strategy }; const orch = new Orchestrator(config); const result = await orch.createBranch(testCase.options); expect(result.operation, `Failed for strategy: ${testCase.strategy}, options: ${JSON.stringify(testCase.options)}`).toBe('createBranch'); expect(result.duration, `Duration exceeded for strategy: ${testCase.strategy}`).toBeLessThan(5000); expect(result.duration, `Duration should be > 0 for strategy: ${testCase.strategy}`).toBeGreaterThan(0); } }); }); describe('Branch Switching', () => { it('should switch to existing branch in <5s (NFR-GIT-001)', async () => { const result = await orchestrator.switchBranch('main'); expect(result.operation).toBe('switchBranch'); expect(result.duration).toBeGreaterThanOrEqual(0); expect(result.duration).toBeLessThan(5000); }); it('should handle non-existent branch gracefully', async () => { const result = await orchestrator.switchBranch('non-existent-branch'); expect(result.success).toBe(false); expect(result.error).toBeDefined(); }); }); describe('Branch Deletion', () => { it('should delete branches with various options', async () => { const testCases = [ { name: 'feature-branch', deleteRemote: false }, { name: 'feature-branch', deleteRemote: true }, { name: 'non-existent', deleteRemote: false } ]; for (const testCase of testCases) { const result = await orchestrator.deleteBranch(testCase.name, testCase.deleteRemote); expect(result.operation, `Failed for branch: ${testCase.name}, deleteRemote: ${testCase.deleteRemote}`).toBe('deleteBranch'); if (testCase.name === 'non-existent') { expect(result.success, 'Non-existent branch should fail gracefully').toBe(false); expect(result.error, 'Non-existent branch should have error').toBeDefined(); } } }); }); describe('Branch Listing', () => { it('should list local and remote branches', async () => { const testCases = [false, true]; // local only, then local+remote for (const includeRemote of testCases) { try { const branches = await orchestrator.listBranches(includeRemote); expect(branches, `Failed for includeRemote: ${includeRemote}`).toBeInstanceOf(Array); } catch { // Expected without real git repo } } }); }); }); describe('Commit Operations (NFR-GIT-003)', () => { describe('Conventional Commits', () => { it('should create commits with all types and scopes in <5s (NFR-GIT-001)', async () => { const testCases: CommitOptions[] = [ { type: 'feat', message: 'add user authentication', autoStage: true, files: ['src/auth.ts'] }, { type: 'fix', message: 'resolve null pointer exception', files: ['src/index.ts'] }, { type: 'feat', scope: 'auth', message: 'add JWT validation', files: ['src/auth/jwt.ts'] }, { type: 'feat', scope: 'api', breaking: true, message: 'change API endpoint structure', files: ['src/api.ts'] }, { message: 'update components', autoStage: true, files: ['src/App.tsx', 'src/utils.ts'] }, { message: 'test commit', files: ['test.txt'] } ]; for (const options of testCases) { const result = await orchestrator.commit(options); expect(result.operation, `Failed for commit options: ${JSON.stringify(options)}`).toBe('commit'); expect(result.duration, `Duration exceeded for commit: ${options.message}`).toBeLessThan(5000); } }); it('should auto-generate commit messages and detect types from files (NFR-GIT-003)', async () => { const testFileSets = [ ['test/index.test.ts'], ['docs/README.md'], ['.github/workflows/ci.yml'], ['package.json'], ['src/index.ts'], ['src/index.ts', 'src/utils.ts'] ]; for (const files of testFileSets) { const options: CommitOptions = { generateMessage: true, files }; const result = await orchestrator.commit(options); expect(result.operation, `Failed for files: ${files.join(', ')}`).toBe('commit'); } }); }); describe('Commit Message Generation', () => { it('should generate messages for various file patterns', async () => { const testCases: { files: string[] }[] = [ { files: ['src/index.ts'] }, { files: ['src/file1.ts', 'src/file2.ts', 'src/file3.ts'] }, { files: ['README.md', 'docs/guide.md'] } ]; for (const testCase of testCases) { const options: CommitOptions = { generateMessage: true, files: testCase.files }; const result = await orchestrator.commit(options); expect(result.operation, `Failed for files: ${testCase.files.join(', ')}`).toBe('commit'); } }); }); }); describe('Merge Operations (NFR-GIT-002)', () => { describe('Merge Strategies', () => { it('should merge with all strategies and options in <5s (NFR-GIT-001)', async () => { const testCases: MergeOptions[] = [ { sourceBranch: 'feature-branch' }, { sourceBranch: 'feature-branch', strategy: 'squash' }, { sourceBranch: 'feature-branch', strategy: 'rebase' }, { sourceBranch: 'feature-branch', deleteSource: true } ]; for (const options of testCases) { const result = await orchestrator.merge(options); expect(result.operation, `Failed for merge options: ${JSON.stringify(options)}`).toBe('merge'); expect(result.duration, `Duration exceeded for merge strategy: ${options.strategy || 'default'}`).toBeLessThan(5000); } }); }); describe('Conflict Detection (NFR-GIT-002)', () => { it('should detect merge conflicts before merging', async () => { const options: MergeOptions = { sourceBranch: 'conflicting-branch', checkConflicts: true }; const result = await orchestrator.merge(options); expect(result.operation).toBe('merge'); }); it('should detect conflicts with properties and suggestions with >90% accuracy in <5s (NFR-GIT-002, NFR-GIT-001)', async () => { const startTime = Date.now(); try { const conflicts = await orchestrator.detectMergeConflicts('feature-branch'); expect(conflicts).toBeInstanceOf(Array); // Check all conflict properties in single loop conflicts.forEach((conflict, index) => { expect(conflict.severity, `Conflict ${index} missing severity`).toMatch(/trivial|moderate|complex/); expect(conflict.lineRanges, `Conflict ${index} missing lineRanges`).toBeInstanceOf(Array); expect(conflict.suggestions, `Conflict ${index} missing suggestions`).toBeInstanceOf(Array); }); } catch { // Expected without real git repo } const duration = Date.now() - startTime; expect(duration).toBeLessThan(5000); }); }); }); describe('Pull Request Operations', () => { describe('PR Creation', () => { it('should create PRs with various options in <5s (NFR-GIT-001)', async () => { const testCases: PROptions[] = [ { title: 'Add user authentication', body: 'This PR adds JWT-based authentication', baseBranch: 'main' }, { autoGenerate: true, baseBranch: 'main' }, { title: 'Update documentation', reviewers: ['alice', 'bob'], baseBranch: 'main' }, { title: 'Fix critical bug', labels: ['bug', 'critical', 'hotfix'], baseBranch: 'main' }, { title: 'Add feature', assignees: ['john'], baseBranch: 'main' }, { title: 'Test PR', baseBranch: 'main' } ]; for (const options of testCases) { const result = await orchestrator.createPR(options); expect(result.operation, `Failed for PR options: ${JSON.stringify(options)}`).toBe('createPR'); expect(result.duration, `Duration exceeded for PR: ${options.title || 'auto-generated'}`).toBeLessThan(5000); } }); }); describe('PR Auto-generation', () => { it('should generate PR title and body from commit history', async () => { // All auto-generation scenarios use same pattern const result = await orchestrator.createPR({ autoGenerate: true, baseBranch: 'main' }); expect(result.operation).toBe('createPR'); }); }); }); describe('Integration Tests', () => { it('should handle complete workflow: branch → commit → merge', async () => { // Create branch const branchResult = await orchestrator.createBranch({ name: 'feature-workflow' }); expect(branchResult.operation).toBe('createBranch'); // Create commit const commitResult = await orchestrator.commit({ message: 'add workflow feature', files: ['src/workflow.ts'] }); expect(commitResult.operation).toBe('commit'); // Merge const mergeResult = await orchestrator.merge({ sourceBranch: 'feature-workflow', deleteSource: true }); expect(mergeResult.operation).toBe('merge'); }); it('should handle GitFlow and trunk-based workflows', async () => { const { GitWorkflowOrchestrator: Orchestrator } = await import( '../../../src/git/git-workflow-orchestrator.js' ); const workflowTests = [ { strategy: 'gitflow' as const, branchOpts: { name: 'user-profile', type: 'feature' as const }, commitOpts: { type: 'feat', scope: 'profile', message: 'add profile page' } }, { strategy: 'trunk-based' as const, branchOpts: { name: 'quick-fix' }, commitOpts: { message: 'fix: resolve edge case' } } ]; for (const test of workflowTests) { const config: GitConfig = { repoPath: testRepo, branchStrategy: test.strategy }; const orch = new Orchestrator(config); const branchResult = await orch.createBranch(test.branchOpts); expect(branchResult.operation, `Branch creation failed for strategy: ${test.strategy}`).toBe('createBranch'); const commitResult = await orch.commit(test.commitOpts); expect(commitResult.operation, `Commit failed for strategy: ${test.strategy}`).toBe('commit'); } }); }); describe('Performance Tests (NFR-GIT-001)', () => { it('should execute all operations in <5s each', async () => { const operations = [ () => orchestrator.createBranch({ name: 'perf-test' }), () => orchestrator.switchBranch('main'), () => orchestrator.commit({ message: 'test', files: ['test.txt'] }), () => orchestrator.merge({ sourceBranch: 'perf-test' }), () => orchestrator.deleteBranch('perf-test') ]; for (const op of operations) { const result = await op(); expect(result.duration, `Operation exceeded 5s: ${result.operation}`).toBeLessThan(5000); } }); }); describe('Error Handling', () => { it('should handle missing repository gracefully', async () => { const { GitWorkflowOrchestrator: Orchestrator } = await import( '../../../src/git/git-workflow-orchestrator.js' ); const config: GitConfig = { repoPath: '/non/existent/path' }; const orch = new Orchestrator(config); const result = await orch.createBranch({ name: 'test' }); expect(result.success).toBe(false); expect(result.error).toBeDefined(); }); it('should handle merge conflicts and missing commit messages gracefully', async () => { // Test merge conflict handling const mergeResult = await orchestrator.merge({ sourceBranch: 'conflicting-branch', checkConflicts: true }); expect(mergeResult.operation).toBe('merge'); // Test missing commit message handling const commitResult = await orchestrator.commit({ files: ['test.txt'] // No message provided }); expect(commitResult.operation).toBe('commit'); }); }); });