@task-mcp/shared 1.0.19 → 1.0.21

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.
Files changed (163) hide show
  1. package/package.json +1 -6
  2. package/src/schemas/task.ts +60 -14
  3. package/src/utils/date.ts +40 -0
  4. package/src/utils/hierarchy.ts +63 -1
  5. package/src/utils/index.ts +2 -0
  6. package/src/utils/natural-language.ts +12 -0
  7. package/src/utils/projection.ts +14 -3
  8. package/dist/algorithms/critical-path.d.ts +0 -46
  9. package/dist/algorithms/critical-path.d.ts.map +0 -1
  10. package/dist/algorithms/critical-path.js +0 -320
  11. package/dist/algorithms/critical-path.js.map +0 -1
  12. package/dist/algorithms/critical-path.test.d.ts +0 -2
  13. package/dist/algorithms/critical-path.test.d.ts.map +0 -1
  14. package/dist/algorithms/critical-path.test.js +0 -194
  15. package/dist/algorithms/critical-path.test.js.map +0 -1
  16. package/dist/algorithms/dependency-integrity.d.ts +0 -81
  17. package/dist/algorithms/dependency-integrity.d.ts.map +0 -1
  18. package/dist/algorithms/dependency-integrity.js +0 -207
  19. package/dist/algorithms/dependency-integrity.js.map +0 -1
  20. package/dist/algorithms/dependency-integrity.test.d.ts +0 -2
  21. package/dist/algorithms/dependency-integrity.test.d.ts.map +0 -1
  22. package/dist/algorithms/dependency-integrity.test.js +0 -309
  23. package/dist/algorithms/dependency-integrity.test.js.map +0 -1
  24. package/dist/algorithms/index.d.ts +0 -5
  25. package/dist/algorithms/index.d.ts.map +0 -1
  26. package/dist/algorithms/index.js +0 -5
  27. package/dist/algorithms/index.js.map +0 -1
  28. package/dist/algorithms/tech-analysis.d.ts +0 -106
  29. package/dist/algorithms/tech-analysis.d.ts.map +0 -1
  30. package/dist/algorithms/tech-analysis.js +0 -344
  31. package/dist/algorithms/tech-analysis.js.map +0 -1
  32. package/dist/algorithms/tech-analysis.test.d.ts +0 -2
  33. package/dist/algorithms/tech-analysis.test.d.ts.map +0 -1
  34. package/dist/algorithms/tech-analysis.test.js +0 -338
  35. package/dist/algorithms/tech-analysis.test.js.map +0 -1
  36. package/dist/algorithms/topological-sort.d.ts +0 -41
  37. package/dist/algorithms/topological-sort.d.ts.map +0 -1
  38. package/dist/algorithms/topological-sort.js +0 -165
  39. package/dist/algorithms/topological-sort.js.map +0 -1
  40. package/dist/algorithms/topological-sort.test.d.ts +0 -2
  41. package/dist/algorithms/topological-sort.test.d.ts.map +0 -1
  42. package/dist/algorithms/topological-sort.test.js +0 -162
  43. package/dist/algorithms/topological-sort.test.js.map +0 -1
  44. package/dist/index.d.ts +0 -4
  45. package/dist/index.d.ts.map +0 -1
  46. package/dist/index.js +0 -7
  47. package/dist/index.js.map +0 -1
  48. package/dist/schemas/inbox.d.ts +0 -55
  49. package/dist/schemas/inbox.d.ts.map +0 -1
  50. package/dist/schemas/inbox.js +0 -25
  51. package/dist/schemas/inbox.js.map +0 -1
  52. package/dist/schemas/index.d.ts +0 -7
  53. package/dist/schemas/index.d.ts.map +0 -1
  54. package/dist/schemas/index.js +0 -17
  55. package/dist/schemas/index.js.map +0 -1
  56. package/dist/schemas/project.d.ts +0 -177
  57. package/dist/schemas/project.d.ts.map +0 -1
  58. package/dist/schemas/project.js +0 -56
  59. package/dist/schemas/project.js.map +0 -1
  60. package/dist/schemas/response-format.d.ts +0 -148
  61. package/dist/schemas/response-format.d.ts.map +0 -1
  62. package/dist/schemas/response-format.js +0 -18
  63. package/dist/schemas/response-format.js.map +0 -1
  64. package/dist/schemas/response-schema.d.ts +0 -307
  65. package/dist/schemas/response-schema.d.ts.map +0 -1
  66. package/dist/schemas/response-schema.js +0 -75
  67. package/dist/schemas/response-schema.js.map +0 -1
  68. package/dist/schemas/response-schema.test.d.ts +0 -2
  69. package/dist/schemas/response-schema.test.d.ts.map +0 -1
  70. package/dist/schemas/response-schema.test.js +0 -256
  71. package/dist/schemas/response-schema.test.js.map +0 -1
  72. package/dist/schemas/state.d.ts +0 -17
  73. package/dist/schemas/state.d.ts.map +0 -1
  74. package/dist/schemas/state.js +0 -17
  75. package/dist/schemas/state.js.map +0 -1
  76. package/dist/schemas/task.d.ts +0 -625
  77. package/dist/schemas/task.d.ts.map +0 -1
  78. package/dist/schemas/task.js +0 -152
  79. package/dist/schemas/task.js.map +0 -1
  80. package/dist/schemas/view.d.ts +0 -143
  81. package/dist/schemas/view.d.ts.map +0 -1
  82. package/dist/schemas/view.js +0 -48
  83. package/dist/schemas/view.js.map +0 -1
  84. package/dist/utils/dashboard-renderer.d.ts +0 -93
  85. package/dist/utils/dashboard-renderer.d.ts.map +0 -1
  86. package/dist/utils/dashboard-renderer.js +0 -424
  87. package/dist/utils/dashboard-renderer.js.map +0 -1
  88. package/dist/utils/dashboard-renderer.test.d.ts +0 -2
  89. package/dist/utils/dashboard-renderer.test.d.ts.map +0 -1
  90. package/dist/utils/dashboard-renderer.test.js +0 -774
  91. package/dist/utils/dashboard-renderer.test.js.map +0 -1
  92. package/dist/utils/date.d.ts +0 -81
  93. package/dist/utils/date.d.ts.map +0 -1
  94. package/dist/utils/date.js +0 -294
  95. package/dist/utils/date.js.map +0 -1
  96. package/dist/utils/date.test.d.ts +0 -2
  97. package/dist/utils/date.test.d.ts.map +0 -1
  98. package/dist/utils/date.test.js +0 -276
  99. package/dist/utils/date.test.js.map +0 -1
  100. package/dist/utils/hierarchy.d.ts +0 -75
  101. package/dist/utils/hierarchy.d.ts.map +0 -1
  102. package/dist/utils/hierarchy.js +0 -189
  103. package/dist/utils/hierarchy.js.map +0 -1
  104. package/dist/utils/hierarchy.test.d.ts +0 -2
  105. package/dist/utils/hierarchy.test.d.ts.map +0 -1
  106. package/dist/utils/hierarchy.test.js +0 -351
  107. package/dist/utils/hierarchy.test.js.map +0 -1
  108. package/dist/utils/id.d.ts +0 -60
  109. package/dist/utils/id.d.ts.map +0 -1
  110. package/dist/utils/id.js +0 -118
  111. package/dist/utils/id.js.map +0 -1
  112. package/dist/utils/id.test.d.ts +0 -2
  113. package/dist/utils/id.test.d.ts.map +0 -1
  114. package/dist/utils/id.test.js +0 -193
  115. package/dist/utils/id.test.js.map +0 -1
  116. package/dist/utils/index.d.ts +0 -12
  117. package/dist/utils/index.d.ts.map +0 -1
  118. package/dist/utils/index.js +0 -34
  119. package/dist/utils/index.js.map +0 -1
  120. package/dist/utils/natural-language.d.ts +0 -57
  121. package/dist/utils/natural-language.d.ts.map +0 -1
  122. package/dist/utils/natural-language.js +0 -205
  123. package/dist/utils/natural-language.js.map +0 -1
  124. package/dist/utils/natural-language.test.d.ts +0 -2
  125. package/dist/utils/natural-language.test.d.ts.map +0 -1
  126. package/dist/utils/natural-language.test.js +0 -156
  127. package/dist/utils/natural-language.test.js.map +0 -1
  128. package/dist/utils/priority-queue.d.ts +0 -17
  129. package/dist/utils/priority-queue.d.ts.map +0 -1
  130. package/dist/utils/priority-queue.js +0 -62
  131. package/dist/utils/priority-queue.js.map +0 -1
  132. package/dist/utils/projection.d.ts +0 -65
  133. package/dist/utils/projection.d.ts.map +0 -1
  134. package/dist/utils/projection.js +0 -170
  135. package/dist/utils/projection.js.map +0 -1
  136. package/dist/utils/projection.test.d.ts +0 -2
  137. package/dist/utils/projection.test.d.ts.map +0 -1
  138. package/dist/utils/projection.test.js +0 -336
  139. package/dist/utils/projection.test.js.map +0 -1
  140. package/dist/utils/terminal-ui.d.ts +0 -208
  141. package/dist/utils/terminal-ui.d.ts.map +0 -1
  142. package/dist/utils/terminal-ui.js +0 -611
  143. package/dist/utils/terminal-ui.js.map +0 -1
  144. package/dist/utils/terminal-ui.test.d.ts +0 -2
  145. package/dist/utils/terminal-ui.test.d.ts.map +0 -1
  146. package/dist/utils/terminal-ui.test.js +0 -683
  147. package/dist/utils/terminal-ui.test.js.map +0 -1
  148. package/dist/utils/workspace.d.ts +0 -100
  149. package/dist/utils/workspace.d.ts.map +0 -1
  150. package/dist/utils/workspace.js +0 -173
  151. package/dist/utils/workspace.js.map +0 -1
  152. package/src/algorithms/critical-path.test.ts +0 -241
  153. package/src/algorithms/dependency-integrity.test.ts +0 -348
  154. package/src/algorithms/tech-analysis.test.ts +0 -413
  155. package/src/algorithms/topological-sort.test.ts +0 -190
  156. package/src/schemas/response-schema.test.ts +0 -314
  157. package/src/utils/dashboard-renderer.test.ts +0 -983
  158. package/src/utils/date.test.ts +0 -329
  159. package/src/utils/hierarchy.test.ts +0 -411
  160. package/src/utils/id.test.ts +0 -235
  161. package/src/utils/natural-language.test.ts +0 -182
  162. package/src/utils/projection.test.ts +0 -425
  163. package/src/utils/terminal-ui.test.ts +0 -831
@@ -1,983 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import {
3
- calculateStats,
4
- calculateDependencyMetrics,
5
- renderStatusWidget,
6
- renderActionsWidget,
7
- renderInboxWidget,
8
- renderProjectsTable,
9
- renderTasksTable,
10
- renderDashboard,
11
- renderProjectDashboard,
12
- renderGlobalDashboard,
13
- type DashboardStats,
14
- type DependencyMetrics,
15
- } from "./dashboard-renderer.js";
16
- import { stripAnsi } from "./terminal-ui.js";
17
- import type { Task } from "../schemas/task.js";
18
- import type { InboxItem } from "../schemas/inbox.js";
19
-
20
- interface WorkspaceInfo {
21
- name: string;
22
- taskCount: number;
23
- completedCount: number;
24
- }
25
-
26
- // =============================================================================
27
- // Test Fixtures
28
- // =============================================================================
29
-
30
- const createTask = (overrides: Partial<Task> = {}): Task => ({
31
- id: "task-1",
32
- workspace: "test-workspace",
33
- title: "Test Task",
34
- status: "pending",
35
- priority: "medium",
36
- createdAt: "2025-01-01T00:00:00.000Z",
37
- updatedAt: "2025-01-01T00:00:00.000Z",
38
- ...overrides,
39
- });
40
-
41
- const createWorkspaceInfo = (overrides: Partial<WorkspaceInfo> = {}): WorkspaceInfo => ({
42
- name: "test-workspace",
43
- taskCount: 10,
44
- completedCount: 5,
45
- ...overrides,
46
- });
47
-
48
- const createInboxItem = (overrides: Partial<InboxItem> = {}): InboxItem => ({
49
- id: "inbox-1",
50
- content: "Quick idea for later",
51
- status: "pending",
52
- capturedAt: "2025-01-01T00:00:00.000Z",
53
- ...overrides,
54
- });
55
-
56
- // =============================================================================
57
- // calculateStats Tests
58
- // =============================================================================
59
-
60
- describe("calculateStats", () => {
61
- test("returns zero stats for empty task list", () => {
62
- const stats = calculateStats([]);
63
-
64
- expect(stats).toEqual({
65
- total: 0,
66
- completed: 0,
67
- inProgress: 0,
68
- pending: 0,
69
- blocked: 0,
70
- cancelled: 0,
71
- byPriority: { critical: 0, high: 0, medium: 0, low: 0 },
72
- });
73
- });
74
-
75
- test("counts tasks by status correctly", () => {
76
- const tasks = [
77
- createTask({ id: "t1", status: "completed" }),
78
- createTask({ id: "t2", status: "completed" }),
79
- createTask({ id: "t3", status: "in_progress" }),
80
- createTask({ id: "t4", status: "pending" }),
81
- createTask({ id: "t5", status: "pending" }),
82
- createTask({ id: "t6", status: "pending" }),
83
- createTask({ id: "t7", status: "blocked" }),
84
- createTask({ id: "t8", status: "cancelled" }),
85
- ];
86
-
87
- const stats = calculateStats(tasks);
88
-
89
- expect(stats.total).toBe(8);
90
- expect(stats.completed).toBe(2);
91
- expect(stats.inProgress).toBe(1);
92
- expect(stats.pending).toBe(3);
93
- expect(stats.blocked).toBe(1);
94
- expect(stats.cancelled).toBe(1);
95
- });
96
-
97
- test("counts tasks by priority correctly", () => {
98
- const tasks = [
99
- createTask({ id: "t1", priority: "critical" }),
100
- createTask({ id: "t2", priority: "critical" }),
101
- createTask({ id: "t3", priority: "high" }),
102
- createTask({ id: "t4", priority: "medium" }),
103
- createTask({ id: "t5", priority: "medium" }),
104
- createTask({ id: "t6", priority: "low" }),
105
- ];
106
-
107
- const stats = calculateStats(tasks);
108
-
109
- expect(stats.byPriority.critical).toBe(2);
110
- expect(stats.byPriority.high).toBe(1);
111
- expect(stats.byPriority.medium).toBe(2);
112
- expect(stats.byPriority.low).toBe(1);
113
- });
114
-
115
- test("defaults to medium priority in stats calculation", () => {
116
- // When priority is "medium", it should be counted in the medium bucket
117
- const tasks = [
118
- createTask({ id: "t1" }), // Uses default priority: "medium"
119
- createTask({ id: "t2" }), // Uses default priority: "medium"
120
- ];
121
-
122
- const stats = calculateStats(tasks);
123
-
124
- expect(stats.byPriority.medium).toBe(2);
125
- });
126
- });
127
-
128
- // =============================================================================
129
- // calculateDependencyMetrics Tests
130
- // =============================================================================
131
-
132
- describe("calculateDependencyMetrics", () => {
133
- test("returns zero metrics for empty task list", () => {
134
- const metrics = calculateDependencyMetrics([]);
135
-
136
- expect(metrics).toEqual({
137
- readyToWork: 0,
138
- blockedByDependencies: 0,
139
- mostDependedOn: undefined,
140
- });
141
- });
142
-
143
- test("counts tasks without dependencies as ready", () => {
144
- const tasks = [
145
- createTask({ id: "t1", status: "pending" }),
146
- createTask({ id: "t2", status: "pending" }),
147
- createTask({ id: "t3", status: "in_progress" }),
148
- ];
149
-
150
- const metrics = calculateDependencyMetrics(tasks);
151
-
152
- expect(metrics.readyToWork).toBe(3);
153
- expect(metrics.blockedByDependencies).toBe(0);
154
- });
155
-
156
- test("counts tasks with unsatisfied dependencies as blocked", () => {
157
- const tasks = [
158
- createTask({ id: "t1", status: "pending" }),
159
- createTask({
160
- id: "t2",
161
- status: "pending",
162
- dependencies: [{ taskId: "t1", type: "blocked_by" }],
163
- }),
164
- createTask({
165
- id: "t3",
166
- status: "pending",
167
- dependencies: [{ taskId: "t1", type: "blocked_by" }],
168
- }),
169
- ];
170
-
171
- const metrics = calculateDependencyMetrics(tasks);
172
-
173
- expect(metrics.readyToWork).toBe(1); // t1 has no deps
174
- expect(metrics.blockedByDependencies).toBe(2); // t2, t3 blocked by t1
175
- });
176
-
177
- test("counts tasks with satisfied dependencies as ready", () => {
178
- const tasks = [
179
- createTask({ id: "t1", status: "completed" }),
180
- createTask({
181
- id: "t2",
182
- status: "pending",
183
- dependencies: [{ taskId: "t1", type: "blocked_by" }],
184
- }),
185
- ];
186
-
187
- const metrics = calculateDependencyMetrics(tasks);
188
-
189
- expect(metrics.readyToWork).toBe(1); // t2 is ready because t1 is completed
190
- expect(metrics.blockedByDependencies).toBe(0);
191
- });
192
-
193
- test("ignores completed and cancelled tasks in readyToWork count", () => {
194
- const tasks = [
195
- createTask({ id: "t1", status: "completed" }),
196
- createTask({ id: "t2", status: "cancelled" }),
197
- createTask({ id: "t3", status: "pending" }),
198
- ];
199
-
200
- const metrics = calculateDependencyMetrics(tasks);
201
-
202
- expect(metrics.readyToWork).toBe(1); // Only t3 counts
203
- });
204
-
205
- test("identifies most depended on task", () => {
206
- const tasks = [
207
- createTask({ id: "t1", title: "Foundation Task", status: "pending" }),
208
- createTask({
209
- id: "t2",
210
- status: "pending",
211
- dependencies: [{ taskId: "t1", type: "blocked_by" }],
212
- }),
213
- createTask({
214
- id: "t3",
215
- status: "pending",
216
- dependencies: [{ taskId: "t1", type: "blocked_by" }],
217
- }),
218
- createTask({
219
- id: "t4",
220
- status: "pending",
221
- dependencies: [{ taskId: "t1", type: "blocked_by" }],
222
- }),
223
- ];
224
-
225
- const metrics = calculateDependencyMetrics(tasks);
226
-
227
- expect(metrics.mostDependedOn).toEqual({
228
- id: "t1",
229
- title: "Foundation Task",
230
- dependentCount: 3,
231
- });
232
- });
233
-
234
- test("returns undefined mostDependedOn when no dependencies exist", () => {
235
- const tasks = [
236
- createTask({ id: "t1", status: "pending" }),
237
- createTask({ id: "t2", status: "pending" }),
238
- ];
239
-
240
- const metrics = calculateDependencyMetrics(tasks);
241
-
242
- expect(metrics.mostDependedOn).toBeUndefined();
243
- });
244
- });
245
-
246
- // =============================================================================
247
- // renderStatusWidget Tests
248
- // =============================================================================
249
-
250
- describe("renderStatusWidget", () => {
251
- test("renders status widget with basic stats", () => {
252
- const tasks = [
253
- createTask({ id: "t1", status: "completed" }),
254
- createTask({ id: "t2", status: "pending" }),
255
- createTask({ id: "t3", status: "in_progress" }),
256
- ];
257
-
258
- const widget = renderStatusWidget(tasks);
259
- const plain = stripAnsi(widget);
260
-
261
- expect(plain).toContain("Status");
262
- expect(plain).toContain("Done");
263
- expect(plain).toContain("Progress");
264
- expect(plain).toContain("Pending");
265
- });
266
-
267
- test("renders progress percentage", () => {
268
- const tasks = [
269
- createTask({ id: "t1", status: "completed" }),
270
- createTask({ id: "t2", status: "completed" }),
271
- createTask({ id: "t3", status: "pending" }),
272
- createTask({ id: "t4", status: "pending" }),
273
- ];
274
-
275
- const widget = renderStatusWidget(tasks);
276
- const plain = stripAnsi(widget);
277
-
278
- expect(plain).toContain("50%");
279
- expect(plain).toContain("2/4 tasks");
280
- });
281
-
282
- test("renders priority breakdown", () => {
283
- const tasks = [
284
- createTask({ id: "t1", priority: "critical" }),
285
- createTask({ id: "t2", priority: "high" }),
286
- ];
287
-
288
- const widget = renderStatusWidget(tasks);
289
- const plain = stripAnsi(widget);
290
-
291
- expect(plain).toContain("Critical");
292
- expect(plain).toContain("High");
293
- expect(plain).toContain("Medium");
294
- expect(plain).toContain("Low");
295
- });
296
-
297
- test("renders dependency metrics", () => {
298
- const tasks = [
299
- createTask({ id: "t1", status: "pending" }),
300
- createTask({
301
- id: "t2",
302
- status: "pending",
303
- dependencies: [{ taskId: "t1", type: "blocked_by" }],
304
- }),
305
- ];
306
-
307
- const widget = renderStatusWidget(tasks);
308
- const plain = stripAnsi(widget);
309
-
310
- expect(plain).toContain("Ready");
311
- expect(plain).toContain("Blocked");
312
- });
313
-
314
- test("excludes cancelled tasks from active count", () => {
315
- const tasks = [
316
- createTask({ id: "t1", status: "completed" }),
317
- createTask({ id: "t2", status: "cancelled" }),
318
- createTask({ id: "t3", status: "pending" }),
319
- ];
320
-
321
- const widget = renderStatusWidget(tasks);
322
- const plain = stripAnsi(widget);
323
-
324
- // 1 completed out of 2 active = 50%
325
- expect(plain).toContain("50%");
326
- expect(plain).toContain("1/2 tasks");
327
- });
328
- });
329
-
330
- // =============================================================================
331
- // renderActionsWidget Tests
332
- // =============================================================================
333
-
334
- describe("renderActionsWidget", () => {
335
- test("renders next actions widget title", () => {
336
- const tasks = [createTask({ status: "pending" })];
337
-
338
- const widget = renderActionsWidget(tasks);
339
- const plain = stripAnsi(widget);
340
-
341
- expect(plain).toContain("Next Actions");
342
- });
343
-
344
- test("shows ready tasks without dependencies", () => {
345
- const tasks = [
346
- createTask({ id: "t1", title: "Ready Task 1", status: "pending" }),
347
- createTask({ id: "t2", title: "Ready Task 2", status: "pending" }),
348
- ];
349
-
350
- const widget = renderActionsWidget(tasks);
351
- const plain = stripAnsi(widget);
352
-
353
- expect(plain).toContain("Ready Task 1");
354
- expect(plain).toContain("Ready Task 2");
355
- expect(plain).toContain("ready");
356
- });
357
-
358
- test("excludes tasks with dependencies", () => {
359
- const tasks = [
360
- createTask({ id: "t1", title: "Ready Task", status: "pending" }),
361
- createTask({
362
- id: "t2",
363
- title: "Blocked Task",
364
- status: "pending",
365
- dependencies: [{ taskId: "t1", type: "blocked_by" }],
366
- }),
367
- ];
368
-
369
- const widget = renderActionsWidget(tasks);
370
- const plain = stripAnsi(widget);
371
-
372
- expect(plain).toContain("Ready Task");
373
- expect(plain).not.toContain("Blocked Task");
374
- });
375
-
376
- test("sorts by priority", () => {
377
- const tasks = [
378
- createTask({ id: "t1", title: "Low Priority", status: "pending", priority: "low" }),
379
- createTask({ id: "t2", title: "Critical Task", status: "pending", priority: "critical" }),
380
- createTask({ id: "t3", title: "High Priority", status: "pending", priority: "high" }),
381
- ];
382
-
383
- const widget = renderActionsWidget(tasks);
384
- const plain = stripAnsi(widget);
385
-
386
- // Critical should appear before others
387
- const criticalPos = plain.indexOf("Critical Task");
388
- const highPos = plain.indexOf("High Priority");
389
- const lowPos = plain.indexOf("Low Priority");
390
-
391
- expect(criticalPos).toBeLessThan(highPos);
392
- expect(highPos).toBeLessThan(lowPos);
393
- });
394
-
395
- test("limits to 4 tasks", () => {
396
- const tasks = Array.from({ length: 10 }, (_, i) =>
397
- createTask({ id: `t${i}`, title: `Task ${i}`, status: "pending" })
398
- );
399
-
400
- const widget = renderActionsWidget(tasks);
401
- const plain = stripAnsi(widget);
402
-
403
- // Should show only first 4
404
- expect(plain).toContain("Task 0");
405
- expect(plain).toContain("Task 3");
406
- expect(plain).not.toContain("Task 9");
407
- });
408
-
409
- test("shows message when no tasks ready", () => {
410
- const tasks = [
411
- createTask({ id: "t1", status: "completed" }),
412
- createTask({
413
- id: "t2",
414
- status: "pending",
415
- dependencies: [{ taskId: "t1", type: "blocked_by" }],
416
- }),
417
- ];
418
-
419
- // t2 has deps, but t1 is completed so t2 is actually ready
420
- // Let's use a case where deps are NOT satisfied
421
- const blockedTasks = [
422
- createTask({ id: "t1", status: "pending" }),
423
- createTask({
424
- id: "t2",
425
- status: "pending",
426
- dependencies: [{ taskId: "t1", type: "blocked_by" }],
427
- }),
428
- ];
429
-
430
- // Filter out t1 to simulate only blocked tasks
431
- const onlyBlockedTask = blockedTasks.filter((t) => t.id === "t2");
432
- const widget = renderActionsWidget(onlyBlockedTask);
433
- const plain = stripAnsi(widget);
434
-
435
- expect(plain).toContain("No tasks ready");
436
- });
437
- });
438
-
439
- // =============================================================================
440
- // renderInboxWidget Tests
441
- // =============================================================================
442
-
443
- describe("renderInboxWidget", () => {
444
- test("returns null for empty inbox", () => {
445
- const widget = renderInboxWidget([]);
446
- expect(widget).toBeNull();
447
- });
448
-
449
- test("returns null when all items are processed", () => {
450
- const items = [
451
- createInboxItem({ id: "i1", status: "promoted" }),
452
- createInboxItem({ id: "i2", status: "discarded" }),
453
- ];
454
-
455
- const widget = renderInboxWidget(items);
456
- expect(widget).toBeNull();
457
- });
458
-
459
- test("renders pending items count", () => {
460
- const items = [
461
- createInboxItem({ id: "i1", status: "pending" }),
462
- createInboxItem({ id: "i2", status: "pending" }),
463
- createInboxItem({ id: "i3", status: "promoted" }),
464
- ];
465
-
466
- const widget = renderInboxWidget(items);
467
- const plain = stripAnsi(widget!);
468
-
469
- expect(plain).toContain("Inbox");
470
- expect(plain).toContain("Pending");
471
- expect(plain).toContain("2 items");
472
- });
473
-
474
- test("shows up to 3 items", () => {
475
- const items = [
476
- createInboxItem({ id: "i1", content: "Item 1", status: "pending" }),
477
- createInboxItem({ id: "i2", content: "Item 2", status: "pending" }),
478
- createInboxItem({ id: "i3", content: "Item 3", status: "pending" }),
479
- createInboxItem({ id: "i4", content: "Item 4", status: "pending" }),
480
- createInboxItem({ id: "i5", content: "Item 5", status: "pending" }),
481
- ];
482
-
483
- const widget = renderInboxWidget(items);
484
- const plain = stripAnsi(widget!);
485
-
486
- expect(plain).toContain("Item 1");
487
- expect(plain).toContain("Item 2");
488
- expect(plain).toContain("Item 3");
489
- expect(plain).not.toContain("Item 4");
490
- expect(plain).toContain("+2 more");
491
- });
492
-
493
- test("shows tags when present", () => {
494
- const items = [
495
- createInboxItem({
496
- id: "i1",
497
- content: "Tagged item",
498
- status: "pending",
499
- tags: ["important"],
500
- }),
501
- ];
502
-
503
- const widget = renderInboxWidget(items);
504
- const plain = stripAnsi(widget!);
505
-
506
- expect(plain).toContain("#important");
507
- });
508
- });
509
-
510
- // =============================================================================
511
- // renderProjectsTable Tests
512
- // =============================================================================
513
-
514
- describe("renderProjectsTable", () => {
515
- test("returns message for empty workspaces", () => {
516
- const result = renderProjectsTable([], () => []);
517
- const plain = stripAnsi(result);
518
-
519
- expect(plain).toContain("No workspaces found");
520
- });
521
-
522
- test("renders workspace names", () => {
523
- const workspaces = [
524
- createWorkspaceInfo({ name: "workspace-alpha" }),
525
- createWorkspaceInfo({ name: "workspace-beta" }),
526
- ];
527
-
528
- const result = renderProjectsTable(workspaces, () => []);
529
- const plain = stripAnsi(result);
530
-
531
- expect(plain).toContain("workspace-alpha");
532
- expect(plain).toContain("workspace-beta");
533
- });
534
-
535
- test("renders progress percentage", () => {
536
- const workspaces = [createWorkspaceInfo({ name: "half-done", taskCount: 2, completedCount: 1 })];
537
- const tasks = [
538
- createTask({ id: "t1", workspace: "half-done", status: "completed" }),
539
- createTask({ id: "t2", workspace: "half-done", status: "pending" }),
540
- ];
541
-
542
- const result = renderProjectsTable(workspaces, () => tasks);
543
- const plain = stripAnsi(result);
544
-
545
- expect(plain).toContain("50%");
546
- });
547
-
548
- test("renders ready and blocked counts", () => {
549
- const workspaces = [createWorkspaceInfo({ name: "test-workspace" })];
550
- const tasks = [
551
- createTask({ id: "t1", workspace: "test-workspace", status: "pending" }),
552
- createTask({
553
- id: "t2",
554
- workspace: "test-workspace",
555
- status: "pending",
556
- dependencies: [{ taskId: "t1", type: "blocked_by" }],
557
- }),
558
- ];
559
-
560
- const result = renderProjectsTable(workspaces, () => tasks);
561
- const plain = stripAnsi(result);
562
-
563
- // Table headers should be present
564
- expect(plain).toContain("Workspace");
565
- expect(plain).toContain("Progress");
566
- expect(plain).toContain("Tasks");
567
- expect(plain).toContain("Ready");
568
- expect(plain).toContain("Blocked");
569
- });
570
-
571
- test("limits to 10 workspaces", () => {
572
- const workspaces = Array.from({ length: 15 }, (_, i) =>
573
- createWorkspaceInfo({ name: `workspace-${i}` })
574
- );
575
-
576
- const result = renderProjectsTable(workspaces, () => []);
577
- const plain = stripAnsi(result);
578
-
579
- expect(plain).toContain("workspace-0");
580
- expect(plain).toContain("workspace-9");
581
- expect(plain).not.toContain("workspace-10");
582
- });
583
- });
584
-
585
- // =============================================================================
586
- // renderTasksTable Tests
587
- // =============================================================================
588
-
589
- describe("renderTasksTable", () => {
590
- test("returns message for no active tasks", () => {
591
- const tasks = [
592
- createTask({ id: "t1", status: "completed" }),
593
- createTask({ id: "t2", status: "cancelled" }),
594
- ];
595
-
596
- const result = renderTasksTable(tasks);
597
- const plain = stripAnsi(result);
598
-
599
- expect(plain).toContain("No active tasks");
600
- });
601
-
602
- test("renders task titles", () => {
603
- const tasks = [
604
- createTask({ id: "t1", title: "First Task", status: "pending" }),
605
- createTask({ id: "t2", title: "Second Task", status: "in_progress" }),
606
- ];
607
-
608
- const result = renderTasksTable(tasks);
609
- const plain = stripAnsi(result);
610
-
611
- expect(plain).toContain("First Task");
612
- expect(plain).toContain("Second Task");
613
- });
614
-
615
- test("excludes completed and cancelled tasks", () => {
616
- const tasks = [
617
- createTask({ id: "t1", title: "Active Task", status: "pending" }),
618
- createTask({ id: "t2", title: "Done Task", status: "completed" }),
619
- createTask({ id: "t3", title: "Cancelled Task", status: "cancelled" }),
620
- ];
621
-
622
- const result = renderTasksTable(tasks);
623
- const plain = stripAnsi(result);
624
-
625
- expect(plain).toContain("Active Task");
626
- expect(plain).not.toContain("Done Task");
627
- expect(plain).not.toContain("Cancelled Task");
628
- });
629
-
630
- test("respects limit parameter", () => {
631
- const tasks = Array.from({ length: 20 }, (_, i) =>
632
- createTask({ id: `t${i}`, title: `Task ${i}`, status: "pending" })
633
- );
634
-
635
- const result = renderTasksTable(tasks, 5);
636
- const plain = stripAnsi(result);
637
-
638
- expect(plain).toContain("Task 0");
639
- expect(plain).toContain("Task 4");
640
- expect(plain).not.toContain("Task 5");
641
- expect(plain).toContain("+15 more tasks");
642
- });
643
-
644
- test("renders status column", () => {
645
- const tasks = [
646
- createTask({ id: "t1", title: "Pending Task", status: "pending" }),
647
- createTask({ id: "t2", title: "In Progress Task", status: "in_progress" }),
648
- ];
649
-
650
- const result = renderTasksTable(tasks);
651
- const plain = stripAnsi(result);
652
-
653
- expect(plain).toContain("Status");
654
- expect(plain).toContain("pending");
655
- // Status may be truncated in the column, check for partial match
656
- expect(plain).toMatch(/in_prog/);
657
- });
658
-
659
- test("renders priority column", () => {
660
- const tasks = [
661
- createTask({ id: "t1", title: "Critical Task", status: "pending", priority: "critical" }),
662
- createTask({ id: "t2", title: "Low Task", status: "pending", priority: "low" }),
663
- ];
664
-
665
- const result = renderTasksTable(tasks);
666
- const plain = stripAnsi(result);
667
-
668
- expect(plain).toContain("Priority");
669
- });
670
- });
671
-
672
- // =============================================================================
673
- // renderDashboard Tests
674
- // =============================================================================
675
-
676
- describe("renderDashboard", () => {
677
- const defaultData = {
678
- tasks: [createTask({ status: "pending" })],
679
- workspaces: [createWorkspaceInfo()],
680
- };
681
-
682
- test("renders banner by default", () => {
683
- const result = renderDashboard(defaultData, () => []);
684
- const plain = stripAnsi(result);
685
-
686
- // Banner is rendered as ASCII art blocks, check for pattern
687
- expect(plain).toMatch(/████/);
688
- });
689
-
690
- test("hides banner when showBanner is false", () => {
691
- const result = renderDashboard(defaultData, () => [], { showBanner: false });
692
- const plain = stripAnsi(result);
693
-
694
- // Banner is ASCII art blocks, should not be present
695
- expect(plain).not.toMatch(/████.*████.*████/);
696
- });
697
-
698
- test("shows version when provided", () => {
699
- const data = { ...defaultData, version: "1.2.3" };
700
- const result = renderDashboard(data, () => []);
701
- const plain = stripAnsi(result);
702
-
703
- expect(plain).toContain("v1.2.3");
704
- });
705
-
706
- test("shows current workspace name when provided", () => {
707
- const data = {
708
- ...defaultData,
709
- currentWorkspace: "my-workspace",
710
- };
711
- const result = renderDashboard(data, () => []);
712
- const plain = stripAnsi(result);
713
-
714
- expect(plain).toContain("Workspace:");
715
- expect(plain).toContain("my-workspace");
716
- });
717
-
718
- test("shows all workspaces count when no current workspace", () => {
719
- const data = {
720
- tasks: [],
721
- workspaces: [createWorkspaceInfo({ name: "ws1" }), createWorkspaceInfo({ name: "ws2" })],
722
- };
723
- const result = renderDashboard(data, () => []);
724
- const plain = stripAnsi(result);
725
-
726
- expect(plain).toContain("All Workspaces");
727
- expect(plain).toContain("2 workspaces");
728
- });
729
-
730
- test("renders inbox widget when items present", () => {
731
- const data = {
732
- ...defaultData,
733
- inboxItems: [createInboxItem({ content: "Test inbox item", status: "pending" })],
734
- };
735
- const result = renderDashboard(data, () => [], { showInbox: true });
736
- const plain = stripAnsi(result);
737
-
738
- expect(plain).toContain("Inbox");
739
- expect(plain).toContain("Test inbox item");
740
- });
741
-
742
- test("hides inbox widget when showInbox is false", () => {
743
- const data = {
744
- ...defaultData,
745
- inboxItems: [createInboxItem({ content: "Test inbox item", status: "pending" })],
746
- };
747
- const result = renderDashboard(data, () => [], { showInbox: false });
748
- const plain = stripAnsi(result);
749
-
750
- expect(plain).not.toContain("Test inbox item");
751
- });
752
-
753
- test("renders workspaces table for multi-workspace view", () => {
754
- const data = {
755
- tasks: [],
756
- workspaces: [
757
- createWorkspaceInfo({ name: "workspace-one" }),
758
- createWorkspaceInfo({ name: "workspace-two" }),
759
- ],
760
- };
761
- const result = renderDashboard(data, () => [], { showWorkspaces: true });
762
- const plain = stripAnsi(result);
763
-
764
- expect(plain).toContain("Workspaces");
765
- expect(plain).toContain("workspace-one");
766
- expect(plain).toContain("workspace-two");
767
- });
768
-
769
- test("strips ANSI codes when stripAnsiCodes is true", () => {
770
- const result = renderDashboard(defaultData, () => [], { stripAnsiCodes: true });
771
-
772
- // Should not contain ANSI escape sequences
773
- expect(result).not.toMatch(/\x1b\[/);
774
- });
775
- });
776
-
777
- // =============================================================================
778
- // renderProjectDashboard Tests
779
- // =============================================================================
780
-
781
- describe("renderProjectDashboard", () => {
782
- test("renders single workspace dashboard", () => {
783
- const workspace = "my-workspace";
784
- const tasks = [
785
- createTask({ id: "t1", title: "Task 1", status: "pending" }),
786
- createTask({ id: "t2", title: "Task 2", status: "completed" }),
787
- ];
788
-
789
- const result = renderProjectDashboard(workspace, tasks);
790
- const plain = stripAnsi(result);
791
-
792
- expect(plain).toContain("my-workspace");
793
- expect(plain).toContain("Task 1");
794
- });
795
-
796
- test("includes version when provided", () => {
797
- const workspace = "test-workspace";
798
- const result = renderProjectDashboard(workspace, [], { version: "2.0.0" });
799
- const plain = stripAnsi(result);
800
-
801
- expect(plain).toContain("v2.0.0");
802
- });
803
-
804
- test("strips ANSI when requested", () => {
805
- const workspace = "test-workspace";
806
- const result = renderProjectDashboard(workspace, [], { stripAnsiCodes: true });
807
-
808
- expect(result).not.toMatch(/\x1b\[/);
809
- });
810
-
811
- test("shows tasks table", () => {
812
- const workspace = "test-workspace";
813
- const tasks = [
814
- createTask({ id: "t1", title: "Active Task", status: "pending" }),
815
- ];
816
-
817
- const result = renderProjectDashboard(workspace, tasks);
818
- const plain = stripAnsi(result);
819
-
820
- expect(plain).toContain("Tasks");
821
- expect(plain).toContain("Active Task");
822
- });
823
- });
824
-
825
- // =============================================================================
826
- // renderGlobalDashboard Tests
827
- // =============================================================================
828
-
829
- describe("renderGlobalDashboard", () => {
830
- test("renders global dashboard with all components", () => {
831
- const workspaces = [
832
- createWorkspaceInfo({ name: "workspace-a" }),
833
- createWorkspaceInfo({ name: "workspace-b" }),
834
- ];
835
- const tasks = [
836
- createTask({ id: "t1", workspace: "workspace-a", status: "pending" }),
837
- createTask({ id: "t2", workspace: "workspace-b", status: "completed" }),
838
- ];
839
- const inboxItems = [
840
- createInboxItem({ content: "Inbox note", status: "pending" }),
841
- ];
842
-
843
- const result = renderGlobalDashboard(
844
- workspaces,
845
- tasks,
846
- inboxItems,
847
- (workspace) => tasks.filter((t) => t.workspace === workspace)
848
- );
849
- const plain = stripAnsi(result);
850
-
851
- // Banner is rendered as ASCII art blocks
852
- expect(plain).toMatch(/████/);
853
- expect(plain).toContain("workspace-a");
854
- expect(plain).toContain("workspace-b");
855
- expect(plain).toContain("Inbox note");
856
- });
857
-
858
- test("includes version when provided", () => {
859
- const result = renderGlobalDashboard([], [], [], () => [], { version: "3.0.0" });
860
- const plain = stripAnsi(result);
861
-
862
- expect(plain).toContain("v3.0.0");
863
- });
864
-
865
- test("strips ANSI when requested", () => {
866
- const result = renderGlobalDashboard([], [], [], () => [], {
867
- stripAnsiCodes: true,
868
- });
869
-
870
- expect(result).not.toMatch(/\x1b\[/);
871
- });
872
-
873
- test("shows tasks table for single workspace", () => {
874
- const workspaces = [createWorkspaceInfo({ name: "solo-workspace" })];
875
- const tasks = [
876
- createTask({ id: "t1", workspace: "solo-workspace", title: "Solo Task", status: "pending" }),
877
- ];
878
-
879
- const result = renderGlobalDashboard(workspaces, tasks, [], () => tasks);
880
- const plain = stripAnsi(result);
881
-
882
- expect(plain).toContain("Tasks");
883
- expect(plain).toContain("Solo Task");
884
- });
885
-
886
- test("uses getWorkspaceTasks callback correctly", () => {
887
- const workspaces = [
888
- createWorkspaceInfo({ name: "workspace-one" }),
889
- createWorkspaceInfo({ name: "workspace-two" }),
890
- ];
891
- const allTasks = [
892
- createTask({ id: "t1", workspace: "workspace-one", status: "completed" }),
893
- createTask({ id: "t2", workspace: "workspace-one", status: "pending" }),
894
- createTask({ id: "t3", workspace: "workspace-two", status: "pending" }),
895
- ];
896
-
897
- let callCount = 0;
898
- const getWorkspaceTasks = (workspace: string) => {
899
- callCount++;
900
- return allTasks.filter((t) => t.workspace === workspace);
901
- };
902
-
903
- renderGlobalDashboard(workspaces, allTasks, [], getWorkspaceTasks);
904
-
905
- // Should be called for each workspace in the table
906
- expect(callCount).toBeGreaterThan(0);
907
- });
908
- });
909
-
910
- // =============================================================================
911
- // Edge Cases and Integration Tests
912
- // =============================================================================
913
-
914
- describe("edge cases", () => {
915
- test("handles tasks with all statuses", () => {
916
- const tasks: Task[] = [
917
- createTask({ id: "t1", status: "pending" }),
918
- createTask({ id: "t2", status: "in_progress" }),
919
- createTask({ id: "t3", status: "completed" }),
920
- createTask({ id: "t4", status: "blocked" }),
921
- createTask({ id: "t5", status: "cancelled" }),
922
- ];
923
-
924
- const stats = calculateStats(tasks);
925
-
926
- expect(stats.total).toBe(5);
927
- expect(stats.pending).toBe(1);
928
- expect(stats.inProgress).toBe(1);
929
- expect(stats.completed).toBe(1);
930
- expect(stats.blocked).toBe(1);
931
- expect(stats.cancelled).toBe(1);
932
- });
933
-
934
- test("handles circular-like dependency scenario", () => {
935
- // Not truly circular, but complex dependency chain
936
- const tasks = [
937
- createTask({ id: "t1", status: "pending" }),
938
- createTask({
939
- id: "t2",
940
- status: "pending",
941
- dependencies: [{ taskId: "t1", type: "blocked_by" }],
942
- }),
943
- createTask({
944
- id: "t3",
945
- status: "pending",
946
- dependencies: [{ taskId: "t2", type: "blocked_by" }],
947
- }),
948
- ];
949
-
950
- const metrics = calculateDependencyMetrics(tasks);
951
-
952
- expect(metrics.readyToWork).toBe(1); // Only t1
953
- expect(metrics.blockedByDependencies).toBe(2); // t2, t3
954
- });
955
-
956
- test("handles very long task titles", () => {
957
- const tasks = [
958
- createTask({
959
- id: "t1",
960
- title: "A".repeat(200),
961
- status: "pending",
962
- }),
963
- ];
964
-
965
- const widget = renderActionsWidget(tasks);
966
- // Should not throw and should truncate
967
- expect(widget).toBeDefined();
968
- expect(widget.length).toBeLessThan(1000);
969
- });
970
-
971
- test("handles special characters in content", () => {
972
- const items = [
973
- createInboxItem({
974
- content: "Test with <special> & \"characters\"",
975
- status: "pending",
976
- }),
977
- ];
978
-
979
- const widget = renderInboxWidget(items);
980
- expect(widget).toBeDefined();
981
- expect(widget).not.toBeNull();
982
- });
983
- });