Files
automaker/apps/server/tests/unit/services/auto-mode/task-executor.test.ts
Kacper 51641bad9c 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.
2025-12-23 00:53:10 +01:00

333 lines
9.6 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { TaskExecutor } from '@/services/auto-mode/task-executor.js';
import type { ParsedTask } from '@automaker/types';
import type { TaskExecutionContext } from '@/services/auto-mode/types.js';
// Use vi.hoisted for mock functions
const { mockBuildTaskPrompt, mockProcessStream } = vi.hoisted(() => ({
mockBuildTaskPrompt: vi.fn(),
mockProcessStream: vi.fn(),
}));
// Mock dependencies
vi.mock('@automaker/prompts', () => ({
buildTaskPrompt: mockBuildTaskPrompt,
}));
vi.mock('@automaker/utils', async () => {
const actual = await vi.importActual('@automaker/utils');
return {
...actual,
processStream: mockProcessStream,
};
});
describe('TaskExecutor', () => {
let executor: TaskExecutor;
let mockEvents: { emit: ReturnType<typeof vi.fn> };
let mockProvider: { executeQuery: ReturnType<typeof vi.fn> };
let mockContext: TaskExecutionContext;
let mockTasks: ParsedTask[];
beforeEach(() => {
vi.clearAllMocks();
mockEvents = { emit: vi.fn() };
mockProvider = {
executeQuery: vi.fn().mockReturnValue(
(async function* () {
yield { type: 'text', text: 'Task output' };
})()
),
};
mockContext = {
featureId: 'feature-1',
projectPath: '/project',
workDir: '/project/worktree',
model: 'claude-sonnet-4-20250514',
maxTurns: 100,
allowedTools: ['Read', 'Write'],
abortController: new AbortController(),
planContent: '# Plan\nTask list',
userFeedback: undefined,
};
mockTasks = [
{ id: '1', description: 'Task 1', phase: 'Phase 1' },
{ id: '2', description: 'Task 2', phase: 'Phase 1' },
{ id: '3', description: 'Task 3', phase: 'Phase 2' },
];
mockBuildTaskPrompt.mockReturnValue('Generated task prompt');
mockProcessStream.mockResolvedValue({ text: 'Processed output', toolUses: [] });
executor = new TaskExecutor(mockEvents as any);
});
describe('constructor', () => {
it('should create executor instance', () => {
expect(executor).toBeInstanceOf(TaskExecutor);
});
});
describe('executeAll', () => {
it('should yield started and completed events for each task', async () => {
const results: any[] = [];
for await (const progress of executor.executeAll(
mockTasks,
mockContext,
mockProvider as any
)) {
results.push(progress);
}
// Should have 2 events per task (started + completed)
expect(results).toHaveLength(6);
expect(results[0]).toEqual({
taskId: '1',
taskIndex: 0,
tasksTotal: 3,
status: 'started',
});
expect(results[1]).toEqual({
taskId: '1',
taskIndex: 0,
tasksTotal: 3,
status: 'completed',
output: 'Processed output',
phaseComplete: undefined,
});
});
it('should emit task started events', async () => {
const results: any[] = [];
for await (const progress of executor.executeAll(
mockTasks,
mockContext,
mockProvider as any
)) {
results.push(progress);
}
expect(mockEvents.emit).toHaveBeenCalledWith('auto-mode:event', {
type: 'auto_mode_task_started',
featureId: 'feature-1',
projectPath: '/project',
taskId: '1',
taskDescription: 'Task 1',
taskIndex: 0,
tasksTotal: 3,
});
});
it('should emit task complete events', async () => {
const results: any[] = [];
for await (const progress of executor.executeAll(
mockTasks,
mockContext,
mockProvider as any
)) {
results.push(progress);
}
expect(mockEvents.emit).toHaveBeenCalledWith('auto-mode:event', {
type: 'auto_mode_task_complete',
featureId: 'feature-1',
projectPath: '/project',
taskId: '1',
tasksCompleted: 1,
tasksTotal: 3,
});
});
it('should throw on abort', async () => {
mockContext.abortController.abort();
const results: any[] = [];
await expect(async () => {
for await (const progress of executor.executeAll(
mockTasks,
mockContext,
mockProvider as any
)) {
results.push(progress);
}
}).rejects.toThrow('Feature execution aborted');
});
it('should call provider executeQuery with correct options', async () => {
const results: any[] = [];
for await (const progress of executor.executeAll(
mockTasks,
mockContext,
mockProvider as any
)) {
results.push(progress);
}
expect(mockProvider.executeQuery).toHaveBeenCalledWith({
prompt: 'Generated task prompt',
model: 'claude-sonnet-4-20250514',
maxTurns: 50, // Limited to 50 per task
cwd: '/project/worktree',
allowedTools: ['Read', 'Write'],
abortController: mockContext.abortController,
});
});
it('should detect phase completion', async () => {
const results: any[] = [];
for await (const progress of executor.executeAll(
mockTasks,
mockContext,
mockProvider as any
)) {
results.push(progress);
}
// Task 2 completes Phase 1 (next task is Phase 2)
const task2Completed = results.find((r) => r.taskId === '2' && r.status === 'completed');
expect(task2Completed?.phaseComplete).toBe(1);
// Task 3 completes Phase 2 (no more tasks)
const task3Completed = results.find((r) => r.taskId === '3' && r.status === 'completed');
expect(task3Completed?.phaseComplete).toBe(2);
});
it('should emit phase complete events', async () => {
const results: any[] = [];
for await (const progress of executor.executeAll(
mockTasks,
mockContext,
mockProvider as any
)) {
results.push(progress);
}
expect(mockEvents.emit).toHaveBeenCalledWith('auto-mode:event', {
type: 'auto_mode_phase_complete',
featureId: 'feature-1',
projectPath: '/project',
phaseNumber: 1,
});
});
it('should yield failed status on error', async () => {
mockProcessStream.mockRejectedValueOnce(new Error('Task failed'));
const results: any[] = [];
await expect(async () => {
for await (const progress of executor.executeAll(
mockTasks,
mockContext,
mockProvider as any
)) {
results.push(progress);
}
}).rejects.toThrow('Task failed');
expect(results).toContainEqual({
taskId: '1',
taskIndex: 0,
tasksTotal: 3,
status: 'failed',
output: 'Task failed',
});
});
});
describe('executeOne', () => {
it('should execute a single task and return output', async () => {
const result = await executor.executeOne(
mockTasks[0],
mockTasks,
0,
mockContext,
mockProvider as any
);
expect(result).toBe('Processed output');
});
it('should build prompt with correct parameters', async () => {
await executor.executeOne(mockTasks[0], mockTasks, 0, mockContext, mockProvider as any);
expect(mockBuildTaskPrompt).toHaveBeenCalledWith(
mockTasks[0],
mockTasks,
0,
mockContext.planContent,
mockContext.userFeedback
);
});
it('should emit progress events for text output', async () => {
mockProcessStream.mockImplementation(async (_stream, options) => {
options.onText?.('Some output');
return { text: 'Some output', toolUses: [] };
});
await executor.executeOne(mockTasks[0], mockTasks, 0, mockContext, mockProvider as any);
expect(mockEvents.emit).toHaveBeenCalledWith('auto-mode:event', {
type: 'auto_mode_progress',
featureId: 'feature-1',
content: 'Some output',
});
});
it('should emit tool events for tool use', async () => {
mockProcessStream.mockImplementation(async (_stream, options) => {
options.onToolUse?.('Read', { path: '/file.txt' });
return { text: 'Output', toolUses: [] };
});
await executor.executeOne(mockTasks[0], mockTasks, 0, mockContext, mockProvider as any);
expect(mockEvents.emit).toHaveBeenCalledWith('auto-mode:event', {
type: 'auto_mode_tool',
featureId: 'feature-1',
tool: 'Read',
input: { path: '/file.txt' },
});
});
});
describe('phase detection', () => {
it('should not detect phase completion for tasks without phase', async () => {
const tasksNoPhase = [
{ id: '1', description: 'Task 1' },
{ id: '2', description: 'Task 2' },
];
const results: any[] = [];
for await (const progress of executor.executeAll(
tasksNoPhase,
mockContext,
mockProvider as any
)) {
results.push(progress);
}
const completedResults = results.filter((r) => r.status === 'completed');
expect(completedResults.every((r) => r.phaseComplete === undefined)).toBe(true);
});
it('should detect phase change when next task has different phase', async () => {
const results: any[] = [];
for await (const progress of executor.executeAll(
mockTasks,
mockContext,
mockProvider as any
)) {
results.push(progress);
}
// Task 2 (Phase 1) -> Task 3 (Phase 2) = phase complete
const task2Completed = results.find((r) => r.taskId === '2' && r.status === 'completed');
expect(task2Completed?.phaseComplete).toBe(1);
});
});
});