Files
Codewalkers/apps/server/db/repositories/drizzle/phase.test.ts
Lukas May 34578d39c6 refactor: Restructure monorepo to apps/server/ and apps/web/ layout
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
2026-03-03 11:22:53 +01:00

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);
});
});
});