diff --git a/src/db/repositories/drizzle/initiative.test.ts b/src/db/repositories/drizzle/initiative.test.ts new file mode 100644 index 0000000..c82a099 --- /dev/null +++ b/src/db/repositories/drizzle/initiative.test.ts @@ -0,0 +1,124 @@ +/** + * DrizzleInitiativeRepository Tests + * + * Tests for the Initiative repository adapter. + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { DrizzleInitiativeRepository } from './initiative.js'; +import { createTestDatabase } from './test-helpers.js'; +import type { DrizzleDatabase } from '../../index.js'; + +describe('DrizzleInitiativeRepository', () => { + let db: DrizzleDatabase; + let repo: DrizzleInitiativeRepository; + + beforeEach(() => { + db = createTestDatabase(); + repo = new DrizzleInitiativeRepository(db); + }); + + describe('create', () => { + it('should create an initiative with generated id and timestamps', async () => { + const initiative = await repo.create({ + name: 'Test Initiative', + description: 'A test initiative', + }); + + expect(initiative.id).toBeDefined(); + expect(initiative.id.length).toBeGreaterThan(0); + expect(initiative.name).toBe('Test Initiative'); + expect(initiative.description).toBe('A test initiative'); + expect(initiative.status).toBe('active'); + expect(initiative.createdAt).toBeInstanceOf(Date); + expect(initiative.updatedAt).toBeInstanceOf(Date); + }); + + it('should use provided status', async () => { + const initiative = await repo.create({ + name: 'Completed Initiative', + status: 'completed', + }); + + expect(initiative.status).toBe('completed'); + }); + }); + + describe('findById', () => { + it('should return null for non-existent initiative', async () => { + const result = await repo.findById('non-existent-id'); + expect(result).toBeNull(); + }); + + it('should find an existing initiative', async () => { + const created = await repo.create({ + name: 'Find Me', + }); + + const found = await repo.findById(created.id); + expect(found).not.toBeNull(); + expect(found!.id).toBe(created.id); + expect(found!.name).toBe('Find Me'); + }); + }); + + describe('findAll', () => { + it('should return empty array initially', async () => { + const all = await repo.findAll(); + expect(all).toEqual([]); + }); + + it('should return all initiatives', async () => { + await repo.create({ name: 'Initiative 1' }); + await repo.create({ name: 'Initiative 2' }); + await repo.create({ name: 'Initiative 3' }); + + const all = await repo.findAll(); + expect(all.length).toBe(3); + }); + }); + + describe('update', () => { + it('should update fields and updatedAt', async () => { + const created = await repo.create({ + name: 'Original Name', + status: 'active', + }); + + // Small delay to ensure updatedAt differs + await new Promise((resolve) => setTimeout(resolve, 10)); + + const updated = await repo.update(created.id, { + name: 'Updated Name', + status: 'completed', + }); + + expect(updated.name).toBe('Updated Name'); + expect(updated.status).toBe('completed'); + expect(updated.updatedAt.getTime()).toBeGreaterThan(created.updatedAt.getTime()); + }); + + it('should throw for non-existent initiative', async () => { + await expect( + repo.update('non-existent-id', { name: 'New Name' }) + ).rejects.toThrow('Initiative not found'); + }); + }); + + describe('delete', () => { + it('should delete an existing initiative', async () => { + const created = await repo.create({ name: 'To Delete' }); + + await repo.delete(created.id); + + const found = await repo.findById(created.id); + expect(found).toBeNull(); + }); + + it('should throw for non-existent initiative', async () => { + await expect(repo.delete('non-existent-id')).rejects.toThrow( + 'Initiative not found' + ); + }); + }); +}); diff --git a/src/db/repositories/drizzle/initiative.ts b/src/db/repositories/drizzle/initiative.ts index 69ed3e6..c050c23 100644 --- a/src/db/repositories/drizzle/initiative.ts +++ b/src/db/repositories/drizzle/initiative.ts @@ -24,17 +24,20 @@ export class DrizzleInitiativeRepository implements InitiativeRepository { constructor(private db: DrizzleDatabase) {} async create(data: CreateInitiativeData): Promise { + const id = nanoid(); const now = new Date(); - const initiative = { - id: nanoid(), + + await this.db.insert(initiatives).values({ + id, ...data, + status: data.status ?? 'active', createdAt: now, updatedAt: now, - }; + }); - await this.db.insert(initiatives).values(initiative); - - return initiative as Initiative; + // Fetch to get the complete record with all defaults applied + const created = await this.findById(id); + return created!; } async findById(id: string): Promise { diff --git a/src/db/repositories/drizzle/phase.test.ts b/src/db/repositories/drizzle/phase.test.ts new file mode 100644 index 0000000..2e49f9a --- /dev/null +++ b/src/db/repositories/drizzle/phase.test.ts @@ -0,0 +1,171 @@ +/** + * DrizzlePhaseRepository Tests + * + * Tests for the Phase repository adapter. + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { DrizzlePhaseRepository } from './phase.js'; +import { DrizzleInitiativeRepository } from './initiative.js'; +import { createTestDatabase } from './test-helpers.js'; +import type { DrizzleDatabase } from '../../index.js'; + +describe('DrizzlePhaseRepository', () => { + let db: DrizzleDatabase; + let phaseRepo: DrizzlePhaseRepository; + let initiativeRepo: DrizzleInitiativeRepository; + let testInitiativeId: string; + + beforeEach(async () => { + db = createTestDatabase(); + phaseRepo = new DrizzlePhaseRepository(db); + initiativeRepo = new DrizzleInitiativeRepository(db); + + // Create a test initiative for FK constraint + const initiative = await initiativeRepo.create({ + name: 'Test Initiative', + }); + testInitiativeId = initiative.id; + }); + + describe('create', () => { + it('should create a phase with generated id and timestamps', async () => { + const phase = await phaseRepo.create({ + initiativeId: testInitiativeId, + number: 1, + name: 'Test Phase', + description: 'A test phase', + }); + + expect(phase.id).toBeDefined(); + expect(phase.id.length).toBeGreaterThan(0); + expect(phase.initiativeId).toBe(testInitiativeId); + expect(phase.number).toBe(1); + expect(phase.name).toBe('Test Phase'); + expect(phase.status).toBe('pending'); + expect(phase.createdAt).toBeInstanceOf(Date); + expect(phase.updatedAt).toBeInstanceOf(Date); + }); + + it('should throw for invalid initiativeId (FK constraint)', async () => { + await expect( + phaseRepo.create({ + initiativeId: 'invalid-initiative-id', + number: 1, + name: 'Invalid Phase', + }) + ).rejects.toThrow(); + }); + }); + + describe('findById', () => { + it('should return null for non-existent phase', async () => { + const result = await phaseRepo.findById('non-existent-id'); + expect(result).toBeNull(); + }); + + it('should find an existing phase', async () => { + const created = await phaseRepo.create({ + initiativeId: testInitiativeId, + number: 1, + name: 'Find Me', + }); + + const found = await phaseRepo.findById(created.id); + expect(found).not.toBeNull(); + expect(found!.id).toBe(created.id); + expect(found!.name).toBe('Find Me'); + }); + }); + + describe('findByInitiativeId', () => { + it('should return empty array for initiative with no phases', async () => { + const phases = await phaseRepo.findByInitiativeId(testInitiativeId); + expect(phases).toEqual([]); + }); + + it('should return only matching phases ordered by number', async () => { + // Create phases out of order + await phaseRepo.create({ + initiativeId: testInitiativeId, + number: 3, + name: 'Phase 3', + }); + await phaseRepo.create({ + initiativeId: testInitiativeId, + number: 1, + name: 'Phase 1', + }); + await phaseRepo.create({ + initiativeId: testInitiativeId, + number: 2, + name: 'Phase 2', + }); + + // Create another initiative with phases + const otherInitiative = await initiativeRepo.create({ + name: 'Other Initiative', + }); + await phaseRepo.create({ + initiativeId: otherInitiative.id, + number: 1, + name: 'Other Phase', + }); + + const phases = await phaseRepo.findByInitiativeId(testInitiativeId); + expect(phases.length).toBe(3); + expect(phases[0].name).toBe('Phase 1'); + expect(phases[1].name).toBe('Phase 2'); + expect(phases[2].name).toBe('Phase 3'); + }); + }); + + describe('update', () => { + it('should update fields and updatedAt', async () => { + const created = await phaseRepo.create({ + initiativeId: testInitiativeId, + number: 1, + name: 'Original Name', + status: 'pending', + }); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + const updated = await phaseRepo.update(created.id, { + name: 'Updated Name', + status: 'in_progress', + }); + + expect(updated.name).toBe('Updated Name'); + expect(updated.status).toBe('in_progress'); + expect(updated.updatedAt.getTime()).toBeGreaterThan(created.updatedAt.getTime()); + }); + + it('should throw for non-existent phase', async () => { + await expect( + phaseRepo.update('non-existent-id', { name: 'New Name' }) + ).rejects.toThrow('Phase not found'); + }); + }); + + describe('delete', () => { + it('should delete an existing phase', async () => { + const created = await phaseRepo.create({ + initiativeId: testInitiativeId, + number: 1, + name: 'To Delete', + }); + + await phaseRepo.delete(created.id); + + const found = await phaseRepo.findById(created.id); + expect(found).toBeNull(); + }); + + it('should throw for non-existent phase', async () => { + await expect(phaseRepo.delete('non-existent-id')).rejects.toThrow( + 'Phase not found' + ); + }); + }); +}); diff --git a/src/db/repositories/drizzle/phase.ts b/src/db/repositories/drizzle/phase.ts index fa17081..7adea3a 100644 --- a/src/db/repositories/drizzle/phase.ts +++ b/src/db/repositories/drizzle/phase.ts @@ -24,17 +24,20 @@ export class DrizzlePhaseRepository implements PhaseRepository { constructor(private db: DrizzleDatabase) {} async create(data: CreatePhaseData): Promise { + const id = nanoid(); const now = new Date(); - const phase = { - id: nanoid(), + + await this.db.insert(phases).values({ + id, ...data, + status: data.status ?? 'pending', createdAt: now, updatedAt: now, - }; + }); - await this.db.insert(phases).values(phase); - - return phase as Phase; + // Fetch to get the complete record with all defaults applied + const created = await this.findById(id); + return created!; } async findById(id: string): Promise { diff --git a/src/db/repositories/drizzle/plan.test.ts b/src/db/repositories/drizzle/plan.test.ts new file mode 100644 index 0000000..31baeb4 --- /dev/null +++ b/src/db/repositories/drizzle/plan.test.ts @@ -0,0 +1,169 @@ +/** + * DrizzlePlanRepository Tests + * + * Tests for the Plan repository adapter. + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { DrizzlePlanRepository } from './plan.js'; +import { DrizzlePhaseRepository } from './phase.js'; +import { DrizzleInitiativeRepository } from './initiative.js'; +import { createTestDatabase } from './test-helpers.js'; +import type { DrizzleDatabase } from '../../index.js'; + +describe('DrizzlePlanRepository', () => { + let db: DrizzleDatabase; + let planRepo: DrizzlePlanRepository; + let phaseRepo: DrizzlePhaseRepository; + let initiativeRepo: DrizzleInitiativeRepository; + let testPhaseId: string; + + beforeEach(async () => { + db = createTestDatabase(); + planRepo = new DrizzlePlanRepository(db); + phaseRepo = new DrizzlePhaseRepository(db); + initiativeRepo = new DrizzleInitiativeRepository(db); + + // Create test initiative and phase for FK constraint + const initiative = await initiativeRepo.create({ + name: 'Test Initiative', + }); + const phase = await phaseRepo.create({ + initiativeId: initiative.id, + number: 1, + name: 'Test Phase', + }); + testPhaseId = phase.id; + }); + + describe('create', () => { + it('should create a plan with generated id and timestamps', async () => { + const plan = await planRepo.create({ + phaseId: testPhaseId, + number: 1, + name: 'Test Plan', + description: 'A test plan', + }); + + expect(plan.id).toBeDefined(); + expect(plan.id.length).toBeGreaterThan(0); + expect(plan.phaseId).toBe(testPhaseId); + expect(plan.number).toBe(1); + expect(plan.name).toBe('Test Plan'); + expect(plan.status).toBe('pending'); + expect(plan.createdAt).toBeInstanceOf(Date); + expect(plan.updatedAt).toBeInstanceOf(Date); + }); + + it('should throw for invalid phaseId (FK constraint)', async () => { + await expect( + planRepo.create({ + phaseId: 'invalid-phase-id', + number: 1, + name: 'Invalid Plan', + }) + ).rejects.toThrow(); + }); + }); + + describe('findById', () => { + it('should return null for non-existent plan', async () => { + const result = await planRepo.findById('non-existent-id'); + expect(result).toBeNull(); + }); + + it('should find an existing plan', async () => { + const created = await planRepo.create({ + phaseId: testPhaseId, + number: 1, + name: 'Find Me', + }); + + const found = await planRepo.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 plans', async () => { + const plans = await planRepo.findByPhaseId(testPhaseId); + expect(plans).toEqual([]); + }); + + it('should return only matching plans ordered by number', async () => { + // Create plans out of order + await planRepo.create({ + phaseId: testPhaseId, + number: 3, + name: 'Plan 3', + }); + await planRepo.create({ + phaseId: testPhaseId, + number: 1, + name: 'Plan 1', + }); + await planRepo.create({ + phaseId: testPhaseId, + number: 2, + name: 'Plan 2', + }); + + const plans = await planRepo.findByPhaseId(testPhaseId); + expect(plans.length).toBe(3); + expect(plans[0].name).toBe('Plan 1'); + expect(plans[1].name).toBe('Plan 2'); + expect(plans[2].name).toBe('Plan 3'); + }); + }); + + describe('update', () => { + it('should update fields and updatedAt', async () => { + const created = await planRepo.create({ + phaseId: testPhaseId, + number: 1, + name: 'Original Name', + status: 'pending', + }); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + const updated = await planRepo.update(created.id, { + name: 'Updated Name', + status: 'in_progress', + }); + + expect(updated.name).toBe('Updated Name'); + expect(updated.status).toBe('in_progress'); + expect(updated.updatedAt.getTime()).toBeGreaterThan(created.updatedAt.getTime()); + }); + + it('should throw for non-existent plan', async () => { + await expect( + planRepo.update('non-existent-id', { name: 'New Name' }) + ).rejects.toThrow('Plan not found'); + }); + }); + + describe('delete', () => { + it('should delete an existing plan', async () => { + const created = await planRepo.create({ + phaseId: testPhaseId, + number: 1, + name: 'To Delete', + }); + + await planRepo.delete(created.id); + + const found = await planRepo.findById(created.id); + expect(found).toBeNull(); + }); + + it('should throw for non-existent plan', async () => { + await expect(planRepo.delete('non-existent-id')).rejects.toThrow( + 'Plan not found' + ); + }); + }); +}); diff --git a/src/db/repositories/drizzle/plan.ts b/src/db/repositories/drizzle/plan.ts index f116a8c..9734aea 100644 --- a/src/db/repositories/drizzle/plan.ts +++ b/src/db/repositories/drizzle/plan.ts @@ -24,17 +24,20 @@ export class DrizzlePlanRepository implements PlanRepository { constructor(private db: DrizzleDatabase) {} async create(data: CreatePlanData): Promise { + const id = nanoid(); const now = new Date(); - const plan = { - id: nanoid(), + + await this.db.insert(plans).values({ + id, ...data, + status: data.status ?? 'pending', createdAt: now, updatedAt: now, - }; + }); - await this.db.insert(plans).values(plan); - - return plan as Plan; + // Fetch to get the complete record with all defaults applied + const created = await this.findById(id); + return created!; } async findById(id: string): Promise { diff --git a/src/db/repositories/drizzle/task.test.ts b/src/db/repositories/drizzle/task.test.ts new file mode 100644 index 0000000..91c42b3 --- /dev/null +++ b/src/db/repositories/drizzle/task.test.ts @@ -0,0 +1,206 @@ +/** + * DrizzleTaskRepository Tests + * + * Tests for the Task repository adapter. + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { DrizzleTaskRepository } from './task.js'; +import { DrizzlePlanRepository } from './plan.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 planRepo: DrizzlePlanRepository; + let phaseRepo: DrizzlePhaseRepository; + let initiativeRepo: DrizzleInitiativeRepository; + let testPlanId: string; + + beforeEach(async () => { + db = createTestDatabase(); + taskRepo = new DrizzleTaskRepository(db); + planRepo = new DrizzlePlanRepository(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, + number: 1, + name: 'Test Phase', + }); + const plan = await planRepo.create({ + phaseId: phase.id, + number: 1, + name: 'Test Plan', + }); + testPlanId = plan.id; + }); + + describe('create', () => { + it('should create a task with generated id and timestamps', async () => { + const task = await taskRepo.create({ + planId: testPlanId, + name: 'Test Task', + description: 'A test task', + order: 1, + }); + + expect(task.id).toBeDefined(); + expect(task.id.length).toBeGreaterThan(0); + expect(task.planId).toBe(testPlanId); + 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 planId (FK constraint)', async () => { + await expect( + taskRepo.create({ + planId: 'invalid-plan-id', + name: 'Invalid Task', + order: 1, + }) + ).rejects.toThrow(); + }); + + it('should accept custom type and priority', async () => { + const task = await taskRepo.create({ + planId: testPlanId, + name: 'Checkpoint Task', + type: 'checkpoint:human-verify', + priority: 'high', + order: 1, + }); + + expect(task.type).toBe('checkpoint:human-verify'); + 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({ + planId: testPlanId, + 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('findByPlanId', () => { + it('should return empty array for plan with no tasks', async () => { + const tasks = await taskRepo.findByPlanId(testPlanId); + expect(tasks).toEqual([]); + }); + + it('should return only matching tasks ordered by order field', async () => { + // Create tasks out of order + await taskRepo.create({ + planId: testPlanId, + name: 'Task 3', + order: 3, + }); + await taskRepo.create({ + planId: testPlanId, + name: 'Task 1', + order: 1, + }); + await taskRepo.create({ + planId: testPlanId, + name: 'Task 2', + order: 2, + }); + + const tasks = await taskRepo.findByPlanId(testPlanId); + 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({ + planId: testPlanId, + 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({ + planId: testPlanId, + 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()).toBeGreaterThan(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({ + planId: testPlanId, + 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' + ); + }); + }); +}); diff --git a/src/db/repositories/drizzle/task.ts b/src/db/repositories/drizzle/task.ts index 2d4ad22..1745859 100644 --- a/src/db/repositories/drizzle/task.ts +++ b/src/db/repositories/drizzle/task.ts @@ -24,17 +24,23 @@ export class DrizzleTaskRepository implements TaskRepository { constructor(private db: DrizzleDatabase) {} async create(data: CreateTaskData): Promise { + const id = nanoid(); const now = new Date(); - const task = { - id: nanoid(), + + await this.db.insert(tasks).values({ + id, ...data, + type: data.type ?? 'auto', + priority: data.priority ?? 'medium', + status: data.status ?? 'pending', + order: data.order ?? 0, createdAt: now, updatedAt: now, - }; + }); - await this.db.insert(tasks).values(task); - - return task as Task; + // Fetch to get the complete record with all defaults applied + const created = await this.findById(id); + return created!; } async findById(id: string): Promise { diff --git a/src/db/repositories/drizzle/test-helpers.ts b/src/db/repositories/drizzle/test-helpers.ts new file mode 100644 index 0000000..af9907d --- /dev/null +++ b/src/db/repositories/drizzle/test-helpers.ts @@ -0,0 +1,89 @@ +/** + * Test helpers for repository tests. + * + * Provides utilities for setting up in-memory test databases + * with schema applied. + */ + +import Database from 'better-sqlite3'; +import { drizzle } from 'drizzle-orm/better-sqlite3'; +import type { DrizzleDatabase } from '../../index.js'; +import * as schema from '../../schema.js'; + +/** + * SQL statements to create the database schema. + * These mirror the schema defined in schema.ts. + */ +const CREATE_TABLES_SQL = ` +-- Initiatives table +CREATE TABLE IF NOT EXISTS initiatives ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + description TEXT, + status TEXT NOT NULL DEFAULT 'active', + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); + +-- Phases table +CREATE TABLE IF NOT EXISTS phases ( + id TEXT PRIMARY KEY NOT NULL, + initiative_id TEXT NOT NULL REFERENCES initiatives(id) ON DELETE CASCADE, + number INTEGER NOT NULL, + name TEXT NOT NULL, + description TEXT, + status TEXT NOT NULL DEFAULT 'pending', + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); + +-- Plans table +CREATE TABLE IF NOT EXISTS plans ( + id TEXT PRIMARY KEY NOT NULL, + phase_id TEXT NOT NULL REFERENCES phases(id) ON DELETE CASCADE, + number INTEGER NOT NULL, + name TEXT NOT NULL, + description TEXT, + status TEXT NOT NULL DEFAULT 'pending', + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); + +-- Tasks table +CREATE TABLE IF NOT EXISTS tasks ( + id TEXT PRIMARY KEY NOT NULL, + plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE CASCADE, + name TEXT NOT NULL, + description TEXT, + type TEXT NOT NULL DEFAULT 'auto', + priority TEXT NOT NULL DEFAULT 'medium', + status TEXT NOT NULL DEFAULT 'pending', + "order" INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); + +-- Task dependencies table +CREATE TABLE IF NOT EXISTS task_dependencies ( + id TEXT PRIMARY KEY NOT NULL, + task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, + depends_on_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, + created_at INTEGER NOT NULL +); +`; + +/** + * Create an in-memory test database with schema applied. + * Returns a fresh Drizzle instance for each call. + */ +export function createTestDatabase(): DrizzleDatabase { + const sqlite = new Database(':memory:'); + + // Enable foreign keys + sqlite.pragma('foreign_keys = ON'); + + // Create all tables + sqlite.exec(CREATE_TABLES_SQL); + + return drizzle(sqlite, { schema }); +}