Move src/ → apps/server/ and packages/web/ → apps/web/ to adopt standard monorepo conventions (apps/ for runnable apps, packages/ for reusable libraries). Update all config files, shared package imports, test fixtures, and documentation to reflect new paths. Key fixes: - Update workspace config to ["apps/*", "packages/*"] - Update tsconfig.json rootDir/include for apps/server/ - Add apps/web/** to vitest exclude list - Update drizzle.config.ts schema path - Fix ensure-schema.ts migration path detection (3 levels up in dev, 2 levels up in dist) - Fix tests/integration/cli-server.test.ts import paths - Update packages/shared imports to apps/server/ paths - Update all docs/ files with new paths
317 lines
8.7 KiB
TypeScript
317 lines
8.7 KiB
TypeScript
/**
|
|
* 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,
|
|
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';
|
|
/** Task category */
|
|
category?: 'execute' | 'research' | 'discuss' | 'plan' | 'detail' | 'refine' | 'verify' | 'merge' | 'review';
|
|
/** Names of other tasks in same fixture this task depends on */
|
|
dependsOn?: string[];
|
|
}
|
|
|
|
/**
|
|
* Task group fixture definition (replaces Plan).
|
|
* Tasks are grouped by parent task in the new model.
|
|
*/
|
|
export interface TaskGroupFixture {
|
|
/** Group name (becomes a detail task) */
|
|
name: string;
|
|
/** Tasks in this group */
|
|
tasks: TaskFixture[];
|
|
}
|
|
|
|
/**
|
|
* Phase fixture definition.
|
|
*/
|
|
export interface PhaseFixture {
|
|
/** Phase name */
|
|
name: string;
|
|
/** Task groups in this phase (each group becomes a parent detail task) */
|
|
taskGroups: TaskGroupFixture[];
|
|
}
|
|
|
|
/**
|
|
* 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 task group names to parent task IDs */
|
|
taskGroups: 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, detail tasks (as parent), and child tasks.
|
|
* 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 taskRepo = new DrizzleTaskRepository(db);
|
|
|
|
// Result maps
|
|
const phasesMap = new Map<string, string>();
|
|
const taskGroupsMap = 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,
|
|
status: 'active',
|
|
});
|
|
|
|
// Create phases
|
|
for (const phaseFixture of fixture.phases) {
|
|
const phase = await phaseRepo.create({
|
|
initiativeId: initiative.id,
|
|
name: phaseFixture.name,
|
|
status: 'pending',
|
|
});
|
|
phasesMap.set(phaseFixture.name, phase.id);
|
|
|
|
// Create task groups as parent detail tasks
|
|
let taskOrder = 0;
|
|
for (const groupFixture of phaseFixture.taskGroups) {
|
|
// Create parent detail task
|
|
const parentTask = await taskRepo.create({
|
|
phaseId: phase.id,
|
|
initiativeId: initiative.id,
|
|
name: groupFixture.name,
|
|
description: `Test task group: ${groupFixture.name}`,
|
|
category: 'detail',
|
|
type: 'auto',
|
|
priority: 'medium',
|
|
status: 'completed', // Detail tasks are completed once child tasks are created
|
|
order: taskOrder++,
|
|
});
|
|
taskGroupsMap.set(groupFixture.name, parentTask.id);
|
|
|
|
// Create child tasks linked to parent
|
|
let childOrder = 0;
|
|
for (const taskFixture of groupFixture.tasks) {
|
|
const task = await taskRepo.create({
|
|
parentTaskId: parentTask.id,
|
|
phaseId: phase.id,
|
|
initiativeId: initiative.id,
|
|
name: taskFixture.name,
|
|
description: `Test task: ${taskFixture.name}`,
|
|
category: taskFixture.category ?? 'execute',
|
|
type: 'auto',
|
|
priority: taskFixture.priority ?? 'medium',
|
|
status: 'pending',
|
|
order: childOrder++,
|
|
});
|
|
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,
|
|
taskGroups: taskGroupsMap,
|
|
tasks: tasksMap,
|
|
};
|
|
}
|
|
|
|
// =============================================================================
|
|
// Convenience Fixtures
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Simple fixture: 1 initiative -> 1 phase -> 1 task group -> 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',
|
|
taskGroups: [
|
|
{
|
|
name: 'Task Group 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 task groups (each with 2 independent tasks).
|
|
*
|
|
* Task structure:
|
|
* - Group A: Task X, Task Y (independent)
|
|
* - Group B: Task P, Task Q (independent)
|
|
*/
|
|
export const PARALLEL_FIXTURE: InitiativeFixture = {
|
|
name: 'Parallel Test Initiative',
|
|
phases: [
|
|
{
|
|
name: 'Parallel Phase',
|
|
taskGroups: [
|
|
{
|
|
name: 'Group A',
|
|
tasks: [
|
|
{ id: 'Task X', name: 'Task X', priority: 'high' },
|
|
{ id: 'Task Y', name: 'Task Y', priority: 'medium' },
|
|
],
|
|
},
|
|
{
|
|
name: 'Group 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 task groups with cross-group dependencies.
|
|
*
|
|
* Structure:
|
|
* - Phase 1: Group 1 (Task 1A, 1B), Group 2 (Task 2A depends on 1A)
|
|
* - Phase 2: Group 3 (Task 3A depends on 1B), Group 4 (Task 4A depends on 2A and 3A)
|
|
*/
|
|
export const COMPLEX_FIXTURE: InitiativeFixture = {
|
|
name: 'Complex Test Initiative',
|
|
phases: [
|
|
{
|
|
name: 'Phase 1',
|
|
taskGroups: [
|
|
{
|
|
name: 'Group 1',
|
|
tasks: [
|
|
{ id: 'Task 1A', name: 'Task 1A', priority: 'high' },
|
|
{ id: 'Task 1B', name: 'Task 1B', priority: 'medium' },
|
|
],
|
|
},
|
|
{
|
|
name: 'Group 2',
|
|
tasks: [
|
|
{ id: 'Task 2A', name: 'Task 2A', priority: 'high', dependsOn: ['Task 1A'] },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'Phase 2',
|
|
taskGroups: [
|
|
{
|
|
name: 'Group 3',
|
|
tasks: [
|
|
{ id: 'Task 3A', name: 'Task 3A', priority: 'high', dependsOn: ['Task 1B'] },
|
|
],
|
|
},
|
|
{
|
|
name: 'Group 4',
|
|
tasks: [
|
|
{
|
|
id: 'Task 4A',
|
|
name: 'Task 4A',
|
|
priority: 'high',
|
|
dependsOn: ['Task 2A', 'Task 3A'],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|