From ba1f7ccd625e4fd6721a59b9db5047c2e01d40c9 Mon Sep 17 00:00:00 2001 From: Lukas May Date: Sat, 31 Jan 2026 08:46:22 +0100 Subject: [PATCH] feat(07-02): create fixture helpers for database seeding - Add TaskFixture, PlanFixture, PhaseFixture, InitiativeFixture interfaces - Add seedFixture() for creating complete task hierarchies with dependency resolution - Add SeededFixture result type with name-to-ID mappings - Add SIMPLE_FIXTURE (A -> B, C dependency chain) - Add PARALLEL_FIXTURE (2 plans with independent tasks) - Add COMPLEX_FIXTURE (cross-plan dependencies across 2 phases) --- src/test/fixtures.ts | 311 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 src/test/fixtures.ts diff --git a/src/test/fixtures.ts b/src/test/fixtures.ts new file mode 100644 index 0000000..b3ee5cf --- /dev/null +++ b/src/test/fixtures.ts @@ -0,0 +1,311 @@ +/** + * Test Fixtures for E2E Testing + * + * Provides fixture helpers that seed complete task hierarchies + * for integration and E2E tests. + */ + +import { nanoid } from 'nanoid'; +import type { DrizzleDatabase } from '../db/index.js'; +import { + DrizzleInitiativeRepository, + DrizzlePhaseRepository, + DrizzlePlanRepository, + DrizzleTaskRepository, +} from '../db/repositories/drizzle/index.js'; +import { taskDependencies } from '../db/schema.js'; + +// ============================================================================= +// Fixture Interfaces +// ============================================================================= + +/** + * Task fixture definition. + */ +export interface TaskFixture { + /** Unique identifier for this task (used for dependency references) */ + id: string; + /** Task name */ + name: string; + /** Task priority */ + priority?: 'low' | 'medium' | 'high'; + /** Names of other tasks in same fixture this task depends on */ + dependsOn?: string[]; +} + +/** + * Plan fixture definition. + */ +export interface PlanFixture { + /** Plan name */ + name: string; + /** Tasks in this plan */ + tasks: TaskFixture[]; +} + +/** + * Phase fixture definition. + */ +export interface PhaseFixture { + /** Phase name */ + name: string; + /** Plans in this phase */ + plans: PlanFixture[]; +} + +/** + * Initiative fixture definition (top-level). + */ +export interface InitiativeFixture { + /** Initiative name */ + name: string; + /** Phases in this initiative */ + phases: PhaseFixture[]; +} + +/** + * Result of seeding a fixture. + * Maps names to IDs for all created entities. + */ +export interface SeededFixture { + /** ID of the created initiative */ + initiativeId: string; + /** Map of phase names to IDs */ + phases: Map; + /** Map of plan names to IDs */ + plans: Map; + /** Map of task names to IDs */ + tasks: Map; +} + +// ============================================================================= +// Seed Function +// ============================================================================= + +/** + * Seed a complete task hierarchy from a fixture definition. + * + * Creates initiative, phases, plans, and tasks in correct order. + * Resolves task dependencies by name to actual task IDs. + * + * @param db - Drizzle database instance + * @param fixture - The fixture definition to seed + * @returns SeededFixture with all created entity IDs + */ +export async function seedFixture( + db: DrizzleDatabase, + fixture: InitiativeFixture +): Promise { + // Create repositories + const initiativeRepo = new DrizzleInitiativeRepository(db); + const phaseRepo = new DrizzlePhaseRepository(db); + const planRepo = new DrizzlePlanRepository(db); + const taskRepo = new DrizzleTaskRepository(db); + + // Result maps + const phasesMap = new Map(); + const plansMap = new Map(); + const tasksMap = new Map(); + + // Collect all task dependencies to resolve after creation + const pendingDependencies: Array<{ taskId: string; dependsOnNames: string[] }> = []; + + // Create initiative + const initiative = await initiativeRepo.create({ + name: fixture.name, + description: `Test initiative: ${fixture.name}`, + status: 'active', + }); + + // Create phases + let phaseNumber = 1; + for (const phaseFixture of fixture.phases) { + const phase = await phaseRepo.create({ + initiativeId: initiative.id, + number: phaseNumber++, + name: phaseFixture.name, + description: `Test phase: ${phaseFixture.name}`, + status: 'pending', + }); + phasesMap.set(phaseFixture.name, phase.id); + + // Create plans in phase + let planNumber = 1; + for (const planFixture of phaseFixture.plans) { + const plan = await planRepo.create({ + phaseId: phase.id, + number: planNumber++, + name: planFixture.name, + description: `Test plan: ${planFixture.name}`, + status: 'pending', + }); + plansMap.set(planFixture.name, plan.id); + + // Create tasks in plan + let taskOrder = 0; + for (const taskFixture of planFixture.tasks) { + const task = await taskRepo.create({ + planId: plan.id, + name: taskFixture.name, + description: `Test task: ${taskFixture.name}`, + type: 'auto', + priority: taskFixture.priority ?? 'medium', + status: 'pending', + order: taskOrder++, + }); + tasksMap.set(taskFixture.id, task.id); + + // Collect dependencies to resolve later + if (taskFixture.dependsOn && taskFixture.dependsOn.length > 0) { + pendingDependencies.push({ + taskId: task.id, + dependsOnNames: taskFixture.dependsOn, + }); + } + } + } + } + + // Resolve and insert task dependencies + for (const { taskId, dependsOnNames } of pendingDependencies) { + for (const depName of dependsOnNames) { + const dependsOnTaskId = tasksMap.get(depName); + if (!dependsOnTaskId) { + throw new Error( + `Dependency resolution failed: task "${depName}" not found in fixture` + ); + } + + // Insert into task_dependencies table + await db.insert(taskDependencies).values({ + id: nanoid(), + taskId, + dependsOnTaskId, + createdAt: new Date(), + }); + } + } + + return { + initiativeId: initiative.id, + phases: phasesMap, + plans: plansMap, + tasks: tasksMap, + }; +} + +// ============================================================================= +// Convenience Fixtures +// ============================================================================= + +/** + * Simple fixture: 1 initiative -> 1 phase -> 1 plan -> 3 tasks. + * + * Task dependency structure: + * - Task A: no dependencies + * - Task B: depends on Task A + * - Task C: depends on Task A + */ +export const SIMPLE_FIXTURE: InitiativeFixture = { + name: 'Simple Test Initiative', + phases: [ + { + name: 'Phase 1', + plans: [ + { + name: 'Plan 1', + tasks: [ + { id: 'Task A', name: 'Task A', priority: 'high' }, + { id: 'Task B', name: 'Task B', priority: 'medium', dependsOn: ['Task A'] }, + { id: 'Task C', name: 'Task C', priority: 'medium', dependsOn: ['Task A'] }, + ], + }, + ], + }, + ], +}; + +/** + * Parallel fixture: 1 initiative -> 1 phase -> 2 plans (each with 2 independent tasks). + * + * Task structure: + * - Plan 1: Task X, Task Y (independent) + * - Plan 2: Task P, Task Q (independent) + */ +export const PARALLEL_FIXTURE: InitiativeFixture = { + name: 'Parallel Test Initiative', + phases: [ + { + name: 'Parallel Phase', + plans: [ + { + name: 'Plan A', + tasks: [ + { id: 'Task X', name: 'Task X', priority: 'high' }, + { id: 'Task Y', name: 'Task Y', priority: 'medium' }, + ], + }, + { + name: 'Plan B', + tasks: [ + { id: 'Task P', name: 'Task P', priority: 'high' }, + { id: 'Task Q', name: 'Task Q', priority: 'low' }, + ], + }, + ], + }, + ], +}; + +/** + * Complex fixture: 1 initiative -> 2 phases -> 4 plans with cross-plan dependencies. + * + * Structure: + * - Phase 1: Plan 1 (Task 1A, 1B), Plan 2 (Task 2A depends on 1A) + * - Phase 2: Plan 3 (Task 3A depends on 1B), Plan 4 (Task 4A depends on 2A and 3A) + */ +export const COMPLEX_FIXTURE: InitiativeFixture = { + name: 'Complex Test Initiative', + phases: [ + { + name: 'Phase 1', + plans: [ + { + name: 'Plan 1', + tasks: [ + { id: 'Task 1A', name: 'Task 1A', priority: 'high' }, + { id: 'Task 1B', name: 'Task 1B', priority: 'medium' }, + ], + }, + { + name: 'Plan 2', + tasks: [ + { id: 'Task 2A', name: 'Task 2A', priority: 'high', dependsOn: ['Task 1A'] }, + ], + }, + ], + }, + { + name: 'Phase 2', + plans: [ + { + name: 'Plan 3', + tasks: [ + { id: 'Task 3A', name: 'Task 3A', priority: 'high', dependsOn: ['Task 1B'] }, + ], + }, + { + name: 'Plan 4', + tasks: [ + { + id: 'Task 4A', + name: 'Task 4A', + priority: 'high', + dependsOn: ['Task 2A', 'Task 3A'], + }, + ], + }, + ], + }, + ], +};