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
135 lines
4.4 KiB
TypeScript
135 lines
4.4 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { topologicalSortPhases, type PhaseForSort, type DependencyEdge } from '@codewalk-district/shared';
|
|
|
|
function mkPhase(id: string, createdAt: string | Date): PhaseForSort {
|
|
return { id, createdAt };
|
|
}
|
|
|
|
describe('topologicalSortPhases', () => {
|
|
it('should return empty array for empty input', () => {
|
|
expect(topologicalSortPhases([], [])).toEqual([]);
|
|
});
|
|
|
|
it('should return phases in createdAt order when no edges', () => {
|
|
const phases = [
|
|
mkPhase('c', '2026-01-03'),
|
|
mkPhase('a', '2026-01-01'),
|
|
mkPhase('b', '2026-01-02'),
|
|
];
|
|
const result = topologicalSortPhases(phases, []);
|
|
expect(result.map((p) => p.id)).toEqual(['a', 'b', 'c']);
|
|
});
|
|
|
|
it('should sort linear chain correctly', () => {
|
|
// A -> B -> C (B depends on A, C depends on B)
|
|
const phases = [
|
|
mkPhase('a', '2026-01-01'),
|
|
mkPhase('b', '2026-01-02'),
|
|
mkPhase('c', '2026-01-03'),
|
|
];
|
|
const edges: DependencyEdge[] = [
|
|
{ phaseId: 'b', dependsOnPhaseId: 'a' },
|
|
{ phaseId: 'c', dependsOnPhaseId: 'b' },
|
|
];
|
|
const result = topologicalSortPhases(phases, edges);
|
|
expect(result.map((p) => p.id)).toEqual(['a', 'b', 'c']);
|
|
});
|
|
|
|
it('should handle diamond dependency', () => {
|
|
// A
|
|
// / \
|
|
// B C
|
|
// \ /
|
|
// D
|
|
const phases = [
|
|
mkPhase('a', '2026-01-01'),
|
|
mkPhase('b', '2026-01-02'),
|
|
mkPhase('c', '2026-01-03'),
|
|
mkPhase('d', '2026-01-04'),
|
|
];
|
|
const edges: DependencyEdge[] = [
|
|
{ phaseId: 'b', dependsOnPhaseId: 'a' },
|
|
{ phaseId: 'c', dependsOnPhaseId: 'a' },
|
|
{ phaseId: 'd', dependsOnPhaseId: 'b' },
|
|
{ phaseId: 'd', dependsOnPhaseId: 'c' },
|
|
];
|
|
const result = topologicalSortPhases(phases, edges);
|
|
// A must come first, D must come last, B before C by createdAt
|
|
expect(result[0].id).toBe('a');
|
|
expect(result[3].id).toBe('d');
|
|
expect(result.map((p) => p.id)).toEqual(['a', 'b', 'c', 'd']);
|
|
});
|
|
|
|
it('should use createdAt as deterministic tiebreaker', () => {
|
|
// Three independent phases — should sort by createdAt
|
|
const phases = [
|
|
mkPhase('z', '2026-01-03'),
|
|
mkPhase('y', '2026-01-01'),
|
|
mkPhase('x', '2026-01-02'),
|
|
];
|
|
const result = topologicalSortPhases(phases, []);
|
|
expect(result.map((p) => p.id)).toEqual(['y', 'x', 'z']);
|
|
});
|
|
|
|
it('should handle cycle gracefully by appending cycled nodes', () => {
|
|
// A -> B -> A (cycle), C is independent
|
|
const phases = [
|
|
mkPhase('a', '2026-01-01'),
|
|
mkPhase('b', '2026-01-02'),
|
|
mkPhase('c', '2026-01-03'),
|
|
];
|
|
const edges: DependencyEdge[] = [
|
|
{ phaseId: 'b', dependsOnPhaseId: 'a' },
|
|
{ phaseId: 'a', dependsOnPhaseId: 'b' },
|
|
];
|
|
const result = topologicalSortPhases(phases, edges);
|
|
// C has no deps so it comes first, then A and B appended (cycle)
|
|
expect(result[0].id).toBe('c');
|
|
expect(result.length).toBe(3);
|
|
// A and B are appended in createdAt order
|
|
expect(result[1].id).toBe('a');
|
|
expect(result[2].id).toBe('b');
|
|
});
|
|
|
|
it('should ignore edges referencing non-existent phases', () => {
|
|
const phases = [
|
|
mkPhase('a', '2026-01-01'),
|
|
mkPhase('b', '2026-01-02'),
|
|
];
|
|
const edges: DependencyEdge[] = [
|
|
{ phaseId: 'b', dependsOnPhaseId: 'nonexistent' },
|
|
];
|
|
const result = topologicalSortPhases(phases, edges);
|
|
// Edge is ignored, both treated as independent
|
|
expect(result.map((p) => p.id)).toEqual(['a', 'b']);
|
|
});
|
|
|
|
it('should handle single phase with no edges', () => {
|
|
const phases = [mkPhase('only', '2026-01-01')];
|
|
const result = topologicalSortPhases(phases, []);
|
|
expect(result.map((p) => p.id)).toEqual(['only']);
|
|
});
|
|
|
|
it('should work with Date objects', () => {
|
|
const phases = [
|
|
mkPhase('b', new Date('2026-01-02')),
|
|
mkPhase('a', new Date('2026-01-01')),
|
|
];
|
|
const edges: DependencyEdge[] = [
|
|
{ phaseId: 'b', dependsOnPhaseId: 'a' },
|
|
];
|
|
const result = topologicalSortPhases(phases, edges);
|
|
expect(result.map((p) => p.id)).toEqual(['a', 'b']);
|
|
});
|
|
|
|
it('should preserve extra properties on phase objects', () => {
|
|
const phases = [
|
|
{ id: 'a', createdAt: '2026-01-01', name: 'Alpha', status: 'pending' },
|
|
{ id: 'b', createdAt: '2026-01-02', name: 'Beta', status: 'active' },
|
|
];
|
|
const result = topologicalSortPhases(phases, []);
|
|
expect(result[0].name).toBe('Alpha');
|
|
expect(result[1].name).toBe('Beta');
|
|
});
|
|
});
|