mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
test: add unit tests for exec-utils, feature verification, project analysis, and task execution
- Introduced comprehensive unit tests for exec-utils, covering execAsync, extendedPath, execEnv, and isENOENT functions. - Added tests for FeatureVerificationService to validate feature verification and commit processes. - Implemented tests for ProjectAnalyzer to ensure project analysis functionality. - Created tests for TaskExecutor to validate task execution and event emissions. These additions enhance test coverage and ensure the reliability of core functionalities.
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { FeatureVerificationService } from '@/services/auto-mode/feature-verification.js';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@automaker/platform', () => ({
|
||||
secureFs: {
|
||||
access: vi.fn(),
|
||||
readFile: vi.fn(),
|
||||
},
|
||||
getFeatureDir: vi.fn(
|
||||
(projectPath: string, featureId: string) => `${projectPath}/.automaker/features/${featureId}`
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('@automaker/git-utils', () => ({
|
||||
runVerificationChecks: vi.fn(),
|
||||
hasUncommittedChanges: vi.fn(),
|
||||
commitAll: vi.fn(),
|
||||
shortHash: vi.fn((hash: string) => hash.substring(0, 7)),
|
||||
}));
|
||||
|
||||
vi.mock('@automaker/prompts', () => ({
|
||||
extractTitleFromDescription: vi.fn((desc: string) => desc.split('\n')[0]),
|
||||
}));
|
||||
|
||||
import { secureFs, getFeatureDir } from '@automaker/platform';
|
||||
import { runVerificationChecks, hasUncommittedChanges, commitAll } from '@automaker/git-utils';
|
||||
|
||||
describe('FeatureVerificationService', () => {
|
||||
let service: FeatureVerificationService;
|
||||
let mockEvents: { emit: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockEvents = { emit: vi.fn() };
|
||||
service = new FeatureVerificationService(mockEvents as any);
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create service instance', () => {
|
||||
expect(service).toBeInstanceOf(FeatureVerificationService);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveWorkDir', () => {
|
||||
it('should return worktree path if it exists', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.resolveWorkDir('/project', 'feature-1');
|
||||
|
||||
expect(result).toBe('/project/.worktrees/feature-1');
|
||||
expect(secureFs.access).toHaveBeenCalledWith('/project/.worktrees/feature-1');
|
||||
});
|
||||
|
||||
it('should return project path if worktree does not exist', async () => {
|
||||
vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));
|
||||
|
||||
const result = await service.resolveWorkDir('/project', 'feature-1');
|
||||
|
||||
expect(result).toBe('/project');
|
||||
});
|
||||
});
|
||||
|
||||
describe('verify', () => {
|
||||
it('should emit success event when verification passes', async () => {
|
||||
vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));
|
||||
vi.mocked(runVerificationChecks).mockResolvedValue({ success: true });
|
||||
|
||||
const result = await service.verify('/project', 'feature-1');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockEvents.emit).toHaveBeenCalledWith('auto-mode:event', {
|
||||
type: 'auto_mode_feature_complete',
|
||||
featureId: 'feature-1',
|
||||
passes: true,
|
||||
message: 'All verification checks passed',
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit failure event when verification fails', async () => {
|
||||
vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));
|
||||
vi.mocked(runVerificationChecks).mockResolvedValue({
|
||||
success: false,
|
||||
failedCheck: 'lint',
|
||||
});
|
||||
|
||||
const result = await service.verify('/project', 'feature-1');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.failedCheck).toBe('lint');
|
||||
expect(mockEvents.emit).toHaveBeenCalledWith('auto-mode:event', {
|
||||
type: 'auto_mode_feature_complete',
|
||||
featureId: 'feature-1',
|
||||
passes: false,
|
||||
message: 'Verification failed: lint',
|
||||
});
|
||||
});
|
||||
|
||||
it('should use worktree path if available', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
vi.mocked(runVerificationChecks).mockResolvedValue({ success: true });
|
||||
|
||||
await service.verify('/project', 'feature-1');
|
||||
|
||||
expect(runVerificationChecks).toHaveBeenCalledWith('/project/.worktrees/feature-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('commit', () => {
|
||||
it('should return null hash when no changes', async () => {
|
||||
vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));
|
||||
vi.mocked(hasUncommittedChanges).mockResolvedValue(false);
|
||||
|
||||
const result = await service.commit('/project', 'feature-1', null);
|
||||
|
||||
expect(result.hash).toBeNull();
|
||||
expect(commitAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should commit changes and return hash', async () => {
|
||||
vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));
|
||||
vi.mocked(hasUncommittedChanges).mockResolvedValue(true);
|
||||
vi.mocked(commitAll).mockResolvedValue('abc123def456');
|
||||
|
||||
const result = await service.commit('/project', 'feature-1', {
|
||||
id: 'feature-1',
|
||||
description: 'Add login button\nWith authentication',
|
||||
} as any);
|
||||
|
||||
expect(result.hash).toBe('abc123def456');
|
||||
expect(result.shortHash).toBe('abc123d');
|
||||
expect(commitAll).toHaveBeenCalledWith(
|
||||
'/project',
|
||||
expect.stringContaining('feat: Add login button')
|
||||
);
|
||||
});
|
||||
|
||||
it('should use provided worktree path', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
vi.mocked(hasUncommittedChanges).mockResolvedValue(true);
|
||||
vi.mocked(commitAll).mockResolvedValue('abc123');
|
||||
|
||||
await service.commit('/project', 'feature-1', null, '/custom/worktree');
|
||||
|
||||
expect(hasUncommittedChanges).toHaveBeenCalledWith('/custom/worktree');
|
||||
});
|
||||
|
||||
it('should fall back to project path if provided worktree does not exist', async () => {
|
||||
vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));
|
||||
vi.mocked(hasUncommittedChanges).mockResolvedValue(false);
|
||||
|
||||
await service.commit('/project', 'feature-1', null, '/nonexistent/worktree');
|
||||
|
||||
expect(hasUncommittedChanges).toHaveBeenCalledWith('/project');
|
||||
});
|
||||
|
||||
it('should use feature ID in commit message when no feature provided', async () => {
|
||||
vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));
|
||||
vi.mocked(hasUncommittedChanges).mockResolvedValue(true);
|
||||
vi.mocked(commitAll).mockResolvedValue('abc123');
|
||||
|
||||
await service.commit('/project', 'feature-123', null);
|
||||
|
||||
expect(commitAll).toHaveBeenCalledWith(
|
||||
'/project',
|
||||
expect.stringContaining('feat: Feature feature-123')
|
||||
);
|
||||
});
|
||||
|
||||
it('should emit event on successful commit', async () => {
|
||||
vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));
|
||||
vi.mocked(hasUncommittedChanges).mockResolvedValue(true);
|
||||
vi.mocked(commitAll).mockResolvedValue('abc123def');
|
||||
|
||||
await service.commit('/project', 'feature-1', null);
|
||||
|
||||
expect(mockEvents.emit).toHaveBeenCalledWith('auto-mode:event', {
|
||||
type: 'auto_mode_feature_complete',
|
||||
featureId: 'feature-1',
|
||||
passes: true,
|
||||
message: expect.stringContaining('Changes committed:'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null hash when commit fails', async () => {
|
||||
vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));
|
||||
vi.mocked(hasUncommittedChanges).mockResolvedValue(true);
|
||||
vi.mocked(commitAll).mockResolvedValue(null);
|
||||
|
||||
const result = await service.commit('/project', 'feature-1', null);
|
||||
|
||||
expect(result.hash).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('contextExists', () => {
|
||||
it('should return true if context file exists', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.contextExists('/project', 'feature-1');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(secureFs.access).toHaveBeenCalledWith(
|
||||
'/project/.automaker/features/feature-1/agent-output.md'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false if context file does not exist', async () => {
|
||||
vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));
|
||||
|
||||
const result = await service.contextExists('/project', 'feature-1');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadContext', () => {
|
||||
it('should return context content if file exists', async () => {
|
||||
vi.mocked(secureFs.readFile).mockResolvedValue('# Agent Output\nSome content');
|
||||
|
||||
const result = await service.loadContext('/project', 'feature-1');
|
||||
|
||||
expect(result).toBe('# Agent Output\nSome content');
|
||||
expect(secureFs.readFile).toHaveBeenCalledWith(
|
||||
'/project/.automaker/features/feature-1/agent-output.md',
|
||||
'utf-8'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null if file does not exist', async () => {
|
||||
vi.mocked(secureFs.readFile).mockRejectedValue(new Error('ENOENT'));
|
||||
|
||||
const result = await service.loadContext('/project', 'feature-1');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user