mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
fix: Treat cancelled tasks as complete in statistics and dependencies (#1393)
* fix: treat cancelled tasks as complete in statistics and dependencies Fixes #1392 ## Changes - Add TERMINAL_COMPLETE_STATUSES constant including 'done', 'completed', 'cancelled' - Update calculateTaskStatistics() to include cancelled tasks in completion % - Update calculateSubtaskStatistics() to include cancelled subtasks in completion % - Update calculateDependencyStatistics() to treat cancelled tasks as satisfied dependencies - Update findNextTask() to treat cancelled tasks as complete for dependency resolution ## Impact - Dashboard now correctly shows 100% for projects with only done/cancelled tasks - Tasks depending on cancelled tasks are no longer permanently blocked - Dependency metrics accurately reflect project state ## Tests - Added comprehensive test suite with RED-GREEN-REFACTOR approach - 9 tests covering completion %, subtask %, and dependency resolution - All tests pass after fixes applied * style: apply biome formatting * style: apply biome formatting to all files * fix: display fraction should show all terminal complete tasks, not just done Previously the completion percentage correctly counted cancelled tasks as complete (e.g., 100% for 14 done + 1 cancelled), but the fraction display used stats.done which only counted 'done' status, showing "100% 14/15" instead of "100% 15/15". Added completedCount field to TaskStatistics interface to track all terminal complete tasks (done + completed + cancelled) and updated display to use this field for accurate fraction representation. Updates: - TaskStatistics interface now includes completedCount field - calculateTaskStatistics() sets completedCount for display - calculateSubtaskStatistics() sets completedCount for display - Display lines now use completedCount instead of done count - Added completedCount assertions to all relevant tests Related to #1392 * refactor: centralize terminal status logic in @tm/core Following code review feedback, extracted TERMINAL_COMPLETE_STATUSES and isTaskComplete() to @tm/core as single source of truth. This prevents duplication and potential drift between CLI and MCP layers. Changes: - Added TERMINAL_COMPLETE_STATUSES constant to @tm/core constants - Added isTaskComplete() helper function to @tm/core constants - Updated dashboard.component.ts to import from tm-core via CLI util - Updated list.command.ts to import from tm-core via CLI util - Updated task-loader.service.ts to use shared isTaskComplete() - Created CLI convenience re-export in utils/task-status.ts Architecture: - Business logic now lives in @tm/core (proper layer) - CLI provides convenience re-exports for ergonomics - MCP already delegates to @tm/core (no changes needed) Addresses review comments from @Crunchyman-ralph on PR #1393 * fix: remove unused TaskStatus import in task-loader.service TypeScript error: TaskStatus was imported but no longer used after refactoring to use isTaskComplete() helper. * fix: correct subtask display and progress bar for cancelled tasks Fixed two display bugs identified by cursor bot: 1. Subtask completion display now shows completedCount instead of done - Before: "Completed: 3/4" (only counted done tasks) - After: "Completed: 4/4" (counts done + cancelled as terminal complete) 2. Progress bar no longer double-counts cancelled tasks - Before: Green section (completionPercentage) + gray cancelled section - After: Green section includes cancelled (as terminal complete status) - Removed separate cancelled section from progress bar - Updated section numbers in comments (2-6 instead of 2-7) These changes ensure visual consistency with the fix that treats cancelled tasks as terminal complete for percentage calculations and dependency resolution. Fixes cursor bot comments on PR #1393 * style: run biome formatter * fix: keep cancelled tasks visually distinct in progress bar Per reviewer feedback from Crunchyman-ralph, cancelled tasks should remain visible as a separate gray section in the progress bar for better visibility, even though they count as complete for the percentage calculation. Changes: - Green section shows 'done' tasks only - Gray section shows 'cancelled' tasks (visually distinct but terminal complete) - completionPercentage still includes both done and cancelled (correct) - This prevents double-counting while maintaining visual clarity Addresses: https://github.com/eyaltoledano/claude-task-master/pull/1393#issuecomment-3517115852 * fix: add done property to StatusBreakdown interface TypeScript error: StatusBreakdown interface was missing 'done' property required by the progress bar visualization code. * fix: Populate done field in calculateStatusBreakdown for progress bar The calculateStatusBreakdown() function was missing the calculation for the 'done' field, causing statusBreakdown.done to always be undefined. This meant the green section of the progress bar never rendered, making completed tasks invisible in the visual representation. Addresses cursor bot feedback on PR #1393. --------- Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
This commit is contained in:
5
.changeset/fix-cancelled-completion.md
Normal file
5
.changeset/fix-cancelled-completion.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Fix completion percentage and dependency resolution to treat cancelled tasks as complete. Cancelled tasks now correctly count toward project completion (e.g., 14 done + 1 cancelled = 100%, not 93%) and satisfy dependencies for dependent tasks, preventing permanent blocks.
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import { displayCommandHeader } from '../utils/display-helpers.js';
|
||||
import { displayError } from '../utils/error-handler.js';
|
||||
import { getProjectRoot } from '../utils/project-root.js';
|
||||
import { isTaskComplete } from '../utils/task-status.js';
|
||||
import * as ui from '../utils/ui.js';
|
||||
|
||||
/**
|
||||
@@ -344,12 +345,12 @@ export class ListTasksCommand extends Command {
|
||||
// Build set of completed task IDs (including subtasks)
|
||||
const completedIds = new Set<string>();
|
||||
tasks.forEach((t) => {
|
||||
if (t.status === 'done' || t.status === 'completed') {
|
||||
if (isTaskComplete(t.status)) {
|
||||
completedIds.add(String(t.id));
|
||||
}
|
||||
if (t.subtasks) {
|
||||
t.subtasks.forEach((st) => {
|
||||
if (st.status === 'done' || st.status === 'completed') {
|
||||
if (isTaskComplete(st.status as TaskStatus)) {
|
||||
completedIds.add(`${t.id}.${st.id}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
* Displays project statistics and dependency information
|
||||
*/
|
||||
|
||||
import type { Task, TaskPriority } from '@tm/core';
|
||||
import type { Task, TaskPriority, TaskStatus } from '@tm/core';
|
||||
import boxen from 'boxen';
|
||||
import chalk from 'chalk';
|
||||
import { isTaskComplete } from '../../utils/task-status.js';
|
||||
import { getComplexityWithColor } from '../../utils/ui.js';
|
||||
|
||||
/**
|
||||
@@ -21,6 +22,8 @@ export interface TaskStatistics {
|
||||
cancelled: number;
|
||||
review?: number;
|
||||
completionPercentage: number;
|
||||
/** Count of all terminal complete tasks (done + completed + cancelled) */
|
||||
completedCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,6 +53,7 @@ export interface NextTaskInfo {
|
||||
* Status breakdown for progress bars
|
||||
*/
|
||||
export interface StatusBreakdown {
|
||||
done?: number;
|
||||
'in-progress'?: number;
|
||||
pending?: number;
|
||||
blocked?: number;
|
||||
@@ -78,14 +82,17 @@ function createProgressBar(
|
||||
let bar = '';
|
||||
let charsUsed = 0;
|
||||
|
||||
// 1. Green filled blocks for completed tasks (done)
|
||||
const completedChars = Math.round((completionPercentage / 100) * width);
|
||||
if (completedChars > 0) {
|
||||
bar += chalk.green('█').repeat(completedChars);
|
||||
charsUsed += completedChars;
|
||||
// 1. Green filled blocks for done tasks only
|
||||
// Note: completionPercentage includes cancelled, but we show them separately in the bar
|
||||
if (statusBreakdown.done && statusBreakdown.done > 0) {
|
||||
const doneChars = Math.round((statusBreakdown.done / 100) * width);
|
||||
if (doneChars > 0) {
|
||||
bar += chalk.green('█').repeat(doneChars);
|
||||
charsUsed += doneChars;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Gray filled blocks for cancelled (won't be done)
|
||||
// 2. Gray filled blocks for cancelled (terminal complete, but visually distinct)
|
||||
if (statusBreakdown.cancelled && charsUsed < width) {
|
||||
const cancelledChars = Math.round(
|
||||
(statusBreakdown.cancelled / 100) * width
|
||||
@@ -170,7 +177,8 @@ export function calculateTaskStatistics(tasks: Task[]): TaskStatistics {
|
||||
deferred: 0,
|
||||
cancelled: 0,
|
||||
review: 0,
|
||||
completionPercentage: 0
|
||||
completionPercentage: 0,
|
||||
completedCount: 0
|
||||
};
|
||||
|
||||
tasks.forEach((task) => {
|
||||
@@ -199,8 +207,12 @@ export function calculateTaskStatistics(tasks: Task[]): TaskStatistics {
|
||||
}
|
||||
});
|
||||
|
||||
// Count terminal complete tasks for percentage calculation and display
|
||||
stats.completedCount = tasks.filter((t) => isTaskComplete(t.status)).length;
|
||||
stats.completionPercentage =
|
||||
stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0;
|
||||
stats.total > 0
|
||||
? Math.round((stats.completedCount / stats.total) * 100)
|
||||
: 0;
|
||||
|
||||
return stats;
|
||||
}
|
||||
@@ -218,13 +230,16 @@ export function calculateSubtaskStatistics(tasks: Task[]): TaskStatistics {
|
||||
deferred: 0,
|
||||
cancelled: 0,
|
||||
review: 0,
|
||||
completionPercentage: 0
|
||||
completionPercentage: 0,
|
||||
completedCount: 0
|
||||
};
|
||||
|
||||
const allSubtasks: Array<{ status: string }> = [];
|
||||
tasks.forEach((task) => {
|
||||
if (task.subtasks && task.subtasks.length > 0) {
|
||||
task.subtasks.forEach((subtask) => {
|
||||
stats.total++;
|
||||
allSubtasks.push(subtask);
|
||||
switch (subtask.status) {
|
||||
case 'done':
|
||||
stats.done++;
|
||||
@@ -252,8 +267,14 @@ export function calculateSubtaskStatistics(tasks: Task[]): TaskStatistics {
|
||||
}
|
||||
});
|
||||
|
||||
// Count terminal complete subtasks for percentage calculation and display
|
||||
stats.completedCount = allSubtasks.filter((st) =>
|
||||
isTaskComplete(st.status as TaskStatus)
|
||||
).length;
|
||||
stats.completionPercentage =
|
||||
stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0;
|
||||
stats.total > 0
|
||||
? Math.round((stats.completedCount / stats.total) * 100)
|
||||
: 0;
|
||||
|
||||
return stats;
|
||||
}
|
||||
@@ -264,18 +285,20 @@ export function calculateSubtaskStatistics(tasks: Task[]): TaskStatistics {
|
||||
export function calculateDependencyStatistics(
|
||||
tasks: Task[]
|
||||
): DependencyStatistics {
|
||||
// Get all terminal complete task IDs - these satisfy dependencies
|
||||
const completedTaskIds = new Set(
|
||||
tasks.filter((t) => t.status === 'done').map((t) => t.id)
|
||||
tasks.filter((t) => isTaskComplete(t.status)).map((t) => t.id)
|
||||
);
|
||||
|
||||
const tasksWithNoDeps = tasks.filter(
|
||||
(t) =>
|
||||
t.status !== 'done' && (!t.dependencies || t.dependencies.length === 0)
|
||||
!isTaskComplete(t.status) &&
|
||||
(!t.dependencies || t.dependencies.length === 0)
|
||||
).length;
|
||||
|
||||
const tasksWithAllDepsSatisfied = tasks.filter(
|
||||
(t) =>
|
||||
t.status !== 'done' &&
|
||||
!isTaskComplete(t.status) &&
|
||||
t.dependencies &&
|
||||
t.dependencies.length > 0 &&
|
||||
t.dependencies.every((depId) => completedTaskIds.has(depId))
|
||||
@@ -283,7 +306,7 @@ export function calculateDependencyStatistics(
|
||||
|
||||
const tasksBlockedByDeps = tasks.filter(
|
||||
(t) =>
|
||||
t.status !== 'done' &&
|
||||
!isTaskComplete(t.status) &&
|
||||
t.dependencies &&
|
||||
t.dependencies.length > 0 &&
|
||||
!t.dependencies.every((depId) => completedTaskIds.has(depId))
|
||||
@@ -356,6 +379,7 @@ function calculateStatusBreakdown(stats: TaskStatistics): StatusBreakdown {
|
||||
if (stats.total === 0) return {};
|
||||
|
||||
return {
|
||||
done: (stats.done / stats.total) * 100,
|
||||
'in-progress': (stats.inProgress / stats.total) * 100,
|
||||
pending: (stats.pending / stats.total) * 100,
|
||||
blocked: (stats.blocked / stats.total) * 100,
|
||||
@@ -378,7 +402,9 @@ function formatStatusLine(
|
||||
|
||||
// Order: Done, Cancelled, Deferred, In Progress, Review, Pending, Blocked
|
||||
if (isSubtask) {
|
||||
parts.push(`Completed: ${chalk.green(`${stats.done}/${stats.total}`)}`);
|
||||
parts.push(
|
||||
`Completed: ${chalk.green(`${stats.completedCount}/${stats.total}`)}`
|
||||
);
|
||||
} else {
|
||||
parts.push(`Done: ${chalk.green(stats.done)}`);
|
||||
}
|
||||
@@ -424,8 +450,8 @@ export function displayProjectDashboard(
|
||||
subtaskStatusBreakdown
|
||||
);
|
||||
|
||||
const taskPercentage = `${taskStats.completionPercentage}% ${taskStats.done}/${taskStats.total}`;
|
||||
const subtaskPercentage = `${subtaskStats.completionPercentage}% ${subtaskStats.done}/${subtaskStats.total}`;
|
||||
const taskPercentage = `${taskStats.completionPercentage}% ${taskStats.completedCount}/${taskStats.total}`;
|
||||
const subtaskPercentage = `${subtaskStats.completionPercentage}% ${subtaskStats.completedCount}/${subtaskStats.total}`;
|
||||
|
||||
const content =
|
||||
chalk.white.bold('Project Dashboard') +
|
||||
|
||||
10
apps/cli/src/utils/task-status.ts
Normal file
10
apps/cli/src/utils/task-status.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* @fileoverview Utilities for working with task statuses
|
||||
* Re-exports status utilities from @tm/core for CLI convenience
|
||||
*/
|
||||
|
||||
// Re-export terminal status utilities from @tm/core (single source of truth)
|
||||
export {
|
||||
TERMINAL_COMPLETE_STATUSES,
|
||||
isTaskComplete
|
||||
} from '@tm/core';
|
||||
@@ -2,8 +2,8 @@
|
||||
* @fileoverview Integration tests for autopilot workflow commands
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import type { WorkflowState } from '@tm/core';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// Track file system state in memory - must be in vi.hoisted() for mock access
|
||||
const {
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
* @fileoverview Unit tests for autopilot shared utilities
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
validateTaskId,
|
||||
OutputFormatter,
|
||||
parseSubtasks,
|
||||
OutputFormatter
|
||||
validateTaskId
|
||||
} from '../../../../src/commands/autopilot/shared.js';
|
||||
|
||||
// Mock fs-extra
|
||||
|
||||
287
apps/cli/tests/unit/ui/dashboard.component.spec.ts
Normal file
287
apps/cli/tests/unit/ui/dashboard.component.spec.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* @fileoverview Tests for dashboard component calculations
|
||||
* Bug fix: Cancelled tasks should be treated as complete
|
||||
*/
|
||||
|
||||
import type { Task } from '@tm/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
type TaskStatistics,
|
||||
calculateDependencyStatistics,
|
||||
calculateSubtaskStatistics,
|
||||
calculateTaskStatistics
|
||||
} from '../../../src/ui/components/dashboard.component.js';
|
||||
|
||||
describe('dashboard.component - Bug Fix: Cancelled Tasks as Complete', () => {
|
||||
describe('calculateTaskStatistics', () => {
|
||||
it('should treat cancelled tasks as complete in percentage calculation', () => {
|
||||
// Arrange: 14 done, 1 cancelled = 100% complete
|
||||
const tasks: Task[] = [
|
||||
...Array.from({ length: 14 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
title: `Task ${i + 1}`,
|
||||
status: 'done' as const,
|
||||
dependencies: []
|
||||
})),
|
||||
{
|
||||
id: 15,
|
||||
title: 'Cancelled Task',
|
||||
status: 'cancelled' as const,
|
||||
dependencies: []
|
||||
}
|
||||
];
|
||||
|
||||
// Act
|
||||
const stats = calculateTaskStatistics(tasks);
|
||||
|
||||
// Assert
|
||||
expect(stats.total).toBe(15);
|
||||
expect(stats.done).toBe(14);
|
||||
expect(stats.cancelled).toBe(1);
|
||||
expect(stats.completedCount).toBe(15); // done + cancelled
|
||||
// BUG: Current code shows 93% (14/15), should be 100% (15/15)
|
||||
expect(stats.completionPercentage).toBe(100);
|
||||
});
|
||||
|
||||
it('should treat completed status as complete in percentage calculation', () => {
|
||||
// Arrange: Mix of done, completed, cancelled
|
||||
const tasks: Task[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Done Task',
|
||||
status: 'done' as const,
|
||||
dependencies: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Completed Task',
|
||||
status: 'completed' as const,
|
||||
dependencies: []
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Cancelled Task',
|
||||
status: 'cancelled' as const,
|
||||
dependencies: []
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Pending Task',
|
||||
status: 'pending' as const,
|
||||
dependencies: []
|
||||
}
|
||||
];
|
||||
|
||||
// Act
|
||||
const stats = calculateTaskStatistics(tasks);
|
||||
|
||||
// Assert
|
||||
expect(stats.total).toBe(4);
|
||||
expect(stats.done).toBe(1);
|
||||
expect(stats.cancelled).toBe(1);
|
||||
expect(stats.completedCount).toBe(3); // done + completed + cancelled
|
||||
// 3 complete out of 4 total = 75%
|
||||
expect(stats.completionPercentage).toBe(75);
|
||||
});
|
||||
|
||||
it('should show 100% completion when all tasks are cancelled', () => {
|
||||
// Arrange
|
||||
const tasks: Task[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Cancelled 1',
|
||||
status: 'cancelled' as const,
|
||||
dependencies: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Cancelled 2',
|
||||
status: 'cancelled' as const,
|
||||
dependencies: []
|
||||
}
|
||||
];
|
||||
|
||||
// Act
|
||||
const stats = calculateTaskStatistics(tasks);
|
||||
|
||||
// Assert
|
||||
expect(stats.total).toBe(2);
|
||||
expect(stats.cancelled).toBe(2);
|
||||
expect(stats.completedCount).toBe(2); // All cancelled = all complete
|
||||
// BUG: Current code shows 0%, should be 100%
|
||||
expect(stats.completionPercentage).toBe(100);
|
||||
});
|
||||
|
||||
it('should show 0% completion when no tasks are complete', () => {
|
||||
// Arrange
|
||||
const tasks: Task[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Pending Task',
|
||||
status: 'pending' as const,
|
||||
dependencies: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'In Progress Task',
|
||||
status: 'in-progress' as const,
|
||||
dependencies: []
|
||||
}
|
||||
];
|
||||
|
||||
// Act
|
||||
const stats = calculateTaskStatistics(tasks);
|
||||
|
||||
// Assert
|
||||
expect(stats.completionPercentage).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateSubtaskStatistics', () => {
|
||||
it('should treat cancelled subtasks as complete in percentage calculation', () => {
|
||||
// Arrange: Task with 3 done subtasks and 1 cancelled = 100%
|
||||
const tasks: Task[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Parent Task',
|
||||
status: 'in-progress' as const,
|
||||
dependencies: [],
|
||||
subtasks: [
|
||||
{ id: '1', title: 'Sub 1', status: 'done' },
|
||||
{ id: '2', title: 'Sub 2', status: 'done' },
|
||||
{ id: '3', title: 'Sub 3', status: 'done' },
|
||||
{ id: '4', title: 'Sub 4', status: 'cancelled' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Act
|
||||
const stats = calculateSubtaskStatistics(tasks);
|
||||
|
||||
// Assert
|
||||
expect(stats.total).toBe(4);
|
||||
expect(stats.done).toBe(3);
|
||||
expect(stats.cancelled).toBe(1);
|
||||
expect(stats.completedCount).toBe(4); // done + cancelled
|
||||
// BUG: Current code shows 75% (3/4), should be 100% (4/4)
|
||||
expect(stats.completionPercentage).toBe(100);
|
||||
});
|
||||
|
||||
it('should handle completed status in subtasks', () => {
|
||||
// Arrange
|
||||
const tasks: Task[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Parent Task',
|
||||
status: 'in-progress' as const,
|
||||
dependencies: [],
|
||||
subtasks: [
|
||||
{ id: '1', title: 'Sub 1', status: 'done' },
|
||||
{ id: '2', title: 'Sub 2', status: 'completed' },
|
||||
{ id: '3', title: 'Sub 3', status: 'pending' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Act
|
||||
const stats = calculateSubtaskStatistics(tasks);
|
||||
|
||||
// Assert
|
||||
expect(stats.total).toBe(3);
|
||||
expect(stats.completedCount).toBe(2); // done + completed
|
||||
// 2 complete (done + completed) out of 3 = 67%
|
||||
expect(stats.completionPercentage).toBe(67);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateDependencyStatistics', () => {
|
||||
it('should treat cancelled tasks as satisfied dependencies', () => {
|
||||
// Arrange: Task 15 depends on cancelled task 14
|
||||
const tasks: Task[] = [
|
||||
...Array.from({ length: 13 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
title: `Task ${i + 1}`,
|
||||
status: 'done' as const,
|
||||
dependencies: []
|
||||
})),
|
||||
{
|
||||
id: 14,
|
||||
title: 'Cancelled Dependency',
|
||||
status: 'cancelled' as const,
|
||||
dependencies: []
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
title: 'Dependent Task',
|
||||
status: 'pending' as const,
|
||||
dependencies: [14]
|
||||
}
|
||||
];
|
||||
|
||||
// Act
|
||||
const stats = calculateDependencyStatistics(tasks);
|
||||
|
||||
// Assert
|
||||
// Task 15 should be ready to work on since its dependency (14) is cancelled
|
||||
// BUG: Current code shows task 15 as blocked, should show as ready
|
||||
expect(stats.tasksBlockedByDeps).toBe(0);
|
||||
expect(stats.tasksReadyToWork).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should treat completed status as satisfied dependencies', () => {
|
||||
// Arrange
|
||||
const tasks: Task[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Completed Dependency',
|
||||
status: 'completed' as const,
|
||||
dependencies: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Dependent Task',
|
||||
status: 'pending' as const,
|
||||
dependencies: [1]
|
||||
}
|
||||
];
|
||||
|
||||
// Act
|
||||
const stats = calculateDependencyStatistics(tasks);
|
||||
|
||||
// Assert
|
||||
expect(stats.tasksBlockedByDeps).toBe(0);
|
||||
expect(stats.tasksReadyToWork).toBe(1);
|
||||
});
|
||||
|
||||
it('should count tasks with cancelled dependencies as ready', () => {
|
||||
// Arrange: Multiple tasks depending on cancelled tasks
|
||||
const tasks: Task[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Cancelled Task',
|
||||
status: 'cancelled' as const,
|
||||
dependencies: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Dependent 1',
|
||||
status: 'pending' as const,
|
||||
dependencies: [1]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Dependent 2',
|
||||
status: 'pending' as const,
|
||||
dependencies: [1]
|
||||
}
|
||||
];
|
||||
|
||||
// Act
|
||||
const stats = calculateDependencyStatistics(tasks);
|
||||
|
||||
// Assert
|
||||
expect(stats.tasksBlockedByDeps).toBe(0);
|
||||
expect(stats.tasksReadyToWork).toBe(2); // Both dependents should be ready
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -36,6 +36,39 @@ export const TASK_STATUSES: readonly TaskStatus[] = [
|
||||
'review'
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Terminal complete statuses - tasks that are finished and satisfy dependencies
|
||||
* These statuses indicate a task is in a final state and:
|
||||
* - Should count toward completion percentage
|
||||
* - Should be considered satisfied for dependency resolution
|
||||
* - Should not be selected as "next task"
|
||||
*
|
||||
* Note: 'completed' is a workflow-specific alias for 'done' used in some contexts
|
||||
*/
|
||||
export const TERMINAL_COMPLETE_STATUSES: readonly TaskStatus[] = [
|
||||
'done',
|
||||
'completed',
|
||||
'cancelled'
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Check if a task status represents a terminal complete state
|
||||
*
|
||||
* @param status - The task status to check
|
||||
* @returns true if the status represents a completed/terminal task
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* isTaskComplete('done') // true
|
||||
* isTaskComplete('completed') // true
|
||||
* isTaskComplete('cancelled') // true
|
||||
* isTaskComplete('pending') // false
|
||||
* ```
|
||||
*/
|
||||
export function isTaskComplete(status: TaskStatus): boolean {
|
||||
return TERMINAL_COMPLETE_STATUSES.includes(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valid task priority values
|
||||
*/
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
* Loads and validates tasks for autopilot execution
|
||||
*/
|
||||
|
||||
import type { Task, Subtask, TaskStatus } from '../../../common/types/index.js';
|
||||
import type { Task, Subtask } from '../../../common/types/index.js';
|
||||
import type { TaskService } from './task-service.js';
|
||||
import { getLogger } from '../../../common/logger/factory.js';
|
||||
import { isTaskComplete } from '../../../common/constants/index.js';
|
||||
|
||||
const logger = getLogger('TaskLoader');
|
||||
|
||||
@@ -131,9 +132,7 @@ export class TaskLoaderService {
|
||||
* Validate task status is appropriate for autopilot
|
||||
*/
|
||||
private validateTaskStatus(task: Task): TaskValidationResult {
|
||||
const completedStatuses: TaskStatus[] = ['done', 'completed', 'cancelled'];
|
||||
|
||||
if (completedStatuses.includes(task.status)) {
|
||||
if (isTaskComplete(task.status)) {
|
||||
return {
|
||||
success: false,
|
||||
errorType: 'task_completed',
|
||||
|
||||
Reference in New Issue
Block a user