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)
This commit is contained in:
Lukas May
2026-01-31 08:46:22 +01:00
parent e07050de93
commit ba1f7ccd62

311
src/test/fixtures.ts Normal file
View File

@@ -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<string, string>;
/** Map of plan names to IDs */
plans: Map<string, string>;
/** Map of task names to IDs */
tasks: Map<string, string>;
}
// =============================================================================
// 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<SeededFixture> {
// 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<string, string>();
const plansMap = new Map<string, string>();
const tasksMap = new Map<string, string>();
// 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'],
},
],
},
],
},
],
};