Files
Codewalkers/apps/server/db/repositories/drizzle/task.test.ts
Lukas May 5137a60e70 feat: add quality_review task status and qualityReview initiative flag
Adds two new fields to the database and propagates them through the
repository layer:

- Task status enum gains 'quality_review' (between in_progress and
  completed), enabling a QA gate before tasks are marked complete.
- initiatives.quality_review (INTEGER DEFAULT 0) lets an initiative be
  flagged for quality-review workflow without a data migration (existing
  rows default to false).

Includes:
- Schema changes in schema.ts
- Migration 0037 (ALTER TABLE initiatives ADD quality_review)
- Snapshot chain repaired: deleted stale 0036 snapshot, fixed 0035
  prevId to create a linear chain (0032 → 0034 → 0035), then generated
  clean 0037 snapshot
- Repository adapter already uses SELECT * / spread-update pattern so
  no adapter code changes were needed
- Initiative and task repository tests extended with qualityReview /
  quality_review_status describe blocks (7 new tests)
- docs/database.md updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 21:47:34 +01:00

234 lines
6.7 KiB
TypeScript

/**
* DrizzleTaskRepository Tests
*
* Tests for the Task repository adapter.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { DrizzleTaskRepository } from './task.js';
import { DrizzlePhaseRepository } from './phase.js';
import { DrizzleInitiativeRepository } from './initiative.js';
import { createTestDatabase } from './test-helpers.js';
import type { DrizzleDatabase } from '../../index.js';
describe('DrizzleTaskRepository', () => {
let db: DrizzleDatabase;
let taskRepo: DrizzleTaskRepository;
let phaseRepo: DrizzlePhaseRepository;
let initiativeRepo: DrizzleInitiativeRepository;
let testPhaseId: string;
beforeEach(async () => {
db = createTestDatabase();
taskRepo = new DrizzleTaskRepository(db);
phaseRepo = new DrizzlePhaseRepository(db);
initiativeRepo = new DrizzleInitiativeRepository(db);
// Create full hierarchy for FK constraint
const initiative = await initiativeRepo.create({
name: 'Test Initiative',
});
const phase = await phaseRepo.create({
initiativeId: initiative.id,
name: 'Test Phase',
});
testPhaseId = phase.id;
});
describe('create', () => {
it('should create a task with generated id and timestamps', async () => {
const task = await taskRepo.create({
phaseId: testPhaseId,
name: 'Test Task',
description: 'A test task',
order: 1,
});
expect(task.id).toBeDefined();
expect(task.id.length).toBeGreaterThan(0);
expect(task.phaseId).toBe(testPhaseId);
expect(task.name).toBe('Test Task');
expect(task.type).toBe('auto');
expect(task.priority).toBe('medium');
expect(task.status).toBe('pending');
expect(task.order).toBe(1);
expect(task.createdAt).toBeInstanceOf(Date);
expect(task.updatedAt).toBeInstanceOf(Date);
});
it('should throw for invalid phaseId (FK constraint)', async () => {
await expect(
taskRepo.create({
phaseId: 'invalid-phase-id',
name: 'Invalid Task',
order: 1,
})
).rejects.toThrow();
});
it('should accept custom type and priority', async () => {
const task = await taskRepo.create({
phaseId: testPhaseId,
name: 'High Priority Task',
type: 'auto',
priority: 'high',
order: 1,
});
expect(task.type).toBe('auto');
expect(task.priority).toBe('high');
});
});
describe('findById', () => {
it('should return null for non-existent task', async () => {
const result = await taskRepo.findById('non-existent-id');
expect(result).toBeNull();
});
it('should find an existing task', async () => {
const created = await taskRepo.create({
phaseId: testPhaseId,
name: 'Find Me',
order: 1,
});
const found = await taskRepo.findById(created.id);
expect(found).not.toBeNull();
expect(found!.id).toBe(created.id);
expect(found!.name).toBe('Find Me');
});
});
describe('findByPhaseId', () => {
it('should return empty array for phase with no tasks', async () => {
const tasks = await taskRepo.findByPhaseId(testPhaseId);
expect(tasks).toEqual([]);
});
it('should return only matching tasks ordered by order field', async () => {
// Create tasks out of order
await taskRepo.create({
phaseId: testPhaseId,
name: 'Task 3',
order: 3,
});
await taskRepo.create({
phaseId: testPhaseId,
name: 'Task 1',
order: 1,
});
await taskRepo.create({
phaseId: testPhaseId,
name: 'Task 2',
order: 2,
});
const tasks = await taskRepo.findByPhaseId(testPhaseId);
expect(tasks.length).toBe(3);
expect(tasks[0].name).toBe('Task 1');
expect(tasks[1].name).toBe('Task 2');
expect(tasks[2].name).toBe('Task 3');
});
});
describe('update', () => {
it('should update status correctly', async () => {
const created = await taskRepo.create({
phaseId: testPhaseId,
name: 'Status Test',
status: 'pending',
order: 1,
});
const updated = await taskRepo.update(created.id, {
status: 'in_progress',
});
expect(updated.status).toBe('in_progress');
});
it('should update fields and updatedAt', async () => {
const created = await taskRepo.create({
phaseId: testPhaseId,
name: 'Original Name',
order: 1,
});
await new Promise((resolve) => setTimeout(resolve, 10));
const updated = await taskRepo.update(created.id, {
name: 'Updated Name',
priority: 'low',
});
expect(updated.name).toBe('Updated Name');
expect(updated.priority).toBe('low');
expect(updated.updatedAt.getTime()).toBeGreaterThanOrEqual(created.updatedAt.getTime());
});
it('should throw for non-existent task', async () => {
await expect(
taskRepo.update('non-existent-id', { name: 'New Name' })
).rejects.toThrow('Task not found');
});
});
describe('delete', () => {
it('should delete an existing task', async () => {
const created = await taskRepo.create({
phaseId: testPhaseId,
name: 'To Delete',
order: 1,
});
await taskRepo.delete(created.id);
const found = await taskRepo.findById(created.id);
expect(found).toBeNull();
});
it('should throw for non-existent task', async () => {
await expect(taskRepo.delete('non-existent-id')).rejects.toThrow(
'Task not found'
);
});
});
describe('quality_review status', () => {
it('can set status to quality_review', async () => {
const task = await taskRepo.create({
phaseId: testPhaseId,
name: 'QR Status Task',
order: 99,
});
const updated = await taskRepo.update(task.id, { status: 'quality_review' });
expect(updated.status).toBe('quality_review');
});
it('round-trips quality_review through findById', async () => {
const task = await taskRepo.create({
phaseId: testPhaseId,
name: 'QR Round-trip Task',
order: 100,
});
await taskRepo.update(task.id, { status: 'quality_review' });
const found = await taskRepo.findById(task.id);
expect(found!.status).toBe('quality_review');
});
it('can transition from quality_review to completed', async () => {
const task = await taskRepo.create({
phaseId: testPhaseId,
name: 'QR to Completed',
order: 101,
});
await taskRepo.update(task.id, { status: 'quality_review' });
const completed = await taskRepo.update(task.id, { status: 'completed' });
expect(completed.status).toBe('completed');
});
});
});