mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
Merge main into massive-terminal-upgrade
Resolves merge conflicts: - apps/server/src/routes/terminal/common.ts: Keep randomBytes import, use @automaker/utils for createLogger - apps/ui/eslint.config.mjs: Use main's explicit globals list with XMLHttpRequest and MediaQueryListEvent additions - apps/ui/src/components/views/terminal-view.tsx: Keep our terminal improvements (killAllSessions, beforeunload, better error handling) - apps/ui/src/config/terminal-themes.ts: Keep our search highlight colors for all themes - apps/ui/src/store/app-store.ts: Keep our terminal settings persistence improvements (merge function) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
188
libs/dependency-resolver/README.md
Normal file
188
libs/dependency-resolver/README.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# @automaker/dependency-resolver
|
||||
|
||||
Feature dependency resolution using topological sorting.
|
||||
|
||||
## Overview
|
||||
|
||||
This package provides dependency resolution for AutoMaker features using Kahn's algorithm with priority-aware ordering. It ensures features are executed in the correct order based on their dependencies.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @automaker/dependency-resolver
|
||||
```
|
||||
|
||||
## Exports
|
||||
|
||||
### Resolve Dependencies
|
||||
|
||||
Order features based on dependencies and priorities.
|
||||
|
||||
```typescript
|
||||
import { resolveDependencies } from '@automaker/dependency-resolver';
|
||||
import type { Feature } from '@automaker/types';
|
||||
|
||||
const features: Feature[] = [
|
||||
{
|
||||
id: 'database',
|
||||
category: 'backend',
|
||||
description: 'Setup database',
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
id: 'auth',
|
||||
category: 'backend',
|
||||
description: 'Add authentication',
|
||||
dependencies: ['database'],
|
||||
priority: 2,
|
||||
},
|
||||
{
|
||||
id: 'api',
|
||||
category: 'backend',
|
||||
description: 'Create API endpoints',
|
||||
dependencies: ['auth'],
|
||||
priority: 3,
|
||||
},
|
||||
];
|
||||
|
||||
const result = resolveDependencies(features);
|
||||
|
||||
console.log(result.orderedFeatures);
|
||||
// [database, auth, api]
|
||||
|
||||
if (result.hasCycle) {
|
||||
console.error('Circular dependency detected!');
|
||||
console.error('Features in cycle:', result.cyclicFeatures);
|
||||
}
|
||||
```
|
||||
|
||||
### Check Dependencies Satisfied
|
||||
|
||||
Check if a feature's dependencies are satisfied.
|
||||
|
||||
```typescript
|
||||
import { areDependenciesSatisfied } from '@automaker/dependency-resolver';
|
||||
|
||||
const allFeatures: Feature[] = [
|
||||
{ id: 'database', status: 'completed', ... },
|
||||
{ id: 'auth', status: 'pending', dependencies: ['database'], ... }
|
||||
];
|
||||
|
||||
const authFeature = allFeatures.find(f => f.id === 'auth');
|
||||
|
||||
if (areDependenciesSatisfied(authFeature, allFeatures)) {
|
||||
console.log('Auth feature is ready to execute');
|
||||
} else {
|
||||
console.log('Waiting for dependencies');
|
||||
}
|
||||
```
|
||||
|
||||
### Get Blocking Dependencies
|
||||
|
||||
Get list of incomplete dependencies blocking a feature.
|
||||
|
||||
```typescript
|
||||
import { getBlockingDependencies } from '@automaker/dependency-resolver';
|
||||
|
||||
const blocking = getBlockingDependencies(feature, allFeatures);
|
||||
|
||||
if (blocking.length > 0) {
|
||||
console.log(`Feature blocked by: ${blocking.join(', ')}`);
|
||||
} else {
|
||||
console.log('No blocking dependencies');
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Example
|
||||
|
||||
```typescript
|
||||
import {
|
||||
resolveDependencies,
|
||||
areDependenciesSatisfied,
|
||||
getBlockingDependencies,
|
||||
} from '@automaker/dependency-resolver';
|
||||
import type { Feature } from '@automaker/types';
|
||||
|
||||
async function executeFeatures(features: Feature[]) {
|
||||
// Resolve dependency order
|
||||
const { orderedFeatures, hasCycle, cyclicFeatures } = resolveDependencies(features);
|
||||
|
||||
if (hasCycle) {
|
||||
throw new Error(`Circular dependency: ${cyclicFeatures.join(' → ')}`);
|
||||
}
|
||||
|
||||
// Execute in order
|
||||
for (const feature of orderedFeatures) {
|
||||
// Check if dependencies are satisfied
|
||||
if (!areDependenciesSatisfied(feature, features)) {
|
||||
const blocking = getBlockingDependencies(feature, features);
|
||||
console.log(`Skipping ${feature.id}, blocked by: ${blocking.join(', ')}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Execute feature
|
||||
console.log(`Executing: ${feature.id}`);
|
||||
await executeFeature(feature);
|
||||
|
||||
// Mark as completed
|
||||
feature.status = 'completed';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Algorithm
|
||||
|
||||
### Topological Sort (Kahn's Algorithm)
|
||||
|
||||
1. Calculate in-degree for each feature (number of dependencies)
|
||||
2. Start with features that have no dependencies (in-degree = 0)
|
||||
3. Process features in priority order
|
||||
4. Remove processed features from dependency graph
|
||||
5. Repeat until all features processed or cycle detected
|
||||
|
||||
### Priority Handling
|
||||
|
||||
- Features with lower priority numbers execute first
|
||||
- When multiple features have same in-degree, priority determines order
|
||||
- Features without explicit priority default to lowest priority
|
||||
|
||||
### Cycle Detection
|
||||
|
||||
- Detects circular dependencies
|
||||
- Returns affected features in cycle
|
||||
- Prevents infinite loops in execution
|
||||
|
||||
## Return Types
|
||||
|
||||
### DependencyResolutionResult
|
||||
|
||||
```typescript
|
||||
interface DependencyResolutionResult {
|
||||
orderedFeatures: Feature[]; // Features in execution order
|
||||
hasCycle: boolean; // True if circular dependency detected
|
||||
cyclicFeatures: string[]; // Feature IDs involved in cycle
|
||||
}
|
||||
```
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### Missing Dependencies
|
||||
|
||||
Features with dependencies on non-existent features are treated as if the dependency is satisfied (allows flexibility).
|
||||
|
||||
### Self-Dependencies
|
||||
|
||||
Features depending on themselves are detected as cycles.
|
||||
|
||||
### Empty Dependencies Array
|
||||
|
||||
Treated same as no dependencies - feature is ready immediately.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `@automaker/types` - Feature type definition
|
||||
|
||||
## Used By
|
||||
|
||||
- `@automaker/server` - Auto-mode feature execution
|
||||
- `@automaker/ui` - Board view feature ordering
|
||||
36
libs/dependency-resolver/package.json
Normal file
36
libs/dependency-resolver/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@automaker/dependency-resolver",
|
||||
"version": "1.0.0",
|
||||
"description": "Feature dependency resolution for AutoMaker",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"watch": "tsc --watch",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"keywords": [
|
||||
"automaker",
|
||||
"dependency",
|
||||
"resolver"
|
||||
],
|
||||
"author": "AutoMaker Team",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@automaker/types": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.0.16"
|
||||
}
|
||||
}
|
||||
11
libs/dependency-resolver/src/index.ts
Normal file
11
libs/dependency-resolver/src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @automaker/dependency-resolver
|
||||
* Feature dependency resolution for AutoMaker
|
||||
*/
|
||||
|
||||
export {
|
||||
resolveDependencies,
|
||||
areDependenciesSatisfied,
|
||||
getBlockingDependencies,
|
||||
type DependencyResolutionResult,
|
||||
} from './resolver.js';
|
||||
211
libs/dependency-resolver/src/resolver.ts
Normal file
211
libs/dependency-resolver/src/resolver.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Dependency Resolution Utility
|
||||
*
|
||||
* Provides topological sorting and dependency analysis for features.
|
||||
* Uses a modified Kahn's algorithm that respects both dependencies and priorities.
|
||||
*/
|
||||
|
||||
import type { Feature } from '@automaker/types';
|
||||
|
||||
export interface DependencyResolutionResult {
|
||||
orderedFeatures: Feature[]; // Features in dependency-aware order
|
||||
circularDependencies: string[][]; // Groups of IDs forming cycles
|
||||
missingDependencies: Map<string, string[]>; // featureId -> missing dep IDs
|
||||
blockedFeatures: Map<string, string[]>; // featureId -> blocking dep IDs (incomplete dependencies)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves feature dependencies using topological sort with priority-aware ordering.
|
||||
*
|
||||
* Algorithm:
|
||||
* 1. Build dependency graph and detect missing/blocked dependencies
|
||||
* 2. Apply Kahn's algorithm for topological sort
|
||||
* 3. Within same dependency level, sort by priority (1=high, 2=medium, 3=low)
|
||||
* 4. Detect circular dependencies for features that can't be ordered
|
||||
*
|
||||
* @param features - Array of features to order
|
||||
* @returns Resolution result with ordered features and dependency metadata
|
||||
*/
|
||||
export function resolveDependencies(features: Feature[]): DependencyResolutionResult {
|
||||
const featureMap = new Map<string, Feature>(features.map((f) => [f.id, f]));
|
||||
const inDegree = new Map<string, number>();
|
||||
const adjacencyList = new Map<string, string[]>(); // dependencyId -> [dependentIds]
|
||||
const missingDependencies = new Map<string, string[]>();
|
||||
const blockedFeatures = new Map<string, string[]>();
|
||||
|
||||
// Initialize graph structures
|
||||
for (const feature of features) {
|
||||
inDegree.set(feature.id, 0);
|
||||
adjacencyList.set(feature.id, []);
|
||||
}
|
||||
|
||||
// Build dependency graph and detect missing/blocked dependencies
|
||||
for (const feature of features) {
|
||||
const deps = feature.dependencies || [];
|
||||
for (const depId of deps) {
|
||||
if (!featureMap.has(depId)) {
|
||||
// Missing dependency - track it
|
||||
if (!missingDependencies.has(feature.id)) {
|
||||
missingDependencies.set(feature.id, []);
|
||||
}
|
||||
missingDependencies.get(feature.id)!.push(depId);
|
||||
} else {
|
||||
// Valid dependency - add edge to graph
|
||||
adjacencyList.get(depId)!.push(feature.id);
|
||||
inDegree.set(feature.id, (inDegree.get(feature.id) || 0) + 1);
|
||||
|
||||
// Check if dependency is incomplete (blocking)
|
||||
const depFeature = featureMap.get(depId)!;
|
||||
if (depFeature.status !== 'completed' && depFeature.status !== 'verified') {
|
||||
if (!blockedFeatures.has(feature.id)) {
|
||||
blockedFeatures.set(feature.id, []);
|
||||
}
|
||||
blockedFeatures.get(feature.id)!.push(depId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kahn's algorithm with priority-aware selection
|
||||
const queue: Feature[] = [];
|
||||
const orderedFeatures: Feature[] = [];
|
||||
|
||||
// Helper to sort features by priority (lower number = higher priority)
|
||||
const sortByPriority = (a: Feature, b: Feature) => (a.priority ?? 2) - (b.priority ?? 2);
|
||||
|
||||
// Start with features that have no dependencies (in-degree 0)
|
||||
for (const [id, degree] of inDegree) {
|
||||
if (degree === 0) {
|
||||
queue.push(featureMap.get(id)!);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort initial queue by priority
|
||||
queue.sort(sortByPriority);
|
||||
|
||||
// Process features in topological order
|
||||
while (queue.length > 0) {
|
||||
// Take highest priority feature from queue
|
||||
const current = queue.shift()!;
|
||||
orderedFeatures.push(current);
|
||||
|
||||
// Process features that depend on this one
|
||||
for (const dependentId of adjacencyList.get(current.id) || []) {
|
||||
const currentDegree = inDegree.get(dependentId);
|
||||
if (currentDegree === undefined) {
|
||||
throw new Error(`In-degree not initialized for feature ${dependentId}`);
|
||||
}
|
||||
const newDegree = currentDegree - 1;
|
||||
inDegree.set(dependentId, newDegree);
|
||||
|
||||
if (newDegree === 0) {
|
||||
queue.push(featureMap.get(dependentId)!);
|
||||
// Re-sort queue to maintain priority order
|
||||
queue.sort(sortByPriority);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detect circular dependencies (features not in output = part of cycle)
|
||||
const circularDependencies: string[][] = [];
|
||||
const processedIds = new Set(orderedFeatures.map((f) => f.id));
|
||||
|
||||
if (orderedFeatures.length < features.length) {
|
||||
// Find cycles using DFS
|
||||
const remaining = features.filter((f) => !processedIds.has(f.id));
|
||||
const cycles = detectCycles(remaining, featureMap);
|
||||
circularDependencies.push(...cycles);
|
||||
|
||||
// Add remaining features at end (part of cycles)
|
||||
orderedFeatures.push(...remaining);
|
||||
}
|
||||
|
||||
return {
|
||||
orderedFeatures,
|
||||
circularDependencies,
|
||||
missingDependencies,
|
||||
blockedFeatures,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects circular dependencies using depth-first search
|
||||
*
|
||||
* @param features - Features that couldn't be topologically sorted (potential cycles)
|
||||
* @param featureMap - Map of all features by ID
|
||||
* @returns Array of cycles, where each cycle is an array of feature IDs
|
||||
*/
|
||||
function detectCycles(features: Feature[], featureMap: Map<string, Feature>): string[][] {
|
||||
const cycles: string[][] = [];
|
||||
const visited = new Set<string>();
|
||||
const recursionStack = new Set<string>();
|
||||
const currentPath: string[] = [];
|
||||
|
||||
function dfs(featureId: string): boolean {
|
||||
visited.add(featureId);
|
||||
recursionStack.add(featureId);
|
||||
currentPath.push(featureId);
|
||||
|
||||
const feature = featureMap.get(featureId);
|
||||
if (feature) {
|
||||
for (const depId of feature.dependencies || []) {
|
||||
if (!visited.has(depId)) {
|
||||
if (dfs(depId)) return true;
|
||||
} else if (recursionStack.has(depId)) {
|
||||
// Found cycle - extract it
|
||||
const cycleStart = currentPath.indexOf(depId);
|
||||
cycles.push(currentPath.slice(cycleStart));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentPath.pop();
|
||||
recursionStack.delete(featureId);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const feature of features) {
|
||||
if (!visited.has(feature.id)) {
|
||||
dfs(feature.id);
|
||||
}
|
||||
}
|
||||
|
||||
return cycles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a feature's dependencies are satisfied (all complete or verified)
|
||||
*
|
||||
* @param feature - Feature to check
|
||||
* @param allFeatures - All features in the project
|
||||
* @returns true if all dependencies are satisfied, false otherwise
|
||||
*/
|
||||
export function areDependenciesSatisfied(feature: Feature, allFeatures: Feature[]): boolean {
|
||||
if (!feature.dependencies || feature.dependencies.length === 0) {
|
||||
return true; // No dependencies = always ready
|
||||
}
|
||||
|
||||
return feature.dependencies.every((depId: string) => {
|
||||
const dep = allFeatures.find((f) => f.id === depId);
|
||||
return dep && (dep.status === 'completed' || dep.status === 'verified');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the blocking dependencies for a feature (dependencies that are incomplete)
|
||||
*
|
||||
* @param feature - Feature to check
|
||||
* @param allFeatures - All features in the project
|
||||
* @returns Array of feature IDs that are blocking this feature
|
||||
*/
|
||||
export function getBlockingDependencies(feature: Feature, allFeatures: Feature[]): string[] {
|
||||
if (!feature.dependencies || feature.dependencies.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return feature.dependencies.filter((depId: string) => {
|
||||
const dep = allFeatures.find((f) => f.id === depId);
|
||||
return dep && dep.status !== 'completed' && dep.status !== 'verified';
|
||||
});
|
||||
}
|
||||
351
libs/dependency-resolver/tests/resolver.test.ts
Normal file
351
libs/dependency-resolver/tests/resolver.test.ts
Normal file
@@ -0,0 +1,351 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
resolveDependencies,
|
||||
areDependenciesSatisfied,
|
||||
getBlockingDependencies,
|
||||
} from '../src/resolver';
|
||||
import type { Feature } from '@automaker/types';
|
||||
|
||||
// Helper to create test features
|
||||
function createFeature(
|
||||
id: string,
|
||||
options: {
|
||||
dependencies?: string[];
|
||||
status?: string;
|
||||
priority?: number;
|
||||
} = {}
|
||||
): Feature {
|
||||
return {
|
||||
id,
|
||||
category: 'test',
|
||||
description: `Feature ${id}`,
|
||||
dependencies: options.dependencies,
|
||||
status: options.status || 'pending',
|
||||
priority: options.priority,
|
||||
};
|
||||
}
|
||||
|
||||
describe('resolver.ts', () => {
|
||||
describe('resolveDependencies', () => {
|
||||
it('should handle features with no dependencies', () => {
|
||||
const features = [createFeature('A'), createFeature('B'), createFeature('C')];
|
||||
|
||||
const result = resolveDependencies(features);
|
||||
|
||||
expect(result.orderedFeatures).toHaveLength(3);
|
||||
expect(result.circularDependencies).toEqual([]);
|
||||
expect(result.missingDependencies.size).toBe(0);
|
||||
expect(result.blockedFeatures.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should order features with linear dependencies', () => {
|
||||
const features = [
|
||||
createFeature('C', { dependencies: ['B'] }),
|
||||
createFeature('A'),
|
||||
createFeature('B', { dependencies: ['A'] }),
|
||||
];
|
||||
|
||||
const result = resolveDependencies(features);
|
||||
|
||||
const ids = result.orderedFeatures.map((f) => f.id);
|
||||
expect(ids.indexOf('A')).toBeLessThan(ids.indexOf('B'));
|
||||
expect(ids.indexOf('B')).toBeLessThan(ids.indexOf('C'));
|
||||
expect(result.circularDependencies).toEqual([]);
|
||||
});
|
||||
|
||||
it('should respect priority within same dependency level', () => {
|
||||
const features = [
|
||||
createFeature('Low', { priority: 3 }),
|
||||
createFeature('High', { priority: 1 }),
|
||||
createFeature('Medium', { priority: 2 }),
|
||||
];
|
||||
|
||||
const result = resolveDependencies(features);
|
||||
|
||||
const ids = result.orderedFeatures.map((f) => f.id);
|
||||
expect(ids).toEqual(['High', 'Medium', 'Low']);
|
||||
});
|
||||
|
||||
it('should use default priority 2 when not specified', () => {
|
||||
const features = [
|
||||
createFeature('NoPriority'),
|
||||
createFeature('HighPriority', { priority: 1 }),
|
||||
createFeature('LowPriority', { priority: 3 }),
|
||||
];
|
||||
|
||||
const result = resolveDependencies(features);
|
||||
|
||||
const ids = result.orderedFeatures.map((f) => f.id);
|
||||
expect(ids.indexOf('HighPriority')).toBeLessThan(ids.indexOf('NoPriority'));
|
||||
expect(ids.indexOf('NoPriority')).toBeLessThan(ids.indexOf('LowPriority'));
|
||||
});
|
||||
|
||||
it('should respect dependencies over priority', () => {
|
||||
const features = [
|
||||
createFeature('B', { dependencies: ['A'], priority: 1 }), // High priority but depends on A
|
||||
createFeature('A', { priority: 3 }), // Low priority but no dependencies
|
||||
];
|
||||
|
||||
const result = resolveDependencies(features);
|
||||
|
||||
const ids = result.orderedFeatures.map((f) => f.id);
|
||||
expect(ids.indexOf('A')).toBeLessThan(ids.indexOf('B'));
|
||||
});
|
||||
|
||||
it('should detect circular dependencies (simple cycle)', () => {
|
||||
const features = [
|
||||
createFeature('A', { dependencies: ['B'] }),
|
||||
createFeature('B', { dependencies: ['A'] }),
|
||||
];
|
||||
|
||||
const result = resolveDependencies(features);
|
||||
|
||||
expect(result.circularDependencies).toHaveLength(1);
|
||||
expect(result.circularDependencies[0]).toContain('A');
|
||||
expect(result.circularDependencies[0]).toContain('B');
|
||||
expect(result.orderedFeatures).toHaveLength(2); // All features still included
|
||||
});
|
||||
|
||||
it('should detect circular dependencies (3-way cycle)', () => {
|
||||
const features = [
|
||||
createFeature('A', { dependencies: ['C'] }),
|
||||
createFeature('B', { dependencies: ['A'] }),
|
||||
createFeature('C', { dependencies: ['B'] }),
|
||||
];
|
||||
|
||||
const result = resolveDependencies(features);
|
||||
|
||||
expect(result.circularDependencies.length).toBeGreaterThan(0);
|
||||
const allCycleIds = result.circularDependencies.flat();
|
||||
expect(allCycleIds).toContain('A');
|
||||
expect(allCycleIds).toContain('B');
|
||||
expect(allCycleIds).toContain('C');
|
||||
});
|
||||
|
||||
it('should detect missing dependencies', () => {
|
||||
const features = [createFeature('A', { dependencies: ['NonExistent'] }), createFeature('B')];
|
||||
|
||||
const result = resolveDependencies(features);
|
||||
|
||||
expect(result.missingDependencies.has('A')).toBe(true);
|
||||
expect(result.missingDependencies.get('A')).toContain('NonExistent');
|
||||
});
|
||||
|
||||
it('should detect blocked features (incomplete dependencies)', () => {
|
||||
const features = [
|
||||
createFeature('A', { status: 'pending' }),
|
||||
createFeature('B', { dependencies: ['A'], status: 'pending' }),
|
||||
];
|
||||
|
||||
const result = resolveDependencies(features);
|
||||
|
||||
expect(result.blockedFeatures.has('B')).toBe(true);
|
||||
expect(result.blockedFeatures.get('B')).toContain('A');
|
||||
});
|
||||
|
||||
it('should not mark features as blocked if dependencies are completed', () => {
|
||||
const features = [
|
||||
createFeature('A', { status: 'completed' }),
|
||||
createFeature('B', { dependencies: ['A'], status: 'pending' }),
|
||||
];
|
||||
|
||||
const result = resolveDependencies(features);
|
||||
|
||||
expect(result.blockedFeatures.has('B')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not mark features as blocked if dependencies are verified', () => {
|
||||
const features = [
|
||||
createFeature('A', { status: 'verified' }),
|
||||
createFeature('B', { dependencies: ['A'], status: 'pending' }),
|
||||
];
|
||||
|
||||
const result = resolveDependencies(features);
|
||||
|
||||
expect(result.blockedFeatures.has('B')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle complex dependency graph', () => {
|
||||
const features = [
|
||||
createFeature('E', { dependencies: ['C', 'D'] }),
|
||||
createFeature('D', { dependencies: ['B'] }),
|
||||
createFeature('C', { dependencies: ['A', 'B'] }),
|
||||
createFeature('B'),
|
||||
createFeature('A'),
|
||||
];
|
||||
|
||||
const result = resolveDependencies(features);
|
||||
|
||||
const ids = result.orderedFeatures.map((f) => f.id);
|
||||
|
||||
// A and B have no dependencies - can be first or second
|
||||
expect(ids.indexOf('A')).toBeLessThan(ids.indexOf('C'));
|
||||
expect(ids.indexOf('B')).toBeLessThan(ids.indexOf('C'));
|
||||
expect(ids.indexOf('B')).toBeLessThan(ids.indexOf('D'));
|
||||
|
||||
// C depends on A and B
|
||||
expect(ids.indexOf('C')).toBeLessThan(ids.indexOf('E'));
|
||||
|
||||
// D depends on B
|
||||
expect(ids.indexOf('D')).toBeLessThan(ids.indexOf('E'));
|
||||
|
||||
expect(result.circularDependencies).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle multiple missing dependencies', () => {
|
||||
const features = [createFeature('A', { dependencies: ['X', 'Y', 'Z'] })];
|
||||
|
||||
const result = resolveDependencies(features);
|
||||
|
||||
expect(result.missingDependencies.get('A')).toEqual(['X', 'Y', 'Z']);
|
||||
});
|
||||
|
||||
it('should handle empty feature list', () => {
|
||||
const result = resolveDependencies([]);
|
||||
|
||||
expect(result.orderedFeatures).toEqual([]);
|
||||
expect(result.circularDependencies).toEqual([]);
|
||||
expect(result.missingDependencies.size).toBe(0);
|
||||
expect(result.blockedFeatures.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle features with both missing and existing dependencies', () => {
|
||||
const features = [
|
||||
createFeature('A'),
|
||||
createFeature('B', { dependencies: ['A', 'NonExistent'] }),
|
||||
];
|
||||
|
||||
const result = resolveDependencies(features);
|
||||
|
||||
expect(result.missingDependencies.get('B')).toContain('NonExistent');
|
||||
const ids = result.orderedFeatures.map((f) => f.id);
|
||||
expect(ids.indexOf('A')).toBeLessThan(ids.indexOf('B'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('areDependenciesSatisfied', () => {
|
||||
it('should return true for feature with no dependencies', () => {
|
||||
const feature = createFeature('A');
|
||||
const allFeatures = [feature];
|
||||
|
||||
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for feature with empty dependencies array', () => {
|
||||
const feature = createFeature('A', { dependencies: [] });
|
||||
const allFeatures = [feature];
|
||||
|
||||
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when all dependencies are completed', () => {
|
||||
const dep = createFeature('Dep', { status: 'completed' });
|
||||
const feature = createFeature('A', { dependencies: ['Dep'] });
|
||||
const allFeatures = [dep, feature];
|
||||
|
||||
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when all dependencies are verified', () => {
|
||||
const dep = createFeature('Dep', { status: 'verified' });
|
||||
const feature = createFeature('A', { dependencies: ['Dep'] });
|
||||
const allFeatures = [dep, feature];
|
||||
|
||||
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when any dependency is pending', () => {
|
||||
const dep = createFeature('Dep', { status: 'pending' });
|
||||
const feature = createFeature('A', { dependencies: ['Dep'] });
|
||||
const allFeatures = [dep, feature];
|
||||
|
||||
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when any dependency is running', () => {
|
||||
const dep = createFeature('Dep', { status: 'running' });
|
||||
const feature = createFeature('A', { dependencies: ['Dep'] });
|
||||
const allFeatures = [dep, feature];
|
||||
|
||||
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when dependency is missing', () => {
|
||||
const feature = createFeature('A', { dependencies: ['NonExistent'] });
|
||||
const allFeatures = [feature];
|
||||
|
||||
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(false);
|
||||
});
|
||||
|
||||
it('should check all dependencies', () => {
|
||||
const dep1 = createFeature('Dep1', { status: 'completed' });
|
||||
const dep2 = createFeature('Dep2', { status: 'pending' });
|
||||
const feature = createFeature('A', { dependencies: ['Dep1', 'Dep2'] });
|
||||
const allFeatures = [dep1, dep2, feature];
|
||||
|
||||
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBlockingDependencies', () => {
|
||||
it('should return empty array for feature with no dependencies', () => {
|
||||
const feature = createFeature('A');
|
||||
const allFeatures = [feature];
|
||||
|
||||
expect(getBlockingDependencies(feature, allFeatures)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when all dependencies are completed', () => {
|
||||
const dep = createFeature('Dep', { status: 'completed' });
|
||||
const feature = createFeature('A', { dependencies: ['Dep'] });
|
||||
const allFeatures = [dep, feature];
|
||||
|
||||
expect(getBlockingDependencies(feature, allFeatures)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when all dependencies are verified', () => {
|
||||
const dep = createFeature('Dep', { status: 'verified' });
|
||||
const feature = createFeature('A', { dependencies: ['Dep'] });
|
||||
const allFeatures = [dep, feature];
|
||||
|
||||
expect(getBlockingDependencies(feature, allFeatures)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return pending dependencies', () => {
|
||||
const dep = createFeature('Dep', { status: 'pending' });
|
||||
const feature = createFeature('A', { dependencies: ['Dep'] });
|
||||
const allFeatures = [dep, feature];
|
||||
|
||||
expect(getBlockingDependencies(feature, allFeatures)).toEqual(['Dep']);
|
||||
});
|
||||
|
||||
it('should return running dependencies', () => {
|
||||
const dep = createFeature('Dep', { status: 'running' });
|
||||
const feature = createFeature('A', { dependencies: ['Dep'] });
|
||||
const allFeatures = [dep, feature];
|
||||
|
||||
expect(getBlockingDependencies(feature, allFeatures)).toEqual(['Dep']);
|
||||
});
|
||||
|
||||
it('should return failed dependencies', () => {
|
||||
const dep = createFeature('Dep', { status: 'failed' });
|
||||
const feature = createFeature('A', { dependencies: ['Dep'] });
|
||||
const allFeatures = [dep, feature];
|
||||
|
||||
expect(getBlockingDependencies(feature, allFeatures)).toEqual(['Dep']);
|
||||
});
|
||||
|
||||
it('should return all incomplete dependencies', () => {
|
||||
const dep1 = createFeature('Dep1', { status: 'pending' });
|
||||
const dep2 = createFeature('Dep2', { status: 'completed' });
|
||||
const dep3 = createFeature('Dep3', { status: 'running' });
|
||||
const feature = createFeature('A', { dependencies: ['Dep1', 'Dep2', 'Dep3'] });
|
||||
const allFeatures = [dep1, dep2, dep3, feature];
|
||||
|
||||
const blocking = getBlockingDependencies(feature, allFeatures);
|
||||
expect(blocking).toContain('Dep1');
|
||||
expect(blocking).toContain('Dep3');
|
||||
expect(blocking).not.toContain('Dep2');
|
||||
});
|
||||
});
|
||||
});
|
||||
9
libs/dependency-resolver/tsconfig.json
Normal file
9
libs/dependency-resolver/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
21
libs/dependency-resolver/vitest.config.ts
Normal file
21
libs/dependency-resolver/vitest.config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['tests/**/*.test.ts'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
include: ['src/**/*.ts'],
|
||||
exclude: ['src/**/*.d.ts', 'src/index.ts'],
|
||||
thresholds: {
|
||||
lines: 90,
|
||||
functions: 100,
|
||||
branches: 85,
|
||||
statements: 90,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user