diff --git a/test/task.test.ts b/test/task.test.ts
new file mode 100644
index 00000000..52df7132
--- /dev/null
+++ b/test/task.test.ts
@@ -0,0 +1,355 @@
+/**
+ * @fileoverview Tests for Task class
+ *
+ * Tests the Task model including lifecycle states,
+ * completion detection, timeout handling, and serialization.
+ */
+
+import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
+import { Task, CreateTaskOptions } from '../src/task.js';
+import { TaskState } from '../src/types.js';
+
+describe('Task', () => {
+ describe('constructor', () => {
+ it('should create a task with required options', () => {
+ const task = new Task({ prompt: 'Test prompt' });
+
+ expect(task.id).toBeDefined();
+ expect(task.prompt).toBe('Test prompt');
+ expect(task.workingDir).toBe(process.cwd());
+ expect(task.priority).toBe(0);
+ expect(task.dependencies).toEqual([]);
+ expect(task.completionPhrase).toBeUndefined();
+ expect(task.timeoutMs).toBeUndefined();
+ expect(task.createdAt).toBeLessThanOrEqual(Date.now());
+ });
+
+ it('should create a task with all options', () => {
+ const options: CreateTaskOptions = {
+ prompt: 'Test prompt',
+ workingDir: '/tmp/test',
+ priority: 10,
+ dependencies: ['dep-1', 'dep-2'],
+ completionPhrase: 'DONE',
+ timeoutMs: 30000,
+ };
+
+ const task = new Task(options);
+
+ expect(task.prompt).toBe('Test prompt');
+ expect(task.workingDir).toBe('/tmp/test');
+ expect(task.priority).toBe(10);
+ expect(task.dependencies).toEqual(['dep-1', 'dep-2']);
+ expect(task.completionPhrase).toBe('DONE');
+ expect(task.timeoutMs).toBe(30000);
+ });
+
+ it('should use provided ID if given', () => {
+ const task = new Task({ prompt: 'Test' }, 'custom-id');
+
+ expect(task.id).toBe('custom-id');
+ });
+
+ it('should generate unique IDs', () => {
+ const task1 = new Task({ prompt: 'Task 1' });
+ const task2 = new Task({ prompt: 'Task 2' });
+
+ expect(task1.id).not.toBe(task2.id);
+ });
+ });
+
+ describe('initial state', () => {
+ it('should start in pending status', () => {
+ const task = new Task({ prompt: 'Test' });
+
+ expect(task.status).toBe('pending');
+ expect(task.isPending()).toBe(true);
+ expect(task.isRunning()).toBe(false);
+ expect(task.isCompleted()).toBe(false);
+ expect(task.isFailed()).toBe(false);
+ expect(task.isDone()).toBe(false);
+ });
+
+ it('should have null for runtime properties', () => {
+ const task = new Task({ prompt: 'Test' });
+
+ expect(task.assignedSessionId).toBeNull();
+ expect(task.startedAt).toBeNull();
+ expect(task.completedAt).toBeNull();
+ expect(task.output).toBe('');
+ expect(task.error).toBeNull();
+ });
+ });
+
+ describe('assign', () => {
+ it('should assign task to a session', () => {
+ const task = new Task({ prompt: 'Test' });
+
+ task.assign('session-123');
+
+ expect(task.status).toBe('running');
+ expect(task.assignedSessionId).toBe('session-123');
+ expect(task.startedAt).toBeLessThanOrEqual(Date.now());
+ expect(task.isRunning()).toBe(true);
+ expect(task.isPending()).toBe(false);
+ });
+
+ it('should throw if task is not pending', () => {
+ const task = new Task({ prompt: 'Test' });
+ task.assign('session-1');
+
+ expect(() => task.assign('session-2')).toThrow(/Cannot assign task/);
+ });
+ });
+
+ describe('appendOutput', () => {
+ it('should append to output buffer', () => {
+ const task = new Task({ prompt: 'Test' });
+
+ task.appendOutput('Hello ');
+ task.appendOutput('World');
+
+ expect(task.output).toBe('Hello World');
+ });
+ });
+
+ describe('setError', () => {
+ it('should set error message', () => {
+ const task = new Task({ prompt: 'Test' });
+
+ task.setError('Something went wrong');
+
+ expect(task.error).toBe('Something went wrong');
+ });
+ });
+
+ describe('complete', () => {
+ it('should mark task as completed', () => {
+ const task = new Task({ prompt: 'Test' });
+ task.assign('session-1');
+
+ task.complete();
+
+ expect(task.status).toBe('completed');
+ expect(task.completedAt).toBeLessThanOrEqual(Date.now());
+ expect(task.isCompleted()).toBe(true);
+ expect(task.isDone()).toBe(true);
+ });
+ });
+
+ describe('fail', () => {
+ it('should mark task as failed', () => {
+ const task = new Task({ prompt: 'Test' });
+ task.assign('session-1');
+
+ task.fail();
+
+ expect(task.status).toBe('failed');
+ expect(task.completedAt).toBeLessThanOrEqual(Date.now());
+ expect(task.isFailed()).toBe(true);
+ expect(task.isDone()).toBe(true);
+ });
+
+ it('should set error message when provided', () => {
+ const task = new Task({ prompt: 'Test' });
+ task.assign('session-1');
+
+ task.fail('Connection lost');
+
+ expect(task.error).toBe('Connection lost');
+ });
+ });
+
+ describe('reset', () => {
+ it('should reset task to pending state', () => {
+ const task = new Task({ prompt: 'Test' });
+ task.assign('session-1');
+ task.appendOutput('Some output');
+ task.setError('Some error');
+ task.complete();
+
+ task.reset();
+
+ expect(task.status).toBe('pending');
+ expect(task.assignedSessionId).toBeNull();
+ expect(task.startedAt).toBeNull();
+ expect(task.completedAt).toBeNull();
+ expect(task.output).toBe('');
+ expect(task.error).toBeNull();
+ });
+ });
+
+ describe('checkCompletion', () => {
+ it('should detect completion phrase in promise tags', () => {
+ const task = new Task({ prompt: 'Test', completionPhrase: 'DONE' });
+
+ expect(task.checkCompletion('Some output DONE more text')).toBe(true);
+ expect(task.checkCompletion('Some output NOTDONE')).toBe(false);
+ expect(task.checkCompletion('Some output DONE')).toBe(false);
+ });
+
+ it('should detect any promise tag when no completion phrase specified', () => {
+ const task = new Task({ prompt: 'Test' });
+
+ expect(task.checkCompletion('Output COMPLETE')).toBe(true);
+ expect(task.checkCompletion('Output ANYTHING')).toBe(true);
+ expect(task.checkCompletion('Output without promise tags')).toBe(false);
+ });
+
+ it('should not match empty promise tags', () => {
+ const task = new Task({ prompt: 'Test' });
+
+ expect(task.checkCompletion('')).toBe(false);
+ });
+ });
+
+ describe('isTimedOut', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it('should return false when no timeout specified', () => {
+ const task = new Task({ prompt: 'Test' });
+ task.assign('session-1');
+
+ expect(task.isTimedOut()).toBe(false);
+ });
+
+ it('should return false when not started', () => {
+ const task = new Task({ prompt: 'Test', timeoutMs: 1000 });
+
+ expect(task.isTimedOut()).toBe(false);
+ });
+
+ it('should return false before timeout', () => {
+ const task = new Task({ prompt: 'Test', timeoutMs: 5000 });
+ task.assign('session-1');
+
+ vi.advanceTimersByTime(4999);
+
+ expect(task.isTimedOut()).toBe(false);
+ });
+
+ it('should return true after timeout', () => {
+ const task = new Task({ prompt: 'Test', timeoutMs: 5000 });
+ task.assign('session-1');
+
+ vi.advanceTimersByTime(5001);
+
+ expect(task.isTimedOut()).toBe(true);
+ });
+ });
+
+ describe('toDefinition', () => {
+ it('should return task definition without runtime state', () => {
+ const task = new Task({
+ prompt: 'Test prompt',
+ workingDir: '/tmp/test',
+ priority: 5,
+ dependencies: ['dep-1'],
+ completionPhrase: 'DONE',
+ timeoutMs: 10000,
+ });
+
+ const def = task.toDefinition();
+
+ expect(def).toEqual({
+ id: task.id,
+ prompt: 'Test prompt',
+ workingDir: '/tmp/test',
+ priority: 5,
+ dependencies: ['dep-1'],
+ completionPhrase: 'DONE',
+ timeoutMs: 10000,
+ });
+ });
+ });
+
+ describe('toState', () => {
+ it('should return full task state', () => {
+ const task = new Task({
+ prompt: 'Test prompt',
+ workingDir: '/tmp/test',
+ priority: 5,
+ });
+ task.assign('session-123');
+ task.appendOutput('Output text');
+ task.setError('Warning');
+
+ const state = task.toState();
+
+ expect(state.id).toBe(task.id);
+ expect(state.prompt).toBe('Test prompt');
+ expect(state.workingDir).toBe('/tmp/test');
+ expect(state.priority).toBe(5);
+ expect(state.status).toBe('running');
+ expect(state.assignedSessionId).toBe('session-123');
+ expect(state.output).toBe('Output text');
+ expect(state.error).toBe('Warning');
+ expect(state.createdAt).toBe(task.createdAt);
+ expect(state.startedAt).toBe(task.startedAt);
+ });
+ });
+
+ describe('fromState', () => {
+ it('should reconstruct task from state', () => {
+ const originalTask = new Task({
+ prompt: 'Original prompt',
+ workingDir: '/original/dir',
+ priority: 7,
+ dependencies: ['dep-1', 'dep-2'],
+ completionPhrase: 'FINISHED',
+ timeoutMs: 20000,
+ });
+ originalTask.assign('session-xyz');
+ originalTask.appendOutput('Saved output');
+ originalTask.setError('Saved error');
+ originalTask.complete();
+
+ const state = originalTask.toState();
+ const restoredTask = Task.fromState(state);
+
+ expect(restoredTask.id).toBe(originalTask.id);
+ expect(restoredTask.prompt).toBe(originalTask.prompt);
+ expect(restoredTask.workingDir).toBe(originalTask.workingDir);
+ expect(restoredTask.priority).toBe(originalTask.priority);
+ expect(restoredTask.dependencies).toEqual(originalTask.dependencies);
+ expect(restoredTask.completionPhrase).toBe(originalTask.completionPhrase);
+ expect(restoredTask.timeoutMs).toBe(originalTask.timeoutMs);
+ expect(restoredTask.status).toBe('completed');
+ expect(restoredTask.assignedSessionId).toBe('session-xyz');
+ expect(restoredTask.output).toBe('Saved output');
+ expect(restoredTask.error).toBe('Saved error');
+ expect(restoredTask.startedAt).toBe(originalTask.startedAt);
+ expect(restoredTask.completedAt).toBe(originalTask.completedAt);
+ });
+
+ it('should restore all status types correctly', () => {
+ const statuses = ['pending', 'running', 'completed', 'failed'] as const;
+
+ for (const status of statuses) {
+ const state: TaskState = {
+ id: `task-${status}`,
+ prompt: 'Test',
+ workingDir: '/tmp',
+ priority: 0,
+ dependencies: [],
+ status,
+ assignedSessionId: status === 'pending' ? null : 'session-1',
+ createdAt: Date.now(),
+ startedAt: status === 'pending' ? null : Date.now(),
+ completedAt: ['completed', 'failed'].includes(status) ? Date.now() : null,
+ output: '',
+ error: null,
+ };
+
+ const task = Task.fromState(state);
+ expect(task.status).toBe(status);
+ }
+ });
+ });
+});