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