GAP A: extend PATCH /tasks/:id to support all five TaskStatus values (open, in_progress, review, done, cancelled). Add reviewTask, cancelTask, reopenTask helpers in taskService. Claim semantics preserved: in_progress requires assignedTo. Board columns for review and cancelled now reachable via the API. GAP B: index fromRole/toRole/fromAgent/toAgent on handoff upsert. SQLite migration guard adds columns to pre-existing DBs without data loss. Board renderHandoffs shows "fromRole[@agent] → toRole[@agent]" via → arrow. GET /handoffs now carries the routing fields in every index entry. Tests: +14 (56 total, 21 files, all green). Covers every new status transition, index field presence, board markup assertions, and a guard for the claim-without-assignedTo 400. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
76 lines
2.5 KiB
TypeScript
76 lines
2.5 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { mkdtempSync, rmSync } from 'fs';
|
|
import { tmpdir } from 'os';
|
|
import { join } from 'path';
|
|
import {
|
|
createTask, listTasks, getTask,
|
|
claimTask, doneTask, reviewTask, cancelTask, reopenTask,
|
|
} from '../src/core/services/taskService.js';
|
|
|
|
describe('taskService', () => {
|
|
let cwd: string;
|
|
|
|
beforeEach(() => {
|
|
cwd = mkdtempSync(join(tmpdir(), 'ah-task-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(cwd, { recursive: true, force: true });
|
|
});
|
|
|
|
it('creates a task', () => {
|
|
const task = createTask(cwd, { title: 'Test', role: 'implementer' });
|
|
expect(task.id).toBe('TSK-0001');
|
|
expect(task.title).toBe('Test');
|
|
expect(task.status).toBe('open');
|
|
});
|
|
|
|
it('lists tasks', () => {
|
|
createTask(cwd, { title: 'A', role: 'implementer' });
|
|
expect(listTasks(cwd)).toHaveLength(1);
|
|
});
|
|
|
|
it('claims and completes a task', () => {
|
|
const task = createTask(cwd, { title: 'B', role: 'implementer' });
|
|
const claimed = claimTask(cwd, task.id, 'codex');
|
|
expect(claimed.status).toBe('in_progress');
|
|
expect(claimed.assignedTo).toBe('codex');
|
|
const done = doneTask(cwd, task.id);
|
|
expect(done.status).toBe('done');
|
|
});
|
|
|
|
it('transitions a task to review', () => {
|
|
const task = createTask(cwd, { title: 'C', role: 'implementer' });
|
|
const inReview = reviewTask(cwd, task.id);
|
|
expect(inReview.status).toBe('review');
|
|
// Verify index is updated
|
|
const listed = listTasks(cwd, { status: 'review' });
|
|
expect(listed).toHaveLength(1);
|
|
expect(listed[0].id).toBe(task.id);
|
|
});
|
|
|
|
it('cancels a task', () => {
|
|
const task = createTask(cwd, { title: 'D', role: 'implementer' });
|
|
const cancelled = cancelTask(cwd, task.id);
|
|
expect(cancelled.status).toBe('cancelled');
|
|
const listed = listTasks(cwd, { status: 'cancelled' });
|
|
expect(listed).toHaveLength(1);
|
|
});
|
|
|
|
it('reopens a task (any status → open)', () => {
|
|
const task = createTask(cwd, { title: 'E', role: 'implementer' });
|
|
cancelTask(cwd, task.id);
|
|
const reopened = reopenTask(cwd, task.id);
|
|
expect(reopened.status).toBe('open');
|
|
const listed = listTasks(cwd, { status: 'open' });
|
|
expect(listed).toHaveLength(1);
|
|
});
|
|
|
|
it('getTask reads back the correct task', () => {
|
|
const task = createTask(cwd, { title: 'F', role: 'architect' });
|
|
const { task: read } = getTask(cwd, task.id);
|
|
expect(read.title).toBe('F');
|
|
expect(read.role).toBe('architect');
|
|
});
|
|
});
|