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
409 lines
13 KiB
TypeScript
409 lines
13 KiB
TypeScript
/**
|
|
* 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,
|
|
name: 'Test Phase',
|
|
});
|
|
|
|
expect(phase.id).toBeDefined();
|
|
expect(phase.id.length).toBeGreaterThan(0);
|
|
expect(phase.initiativeId).toBe(testInitiativeId);
|
|
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',
|
|
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,
|
|
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 createdAt', async () => {
|
|
await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase A',
|
|
});
|
|
await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase B',
|
|
});
|
|
await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase C',
|
|
});
|
|
|
|
// Create another initiative with phases
|
|
const otherInitiative = await initiativeRepo.create({
|
|
name: 'Other Initiative',
|
|
});
|
|
await phaseRepo.create({
|
|
initiativeId: otherInitiative.id,
|
|
name: 'Other Phase',
|
|
});
|
|
|
|
const phases = await phaseRepo.findByInitiativeId(testInitiativeId);
|
|
expect(phases.length).toBe(3);
|
|
expect(phases[0].name).toBe('Phase A');
|
|
expect(phases[1].name).toBe('Phase B');
|
|
expect(phases[2].name).toBe('Phase C');
|
|
});
|
|
});
|
|
|
|
describe('update', () => {
|
|
it('should update fields and updatedAt', async () => {
|
|
const created = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
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()).toBeGreaterThanOrEqual(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,
|
|
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'
|
|
);
|
|
});
|
|
});
|
|
|
|
// ===========================================================================
|
|
// Phase Dependency Tests
|
|
// ===========================================================================
|
|
|
|
describe('createDependency', () => {
|
|
it('should create dependency between two phases', async () => {
|
|
const phase1 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 1',
|
|
});
|
|
const phase2 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 2',
|
|
});
|
|
|
|
await phaseRepo.createDependency(phase2.id, phase1.id);
|
|
|
|
const deps = await phaseRepo.getDependencies(phase2.id);
|
|
expect(deps).toContain(phase1.id);
|
|
});
|
|
|
|
it('should throw if phase does not exist', async () => {
|
|
const phase1 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 1',
|
|
});
|
|
|
|
// Creating dependency with non-existent phase should throw (FK constraint)
|
|
await expect(
|
|
phaseRepo.createDependency('non-existent-phase', phase1.id)
|
|
).rejects.toThrow();
|
|
});
|
|
|
|
it('should allow multiple dependencies for same phase', async () => {
|
|
const phase1 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 1',
|
|
});
|
|
const phase2 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 2',
|
|
});
|
|
const phase3 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 3',
|
|
});
|
|
|
|
// Phase 3 depends on both Phase 1 and Phase 2
|
|
await phaseRepo.createDependency(phase3.id, phase1.id);
|
|
await phaseRepo.createDependency(phase3.id, phase2.id);
|
|
|
|
const deps = await phaseRepo.getDependencies(phase3.id);
|
|
expect(deps.length).toBe(2);
|
|
expect(deps).toContain(phase1.id);
|
|
expect(deps).toContain(phase2.id);
|
|
});
|
|
});
|
|
|
|
describe('getDependencies', () => {
|
|
it('should return empty array for phase with no dependencies', async () => {
|
|
const phase = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 1',
|
|
});
|
|
|
|
const deps = await phaseRepo.getDependencies(phase.id);
|
|
expect(deps).toEqual([]);
|
|
});
|
|
|
|
it('should return dependency IDs for phase with dependencies', async () => {
|
|
const phase1 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 1',
|
|
});
|
|
const phase2 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 2',
|
|
});
|
|
const phase3 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 3',
|
|
});
|
|
|
|
// Phase 3 depends on Phase 1 and Phase 2
|
|
await phaseRepo.createDependency(phase3.id, phase1.id);
|
|
await phaseRepo.createDependency(phase3.id, phase2.id);
|
|
|
|
const deps = await phaseRepo.getDependencies(phase3.id);
|
|
expect(deps.length).toBe(2);
|
|
expect(deps).toContain(phase1.id);
|
|
expect(deps).toContain(phase2.id);
|
|
});
|
|
|
|
it('should return only direct dependencies (not transitive)', async () => {
|
|
// Phase 1 -> Phase 2 -> Phase 3 (linear chain)
|
|
const phase1 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 1',
|
|
});
|
|
const phase2 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 2',
|
|
});
|
|
const phase3 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 3',
|
|
});
|
|
|
|
// Phase 2 depends on Phase 1
|
|
await phaseRepo.createDependency(phase2.id, phase1.id);
|
|
// Phase 3 depends on Phase 2
|
|
await phaseRepo.createDependency(phase3.id, phase2.id);
|
|
|
|
// Phase 3's dependencies should only include Phase 2, not Phase 1
|
|
const depsPhase3 = await phaseRepo.getDependencies(phase3.id);
|
|
expect(depsPhase3.length).toBe(1);
|
|
expect(depsPhase3).toContain(phase2.id);
|
|
expect(depsPhase3).not.toContain(phase1.id);
|
|
});
|
|
});
|
|
|
|
describe('getDependents', () => {
|
|
it('should return empty array for phase with no dependents', async () => {
|
|
const phase = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 1',
|
|
});
|
|
|
|
const dependents = await phaseRepo.getDependents(phase.id);
|
|
expect(dependents).toEqual([]);
|
|
});
|
|
|
|
it('should return dependent phase IDs', async () => {
|
|
const phase1 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 1',
|
|
});
|
|
const phase2 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 2',
|
|
});
|
|
const phase3 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 3',
|
|
});
|
|
|
|
// Phase 2 and Phase 3 both depend on Phase 1
|
|
await phaseRepo.createDependency(phase2.id, phase1.id);
|
|
await phaseRepo.createDependency(phase3.id, phase1.id);
|
|
|
|
const dependents = await phaseRepo.getDependents(phase1.id);
|
|
expect(dependents.length).toBe(2);
|
|
expect(dependents).toContain(phase2.id);
|
|
expect(dependents).toContain(phase3.id);
|
|
});
|
|
|
|
it('should return only direct dependents', async () => {
|
|
// Phase 1 -> Phase 2 -> Phase 3 (linear chain)
|
|
const phase1 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 1',
|
|
});
|
|
const phase2 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 2',
|
|
});
|
|
const phase3 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 3',
|
|
});
|
|
|
|
// Phase 2 depends on Phase 1
|
|
await phaseRepo.createDependency(phase2.id, phase1.id);
|
|
// Phase 3 depends on Phase 2
|
|
await phaseRepo.createDependency(phase3.id, phase2.id);
|
|
|
|
// Phase 1's dependents should only include Phase 2, not Phase 3
|
|
const dependentsPhase1 = await phaseRepo.getDependents(phase1.id);
|
|
expect(dependentsPhase1.length).toBe(1);
|
|
expect(dependentsPhase1).toContain(phase2.id);
|
|
expect(dependentsPhase1).not.toContain(phase3.id);
|
|
});
|
|
});
|
|
|
|
describe('findDependenciesByInitiativeId', () => {
|
|
it('should return empty array for initiative with no dependencies', async () => {
|
|
await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 1',
|
|
});
|
|
|
|
const deps = await phaseRepo.findDependenciesByInitiativeId(testInitiativeId);
|
|
expect(deps).toEqual([]);
|
|
});
|
|
|
|
it('should return all dependency edges for an initiative', async () => {
|
|
const phase1 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 1',
|
|
});
|
|
const phase2 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 2',
|
|
});
|
|
const phase3 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 3',
|
|
});
|
|
|
|
await phaseRepo.createDependency(phase2.id, phase1.id);
|
|
await phaseRepo.createDependency(phase3.id, phase1.id);
|
|
await phaseRepo.createDependency(phase3.id, phase2.id);
|
|
|
|
const deps = await phaseRepo.findDependenciesByInitiativeId(testInitiativeId);
|
|
expect(deps.length).toBe(3);
|
|
expect(deps).toContainEqual({ phaseId: phase2.id, dependsOnPhaseId: phase1.id });
|
|
expect(deps).toContainEqual({ phaseId: phase3.id, dependsOnPhaseId: phase1.id });
|
|
expect(deps).toContainEqual({ phaseId: phase3.id, dependsOnPhaseId: phase2.id });
|
|
});
|
|
|
|
it('should not return dependencies from other initiatives', async () => {
|
|
const phase1 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 1',
|
|
});
|
|
const phase2 = await phaseRepo.create({
|
|
initiativeId: testInitiativeId,
|
|
name: 'Phase 2',
|
|
});
|
|
await phaseRepo.createDependency(phase2.id, phase1.id);
|
|
|
|
const otherInitiative = await initiativeRepo.create({ name: 'Other' });
|
|
const otherPhase1 = await phaseRepo.create({
|
|
initiativeId: otherInitiative.id,
|
|
name: 'Other Phase 1',
|
|
});
|
|
const otherPhase2 = await phaseRepo.create({
|
|
initiativeId: otherInitiative.id,
|
|
name: 'Other Phase 2',
|
|
});
|
|
await phaseRepo.createDependency(otherPhase2.id, otherPhase1.id);
|
|
|
|
const deps = await phaseRepo.findDependenciesByInitiativeId(testInitiativeId);
|
|
expect(deps.length).toBe(1);
|
|
expect(deps[0].phaseId).toBe(phase2.id);
|
|
expect(deps[0].dependsOnPhaseId).toBe(phase1.id);
|
|
});
|
|
});
|
|
});
|