File size: 4,300 Bytes
391c43e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { TaskManager } from '../task-manager';
describe('TaskManager', () => {
let tm: TaskManager;
beforeEach(() => {
tm = new TaskManager({ maxConcurrentPerScope: 3, keyTTLMs: 30 * 60 * 1000 });
});
afterEach(() => {
tm.dispose();
});
it('creates a task and returns taskId', () => {
const taskId = tm.createTask('proj-1', 'session-1', 'sk-key', 'workspace-1');
expect(taskId).toBeDefined();
expect(typeof taskId).toBe('string');
});
it('retrieves a created task', () => {
const taskId = tm.createTask('proj-1', 'session-1', 'sk-key');
const task = tm.getTask(taskId);
expect(task).toBeDefined();
expect(task!.projectId).toBe('proj-1');
expect(task!.status).toBe('running');
});
it('returns undefined for unknown taskId', () => {
expect(tm.getTask('nonexistent')).toBeUndefined();
});
it('stores and retrieves API key', () => {
const taskId = tm.createTask('proj-1', 'session-1', 'sk-secret');
expect(tm.getApiKey(taskId)).toBe('sk-secret');
});
it('deletes API key on task completion', () => {
const taskId = tm.createTask('proj-1', 'session-1', 'sk-secret');
tm.completeTask(taskId, 'completed');
expect(tm.getApiKey(taskId)).toBeUndefined();
});
it('deletes API key on task failure', () => {
const taskId = tm.createTask('proj-1', 'session-1', 'sk-secret');
tm.completeTask(taskId, 'failed');
expect(tm.getApiKey(taskId)).toBeUndefined();
});
it('deletes API key on task cancellation', () => {
const taskId = tm.createTask('proj-1', 'session-1', 'sk-secret');
tm.completeTask(taskId, 'cancelled');
expect(tm.getApiKey(taskId)).toBeUndefined();
});
it('enforces concurrent task limit per workspace', () => {
tm.createTask('proj-1', 'session-1', 'k1', 'ws-1');
tm.createTask('proj-2', 'session-1', 'k2', 'ws-1');
tm.createTask('proj-3', 'session-1', 'k3', 'ws-1');
expect(() => tm.createTask('proj-4', 'session-1', 'k4', 'ws-1')).toThrow(/concurrent task limit/i);
});
it('enforces concurrent task limit per session when no workspace', () => {
tm.createTask('proj-1', 'sess-1', 'k1');
tm.createTask('proj-2', 'sess-1', 'k2');
tm.createTask('proj-3', 'sess-1', 'k3');
expect(() => tm.createTask('proj-4', 'sess-1', 'k4')).toThrow(/concurrent task limit/i);
});
it('allows tasks from different scopes', () => {
tm.createTask('proj-1', 'sess-1', 'k1', 'ws-1');
tm.createTask('proj-2', 'sess-1', 'k2', 'ws-1');
tm.createTask('proj-3', 'sess-1', 'k3', 'ws-1');
const taskId = tm.createTask('proj-4', 'sess-1', 'k4', 'ws-2');
expect(taskId).toBeDefined();
});
it('completed tasks free up slots', () => {
const t1 = tm.createTask('proj-1', 'sess-1', 'k1', 'ws-1');
tm.createTask('proj-2', 'sess-1', 'k2', 'ws-1');
tm.createTask('proj-3', 'sess-1', 'k3', 'ws-1');
tm.completeTask(t1, 'completed');
const t4 = tm.createTask('proj-4', 'sess-1', 'k4', 'ws-1');
expect(t4).toBeDefined();
});
it('getTasksForSession returns all tasks for a session', () => {
tm.createTask('proj-1', 'sess-1', 'k1');
tm.createTask('proj-2', 'sess-1', 'k2');
tm.createTask('proj-3', 'sess-2', 'k3');
expect(tm.getTasksForSession('sess-1')).toHaveLength(2);
expect(tm.getTasksForSession('sess-2')).toHaveLength(1);
});
it('stores metadata (prompt, model, projectName) on task', () => {
const taskId = tm.createTask('proj-1', 'sess-1', 'sk-key');
const task = tm.getTask(taskId)!;
task.prompt = 'make a button';
task.model = 'gpt-4o';
task.projectName = 'My Project';
const retrieved = tm.getTask(taskId)!;
expect(retrieved.prompt).toBe('make a button');
expect(retrieved.model).toBe('gpt-4o');
expect(retrieved.projectName).toBe('My Project');
});
it('TTL sweep removes expired keys', () => {
vi.useFakeTimers();
const shortTTL = new TaskManager({ maxConcurrentPerScope: 3, keyTTLMs: 1000 });
const taskId = shortTTL.createTask('proj-1', 'sess-1', 'sk-expire');
vi.advanceTimersByTime(1500);
shortTTL.sweepExpiredKeys();
expect(shortTTL.getApiKey(taskId)).toBeUndefined();
shortTTL.dispose();
vi.useRealTimers();
});
});
|