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:
311
src/test/fixtures.ts
Normal file
311
src/test/fixtures.ts
Normal 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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user