@task-mcp/cli 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,271 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ calculateStats,
4
+ calculateDependencyMetrics,
5
+ suggestNextTask,
6
+ type Task,
7
+ } from "../storage.js";
8
+
9
+ // Helper to create test tasks
10
+ function createTask(overrides: Partial<Task> = {}): Task {
11
+ return {
12
+ id: `task_${Math.random().toString(36).slice(2, 10)}`,
13
+ title: "Test Task",
14
+ status: "pending",
15
+ priority: "medium",
16
+ projectId: "proj_test",
17
+ createdAt: new Date().toISOString(),
18
+ updatedAt: new Date().toISOString(),
19
+ ...overrides,
20
+ };
21
+ }
22
+
23
+ describe("storage", () => {
24
+ describe("calculateStats", () => {
25
+ test("calculates stats for empty task list", () => {
26
+ const stats = calculateStats([]);
27
+ expect(stats.total).toBe(0);
28
+ expect(stats.completed).toBe(0);
29
+ expect(stats.pending).toBe(0);
30
+ expect(stats.completionPercent).toBe(0);
31
+ });
32
+
33
+ test("counts tasks by status", () => {
34
+ const tasks = [
35
+ createTask({ status: "pending" }),
36
+ createTask({ status: "pending" }),
37
+ createTask({ status: "in_progress" }),
38
+ createTask({ status: "completed" }),
39
+ createTask({ status: "blocked" }),
40
+ createTask({ status: "cancelled" }),
41
+ ];
42
+
43
+ const stats = calculateStats(tasks);
44
+ expect(stats.total).toBe(6);
45
+ expect(stats.pending).toBe(2);
46
+ expect(stats.inProgress).toBe(1);
47
+ expect(stats.completed).toBe(1);
48
+ expect(stats.blocked).toBe(1);
49
+ expect(stats.cancelled).toBe(1);
50
+ });
51
+
52
+ test("counts tasks by priority", () => {
53
+ const tasks = [
54
+ createTask({ priority: "critical" }),
55
+ createTask({ priority: "high" }),
56
+ createTask({ priority: "high" }),
57
+ createTask({ priority: "medium" }),
58
+ createTask({ priority: "low" }),
59
+ ];
60
+
61
+ const stats = calculateStats(tasks);
62
+ expect(stats.byPriority.critical).toBe(1);
63
+ expect(stats.byPriority.high).toBe(2);
64
+ expect(stats.byPriority.medium).toBe(1);
65
+ expect(stats.byPriority.low).toBe(1);
66
+ });
67
+
68
+ test("calculates completion percent excluding cancelled", () => {
69
+ const tasks = [
70
+ createTask({ status: "completed" }),
71
+ createTask({ status: "completed" }),
72
+ createTask({ status: "pending" }),
73
+ createTask({ status: "pending" }),
74
+ createTask({ status: "cancelled" }), // Should be excluded from calculation
75
+ ];
76
+
77
+ const stats = calculateStats(tasks);
78
+ // 2 completed out of 4 non-cancelled = 50%
79
+ expect(stats.completionPercent).toBe(50);
80
+ });
81
+
82
+ test("handles all cancelled tasks", () => {
83
+ const tasks = [
84
+ createTask({ status: "cancelled" }),
85
+ createTask({ status: "cancelled" }),
86
+ ];
87
+
88
+ const stats = calculateStats(tasks);
89
+ expect(stats.completionPercent).toBe(0);
90
+ });
91
+ });
92
+
93
+ describe("calculateDependencyMetrics", () => {
94
+ test("calculates metrics for tasks without dependencies", () => {
95
+ const tasks = [
96
+ createTask({ status: "pending" }),
97
+ createTask({ status: "pending" }),
98
+ createTask({ status: "in_progress" }),
99
+ ];
100
+
101
+ const metrics = calculateDependencyMetrics(tasks);
102
+ expect(metrics.noDependencies).toBe(3);
103
+ expect(metrics.readyToWork).toBe(3);
104
+ expect(metrics.blockedByDependencies).toBe(0);
105
+ expect(metrics.avgDependencies).toBe(0);
106
+ });
107
+
108
+ test("identifies tasks blocked by dependencies", () => {
109
+ const task1 = createTask({ id: "task_1", status: "pending" });
110
+ const task2 = createTask({
111
+ id: "task_2",
112
+ status: "pending",
113
+ dependencies: [{ taskId: "task_1", type: "blocks" }],
114
+ });
115
+
116
+ const metrics = calculateDependencyMetrics([task1, task2]);
117
+ expect(metrics.noDependencies).toBe(1);
118
+ expect(metrics.readyToWork).toBe(1); // Only task1 is ready
119
+ expect(metrics.blockedByDependencies).toBe(1);
120
+ });
121
+
122
+ test("ready to work includes tasks with completed dependencies", () => {
123
+ const task1 = createTask({ id: "task_1", status: "completed" });
124
+ const task2 = createTask({
125
+ id: "task_2",
126
+ status: "pending",
127
+ dependencies: [{ taskId: "task_1", type: "blocks" }],
128
+ });
129
+
130
+ const metrics = calculateDependencyMetrics([task1, task2]);
131
+ expect(metrics.readyToWork).toBe(1); // task2 is ready because task1 is completed
132
+ });
133
+
134
+ test("finds most depended-on task", () => {
135
+ const task1 = createTask({ id: "task_1", title: "Base Task", status: "pending" });
136
+ const task2 = createTask({
137
+ id: "task_2",
138
+ status: "pending",
139
+ dependencies: [{ taskId: "task_1", type: "blocks" }],
140
+ });
141
+ const task3 = createTask({
142
+ id: "task_3",
143
+ status: "pending",
144
+ dependencies: [{ taskId: "task_1", type: "blocks" }],
145
+ });
146
+
147
+ const metrics = calculateDependencyMetrics([task1, task2, task3]);
148
+ expect(metrics.mostDependedOn).not.toBeNull();
149
+ expect(metrics.mostDependedOn?.id).toBe("task_1");
150
+ expect(metrics.mostDependedOn?.count).toBe(2);
151
+ });
152
+
153
+ test("calculates average dependencies", () => {
154
+ const task1 = createTask({ id: "task_1", status: "pending" });
155
+ const task2 = createTask({
156
+ id: "task_2",
157
+ status: "pending",
158
+ dependencies: [
159
+ { taskId: "task_1", type: "blocks" },
160
+ { taskId: "task_x", type: "blocks" },
161
+ ],
162
+ });
163
+
164
+ const metrics = calculateDependencyMetrics([task1, task2]);
165
+ expect(metrics.avgDependencies).toBe(1); // 2 deps / 2 tasks = 1
166
+ });
167
+
168
+ test("excludes completed and cancelled tasks from active count", () => {
169
+ const tasks = [
170
+ createTask({ status: "completed" }),
171
+ createTask({ status: "cancelled" }),
172
+ createTask({ status: "pending" }),
173
+ ];
174
+
175
+ const metrics = calculateDependencyMetrics(tasks);
176
+ expect(metrics.noDependencies).toBe(1); // Only the pending task
177
+ expect(metrics.readyToWork).toBe(1);
178
+ });
179
+ });
180
+
181
+ describe("suggestNextTask", () => {
182
+ test("returns null for empty task list", () => {
183
+ expect(suggestNextTask([])).toBeNull();
184
+ });
185
+
186
+ test("returns null when no actionable tasks", () => {
187
+ const tasks = [
188
+ createTask({ status: "completed" }),
189
+ createTask({ status: "cancelled" }),
190
+ createTask({ status: "blocked" }),
191
+ ];
192
+ expect(suggestNextTask(tasks)).toBeNull();
193
+ });
194
+
195
+ test("suggests task with highest priority", () => {
196
+ const lowTask = createTask({ id: "low", priority: "low", status: "pending" });
197
+ const highTask = createTask({ id: "high", priority: "high", status: "pending" });
198
+ const mediumTask = createTask({ id: "medium", priority: "medium", status: "pending" });
199
+
200
+ const suggestion = suggestNextTask([lowTask, highTask, mediumTask]);
201
+ expect(suggestion?.id).toBe("high");
202
+ });
203
+
204
+ test("critical priority comes first", () => {
205
+ const tasks = [
206
+ createTask({ id: "high", priority: "high", status: "pending" }),
207
+ createTask({ id: "critical", priority: "critical", status: "pending" }),
208
+ ];
209
+
210
+ const suggestion = suggestNextTask(tasks);
211
+ expect(suggestion?.id).toBe("critical");
212
+ });
213
+
214
+ test("suggests in_progress tasks", () => {
215
+ const tasks = [
216
+ createTask({ id: "pending", priority: "high", status: "pending" }),
217
+ createTask({ id: "in_progress", priority: "medium", status: "in_progress" }),
218
+ ];
219
+
220
+ // Both should be considered, high priority wins
221
+ const suggestion = suggestNextTask(tasks);
222
+ expect(suggestion?.id).toBe("pending");
223
+ });
224
+
225
+ test("excludes tasks with uncompleted dependencies", () => {
226
+ const task1 = createTask({ id: "task_1", priority: "low", status: "pending" });
227
+ const task2 = createTask({
228
+ id: "task_2",
229
+ priority: "critical",
230
+ status: "pending",
231
+ dependencies: [{ taskId: "task_1", type: "blocks" }],
232
+ });
233
+
234
+ const suggestion = suggestNextTask([task1, task2]);
235
+ // task2 has higher priority but is blocked, so task1 should be suggested
236
+ expect(suggestion?.id).toBe("task_1");
237
+ });
238
+
239
+ test("includes tasks when dependencies are completed", () => {
240
+ const task1 = createTask({ id: "task_1", priority: "low", status: "completed" });
241
+ const task2 = createTask({
242
+ id: "task_2",
243
+ priority: "critical",
244
+ status: "pending",
245
+ dependencies: [{ taskId: "task_1", type: "blocks" }],
246
+ });
247
+
248
+ const suggestion = suggestNextTask([task1, task2]);
249
+ // task1 is completed, so task2 is now actionable
250
+ expect(suggestion?.id).toBe("task_2");
251
+ });
252
+
253
+ test("same priority uses creation date (older first)", () => {
254
+ const older = createTask({
255
+ id: "older",
256
+ priority: "high",
257
+ status: "pending",
258
+ createdAt: "2024-01-01T00:00:00Z",
259
+ });
260
+ const newer = createTask({
261
+ id: "newer",
262
+ priority: "high",
263
+ status: "pending",
264
+ createdAt: "2024-12-01T00:00:00Z",
265
+ });
266
+
267
+ const suggestion = suggestNextTask([newer, older]);
268
+ expect(suggestion?.id).toBe("older");
269
+ });
270
+ });
271
+ });
package/src/ansi.ts CHANGED
@@ -120,7 +120,7 @@ export const BOX = {
120
120
  /**
121
121
  * Create a horizontal line
122
122
  */
123
- export function hline(width: number, char = BOX.horizontal): string {
123
+ export function hline(width: number, char: string = BOX.horizontal): string {
124
124
  return char.repeat(width);
125
125
  }
126
126