| 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(); |
| }); |
| }); |
|
|