Files
bDS/tests/engine/TaskManager.test.ts
2026-02-10 11:04:44 +01:00

372 lines
11 KiB
TypeScript

/**
* TaskManager Unit Tests
*
* Tests for the async task management system including:
* - Task execution and progress tracking
* - Task queuing and concurrency limits
* - Task cancellation
* - Event emissions
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { TaskManager, Task, TaskProgress, TaskStatus } from '../../src/main/engine/TaskManager';
import {
createMockTask,
createMockSlowTask,
createMockFailingTask,
resetMockCounters
} from '../utils/factories';
describe('TaskManager', () => {
let taskManager: TaskManager;
beforeEach(() => {
taskManager = new TaskManager();
resetMockCounters();
});
describe('Task Execution', () => {
it('should execute a task successfully', async () => {
const task = createMockTask<string>(async (onProgress) => {
onProgress(50, 'Halfway');
onProgress(100, 'Done');
return 'result';
});
const result = await taskManager.runTask(task);
expect(result).toBe('result');
});
it('should track task progress', async () => {
const progressUpdates: { progress: number; message: string }[] = [];
taskManager.on('taskProgress', (taskProgress: TaskProgress) => {
progressUpdates.push({
progress: taskProgress.progress,
message: taskProgress.message,
});
});
const task = createMockTask(async (onProgress) => {
onProgress(25, 'Step 1');
onProgress(50, 'Step 2');
onProgress(75, 'Step 3');
onProgress(100, 'Complete');
});
await taskManager.runTask(task);
expect(progressUpdates.length).toBe(4);
expect(progressUpdates[0]).toEqual({ progress: 25, message: 'Step 1' });
expect(progressUpdates[3]).toEqual({ progress: 100, message: 'Complete' });
});
it('should emit taskCreated event when task starts', async () => {
const createdHandler = vi.fn();
taskManager.on('taskCreated', createdHandler);
const task = createMockTask();
await taskManager.runTask(task);
// The handler is called at some point during task lifecycle
expect(createdHandler).toHaveBeenCalled();
expect(createdHandler.mock.calls[0][0].taskId).toBe(task.id);
});
it('should emit taskStarted event when task begins execution', async () => {
const startedHandler = vi.fn();
taskManager.on('taskStarted', startedHandler);
const task = createMockTask();
await taskManager.runTask(task);
// The handler is called at some point during task lifecycle
expect(startedHandler).toHaveBeenCalled();
expect(startedHandler.mock.calls[0][0].taskId).toBe(task.id);
});
it('should emit taskCompleted event when task finishes', async () => {
const completedHandler = vi.fn();
taskManager.on('taskCompleted', completedHandler);
const task = createMockTask();
await taskManager.runTask(task);
expect(completedHandler).toHaveBeenCalledTimes(1);
expect(completedHandler).toHaveBeenCalledWith(
expect.objectContaining({
taskId: task.id,
status: 'completed',
progress: 100,
})
);
});
it('should set endTime when task completes', async () => {
const task = createMockTask();
await taskManager.runTask(task);
const status = taskManager.getTaskStatus(task.id);
expect(status?.endTime).toBeInstanceOf(Date);
});
});
describe('Task Failure', () => {
it('should handle task failure gracefully', async () => {
const task = createMockFailingTask('Test error');
await expect(taskManager.runTask(task)).rejects.toThrow('Test error');
});
it('should emit taskFailed event on error', async () => {
const failedHandler = vi.fn();
taskManager.on('taskFailed', failedHandler);
const task = createMockFailingTask('Something went wrong');
try {
await taskManager.runTask(task);
} catch {
// Expected
}
expect(failedHandler).toHaveBeenCalledTimes(1);
expect(failedHandler).toHaveBeenCalledWith(
expect.objectContaining({
taskId: task.id,
status: 'failed',
error: 'Something went wrong',
})
);
});
it('should set task status to failed on error', async () => {
const task = createMockFailingTask();
try {
await taskManager.runTask(task);
} catch {
// Expected
}
const status = taskManager.getTaskStatus(task.id);
expect(status?.status).toBe('failed');
});
});
describe('Task Status Queries', () => {
it('should return task status by id', async () => {
const task = createMockTask();
// Before running
expect(taskManager.getTaskStatus(task.id)).toBeUndefined();
await taskManager.runTask(task);
// After running
const status = taskManager.getTaskStatus(task.id);
expect(status).toBeDefined();
expect(status?.taskId).toBe(task.id);
});
it('should return all tasks', async () => {
const task1 = createMockTask();
const task2 = createMockTask();
await Promise.all([
taskManager.runTask(task1),
taskManager.runTask(task2),
]);
const allTasks = taskManager.getAllTasks();
expect(allTasks.length).toBe(2);
});
it('should return only running tasks', async () => {
// Create a slow task that will still be running
let resolveTask: () => void;
const slowTask = createMockTask(async (onProgress) => {
onProgress(10, 'Working...');
await new Promise<void>(resolve => {
resolveTask = resolve;
});
});
const fastTask = createMockTask();
// Start slow task (don't await)
const slowPromise = taskManager.runTask(slowTask);
// Run fast task
await taskManager.runTask(fastTask);
const runningTasks = taskManager.getRunningTasks();
expect(runningTasks.length).toBe(1);
expect(runningTasks[0].taskId).toBe(slowTask.id);
// Cleanup
resolveTask!();
await slowPromise;
});
});
describe('Task Cancellation', () => {
it('should cancel a running task that checks for abort', async () => {
// Task that polls for cancellation via onProgress check
const task = createMockTask(async (onProgress) => {
for (let i = 0; i < 20; i++) {
// onProgress will throw if task was cancelled
onProgress(i * 5, `Step ${i}`);
await new Promise(resolve => setTimeout(resolve, 5));
}
});
const taskPromise = taskManager.runTask(task);
// Give task time to start and make some progress
await new Promise(resolve => setTimeout(resolve, 20));
const cancelled = taskManager.cancelTask(task.id);
expect(cancelled).toBe(true);
// The task should throw 'Task cancelled' error
await expect(taskPromise).rejects.toThrow('Task cancelled');
});
it('should return false when cancelling non-existent task', () => {
const cancelled = taskManager.cancelTask('non-existent-id');
expect(cancelled).toBe(false);
});
it('should set task status to cancelled after cancel', async () => {
// Task that polls for cancellation
const task = createMockTask(async (onProgress) => {
for (let i = 0; i < 20; i++) {
onProgress(i * 5, `Step ${i}`);
await new Promise(resolve => setTimeout(resolve, 5));
}
});
const taskPromise = taskManager.runTask(task);
await new Promise(resolve => setTimeout(resolve, 20));
taskManager.cancelTask(task.id);
try {
await taskPromise;
} catch {
// Expected
}
const status = taskManager.getTaskStatus(task.id);
expect(status?.status).toBe('cancelled');
});
});
describe('Clear Completed Tasks', () => {
it('should remove completed tasks', async () => {
const task = createMockTask();
await taskManager.runTask(task);
expect(taskManager.getAllTasks().length).toBe(1);
taskManager.clearCompletedTasks();
expect(taskManager.getAllTasks().length).toBe(0);
});
it('should remove failed tasks', async () => {
const task = createMockFailingTask();
try {
await taskManager.runTask(task);
} catch {
// Expected
}
expect(taskManager.getAllTasks().length).toBe(1);
taskManager.clearCompletedTasks();
expect(taskManager.getAllTasks().length).toBe(0);
});
it('should emit tasksCleared event', async () => {
const clearedHandler = vi.fn();
taskManager.on('tasksCleared', clearedHandler);
const task = createMockTask();
await taskManager.runTask(task);
taskManager.clearCompletedTasks();
expect(clearedHandler).toHaveBeenCalledTimes(1);
});
});
describe('Task Return Values', () => {
it('should return typed results from tasks', async () => {
interface TaskResult {
count: number;
items: string[];
}
const task = createMockTask<TaskResult>(async () => ({
count: 3,
items: ['a', 'b', 'c'],
}));
const result = await taskManager.runTask(task);
expect(result.count).toBe(3);
expect(result.items).toEqual(['a', 'b', 'c']);
});
it('should handle void tasks', async () => {
const task = createMockTask<void>(async () => {
// No return
});
const result = await taskManager.runTask(task);
expect(result).toBeUndefined();
});
});
});
describe('TaskManager Concurrency', () => {
let taskManager: TaskManager;
const MAX_CONCURRENT = 3;
beforeEach(() => {
taskManager = new TaskManager();
resetMockCounters();
});
it('should run tasks up to concurrency limit', async () => {
// Create fast tasks that complete quickly
const tasks = Array.from({ length: 5 }, () =>
createMockTask(async (onProgress) => {
onProgress(50, 'Working');
await new Promise(resolve => setTimeout(resolve, 10));
})
);
// Start all tasks
const promises = tasks.map(t => taskManager.runTask(t));
// Since max concurrent is 3, we should never exceed that
// The first 3 will start immediately, others will queue
const runningCount = taskManager.getRunningTasks().length;
expect(runningCount).toBeLessThanOrEqual(MAX_CONCURRENT);
// Wait for all to complete
await Promise.all(promises);
// All should be completed now
expect(taskManager.getAllTasks().every(t => t.status === 'completed')).toBe(true);
});
});