@task-mcp/shared 1.0.22 → 1.0.24
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 +47 -0
- package/dist/algorithms/critical-path.d.ts.map +1 -0
- package/dist/algorithms/critical-path.js +340 -0
- package/dist/algorithms/critical-path.js.map +1 -0
- package/dist/algorithms/critical-path.test.d.ts +2 -0
- package/dist/algorithms/critical-path.test.d.ts.map +1 -0
- package/dist/algorithms/critical-path.test.js +184 -0
- package/dist/algorithms/critical-path.test.js.map +1 -0
- package/dist/algorithms/dependency-integrity.d.ts +81 -0
- package/dist/algorithms/dependency-integrity.d.ts.map +1 -0
- package/dist/algorithms/dependency-integrity.js +209 -0
- package/dist/algorithms/dependency-integrity.js.map +1 -0
- 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 +296 -0
- package/dist/algorithms/dependency-integrity.test.js.map +1 -0
- package/dist/algorithms/index.d.ts +5 -0
- package/dist/algorithms/index.d.ts.map +1 -0
- package/dist/algorithms/index.js +5 -0
- package/dist/algorithms/index.js.map +1 -0
- package/dist/algorithms/tech-analysis.d.ts +106 -0
- package/dist/algorithms/tech-analysis.d.ts.map +1 -0
- package/dist/algorithms/tech-analysis.js +351 -0
- package/dist/algorithms/tech-analysis.js.map +1 -0
- package/dist/algorithms/tech-analysis.test.d.ts +2 -0
- package/dist/algorithms/tech-analysis.test.d.ts.map +1 -0
- package/dist/algorithms/tech-analysis.test.js +330 -0
- package/dist/algorithms/tech-analysis.test.js.map +1 -0
- package/dist/algorithms/topological-sort.d.ts +58 -0
- package/dist/algorithms/topological-sort.d.ts.map +1 -0
- package/dist/algorithms/topological-sort.js +201 -0
- package/dist/algorithms/topological-sort.js.map +1 -0
- package/dist/algorithms/topological-sort.test.d.ts +2 -0
- package/dist/algorithms/topological-sort.test.d.ts.map +1 -0
- package/dist/algorithms/topological-sort.test.js +154 -0
- package/dist/algorithms/topological-sort.test.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/inbox.d.ts +55 -0
- package/dist/schemas/inbox.d.ts.map +1 -0
- package/dist/schemas/inbox.js +25 -0
- package/dist/schemas/inbox.js.map +1 -0
- package/dist/schemas/index.d.ts +7 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +17 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/llm-guide.d.ts +147 -0
- package/dist/schemas/llm-guide.d.ts.map +1 -0
- package/dist/schemas/llm-guide.js +72 -0
- package/dist/schemas/llm-guide.js.map +1 -0
- package/dist/schemas/project.d.ts +177 -0
- package/dist/schemas/project.d.ts.map +1 -0
- package/dist/schemas/project.js +56 -0
- package/dist/schemas/project.js.map +1 -0
- package/dist/schemas/response-format.d.ts +148 -0
- package/dist/schemas/response-format.d.ts.map +1 -0
- package/dist/schemas/response-format.js +18 -0
- package/dist/schemas/response-format.js.map +1 -0
- package/dist/schemas/response-schema.d.ts +307 -0
- package/dist/schemas/response-schema.d.ts.map +1 -0
- package/dist/schemas/response-schema.js +78 -0
- package/dist/schemas/response-schema.js.map +1 -0
- package/dist/schemas/response-schema.test.d.ts +2 -0
- package/dist/schemas/response-schema.test.d.ts.map +1 -0
- package/dist/schemas/response-schema.test.js +256 -0
- package/dist/schemas/response-schema.test.js.map +1 -0
- 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 +881 -0
- package/dist/schemas/task.d.ts.map +1 -0
- package/dist/schemas/task.js +177 -0
- package/dist/schemas/task.js.map +1 -0
- package/dist/schemas/view.d.ts +143 -0
- package/dist/schemas/view.d.ts.map +1 -0
- package/dist/schemas/view.js +48 -0
- package/dist/schemas/view.js.map +1 -0
- package/dist/utils/dashboard-renderer.d.ts +93 -0
- package/dist/utils/dashboard-renderer.d.ts.map +1 -0
- package/dist/utils/dashboard-renderer.js +416 -0
- package/dist/utils/dashboard-renderer.js.map +1 -0
- 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 +772 -0
- package/dist/utils/dashboard-renderer.test.js.map +1 -0
- package/dist/utils/date.d.ts +94 -0
- package/dist/utils/date.d.ts.map +1 -0
- package/dist/utils/date.js +323 -0
- package/dist/utils/date.js.map +1 -0
- package/dist/utils/date.test.d.ts +2 -0
- package/dist/utils/date.test.d.ts.map +1 -0
- package/dist/utils/date.test.js +276 -0
- package/dist/utils/date.test.js.map +1 -0
- package/dist/utils/hierarchy.d.ts +102 -0
- package/dist/utils/hierarchy.d.ts.map +1 -0
- package/dist/utils/hierarchy.js +236 -0
- package/dist/utils/hierarchy.js.map +1 -0
- 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 +423 -0
- package/dist/utils/hierarchy.test.js.map +1 -0
- package/dist/utils/id.d.ts +60 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +118 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/id.test.d.ts +2 -0
- package/dist/utils/id.test.d.ts.map +1 -0
- package/dist/utils/id.test.js +193 -0
- package/dist/utils/id.test.js.map +1 -0
- package/dist/utils/index.d.ts +12 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +34 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/natural-language.d.ts +111 -0
- package/dist/utils/natural-language.d.ts.map +1 -0
- package/dist/utils/natural-language.js +297 -0
- package/dist/utils/natural-language.js.map +1 -0
- package/dist/utils/natural-language.test.d.ts +2 -0
- package/dist/utils/natural-language.test.d.ts.map +1 -0
- package/dist/utils/natural-language.test.js +197 -0
- package/dist/utils/natural-language.test.js.map +1 -0
- 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/priority-queue.test.d.ts +2 -0
- package/dist/utils/priority-queue.test.d.ts.map +1 -0
- package/dist/utils/priority-queue.test.js +82 -0
- package/dist/utils/priority-queue.test.js.map +1 -0
- package/dist/utils/projection.d.ts +65 -0
- package/dist/utils/projection.d.ts.map +1 -0
- package/dist/utils/projection.js +180 -0
- package/dist/utils/projection.js.map +1 -0
- package/dist/utils/projection.test.d.ts +2 -0
- package/dist/utils/projection.test.d.ts.map +1 -0
- package/dist/utils/projection.test.js +341 -0
- package/dist/utils/projection.test.js.map +1 -0
- package/dist/utils/terminal-ui.d.ts +208 -0
- package/dist/utils/terminal-ui.d.ts.map +1 -0
- package/dist/utils/terminal-ui.js +614 -0
- package/dist/utils/terminal-ui.js.map +1 -0
- 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/dist/utils/workspace.d.ts +102 -0
- package/dist/utils/workspace.d.ts.map +1 -0
- package/dist/utils/workspace.js +183 -0
- package/dist/utils/workspace.js.map +1 -0
- package/dist/utils/workspace.test.d.ts +2 -0
- package/dist/utils/workspace.test.d.ts.map +1 -0
- package/dist/utils/workspace.test.js +97 -0
- package/dist/utils/workspace.test.js.map +1 -0
- package/package.json +5 -1
- package/src/algorithms/critical-path.test.ts +227 -0
- package/src/algorithms/critical-path.ts +14 -34
- package/src/algorithms/dependency-integrity.test.ts +335 -0
- package/src/algorithms/dependency-integrity.ts +4 -13
- package/src/algorithms/tech-analysis.test.ts +405 -0
- package/src/algorithms/tech-analysis.ts +27 -27
- package/src/algorithms/topological-sort.test.ts +182 -0
- package/src/algorithms/topological-sort.ts +6 -10
- package/src/schemas/index.ts +2 -13
- package/src/schemas/response-format.ts +6 -6
- package/src/schemas/response-schema.test.ts +314 -0
- package/src/schemas/response-schema.ts +25 -20
- package/src/schemas/task.ts +4 -22
- package/src/utils/dashboard-renderer.test.ts +976 -0
- package/src/utils/dashboard-renderer.ts +27 -59
- package/src/utils/date.test.ts +329 -0
- package/src/utils/date.ts +2 -10
- package/src/utils/hierarchy.test.ts +488 -0
- package/src/utils/hierarchy.ts +4 -5
- package/src/utils/id.test.ts +235 -0
- package/src/utils/index.ts +7 -1
- package/src/utils/natural-language.test.ts +234 -0
- package/src/utils/priority-queue.test.ts +103 -0
- package/src/utils/projection.test.ts +430 -0
- package/src/utils/terminal-ui.test.ts +831 -0
- package/src/utils/terminal-ui.ts +53 -54
- package/src/utils/workspace.test.ts +125 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { criticalPathAnalysis, findParallelTasks, suggestNextTask } from "./critical-path.js";
|
|
3
|
+
import type { Task } from "../schemas/task.js";
|
|
4
|
+
|
|
5
|
+
// Helper to create mock tasks
|
|
6
|
+
function createTask(
|
|
7
|
+
id: string,
|
|
8
|
+
options: {
|
|
9
|
+
priority?: string;
|
|
10
|
+
deps?: string[];
|
|
11
|
+
estimate?: number;
|
|
12
|
+
status?: string;
|
|
13
|
+
contexts?: string[];
|
|
14
|
+
} = {}
|
|
15
|
+
): Task {
|
|
16
|
+
const task: Task = {
|
|
17
|
+
id,
|
|
18
|
+
title: `Task ${id}`,
|
|
19
|
+
status: (options.status ?? "pending") as Task["status"],
|
|
20
|
+
priority: (options.priority ?? "medium") as Task["priority"],
|
|
21
|
+
workspace: "test-workspace",
|
|
22
|
+
createdAt: new Date().toISOString(),
|
|
23
|
+
updatedAt: new Date().toISOString(),
|
|
24
|
+
dependencies: (options.deps ?? []).map((depId) => ({
|
|
25
|
+
taskId: depId,
|
|
26
|
+
type: "blocked_by" as const,
|
|
27
|
+
})),
|
|
28
|
+
};
|
|
29
|
+
if (options.estimate) {
|
|
30
|
+
task.estimate = { expected: options.estimate, confidence: "medium" as const };
|
|
31
|
+
}
|
|
32
|
+
if (options.contexts) {
|
|
33
|
+
task.contexts = options.contexts;
|
|
34
|
+
}
|
|
35
|
+
return task;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("criticalPathAnalysis", () => {
|
|
39
|
+
test("returns empty result for empty input", () => {
|
|
40
|
+
const result = criticalPathAnalysis([]);
|
|
41
|
+
expect(result.tasks).toEqual([]);
|
|
42
|
+
expect(result.criticalPath).toEqual([]);
|
|
43
|
+
expect(result.projectDuration).toBe(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("returns empty for all completed tasks", () => {
|
|
47
|
+
const tasks = [createTask("A", { status: "completed" })];
|
|
48
|
+
const result = criticalPathAnalysis(tasks);
|
|
49
|
+
expect(result.tasks).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("calculates single task correctly", () => {
|
|
53
|
+
const tasks = [createTask("A", { estimate: 60 })];
|
|
54
|
+
const result = criticalPathAnalysis(tasks);
|
|
55
|
+
|
|
56
|
+
expect(result.projectDuration).toBe(60);
|
|
57
|
+
expect(result.criticalPath.length).toBe(1);
|
|
58
|
+
expect(result.criticalPath[0]!.id).toBe("A");
|
|
59
|
+
expect(result.criticalPath[0]!.isCritical).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("identifies critical path in linear chain", () => {
|
|
63
|
+
// A -> B -> C (each 30 min)
|
|
64
|
+
const tasks = [
|
|
65
|
+
createTask("A", { estimate: 30 }),
|
|
66
|
+
createTask("B", { estimate: 30, deps: ["A"] }),
|
|
67
|
+
createTask("C", { estimate: 30, deps: ["B"] }),
|
|
68
|
+
];
|
|
69
|
+
const result = criticalPathAnalysis(tasks);
|
|
70
|
+
|
|
71
|
+
expect(result.projectDuration).toBe(90);
|
|
72
|
+
expect(result.criticalPath.length).toBe(3);
|
|
73
|
+
expect(result.criticalPath.map((t) => t.id)).toEqual(["A", "B", "C"]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("calculates slack for parallel tasks", () => {
|
|
77
|
+
// A (60) and B (30) both lead to C
|
|
78
|
+
// A is on critical path, B has 30 min slack
|
|
79
|
+
const tasks = [
|
|
80
|
+
createTask("A", { estimate: 60 }),
|
|
81
|
+
createTask("B", { estimate: 30 }),
|
|
82
|
+
createTask("C", { estimate: 30, deps: ["A", "B"] }),
|
|
83
|
+
];
|
|
84
|
+
const result = criticalPathAnalysis(tasks);
|
|
85
|
+
|
|
86
|
+
expect(result.projectDuration).toBe(90); // A + C
|
|
87
|
+
|
|
88
|
+
const taskA = result.tasks.find((t) => t.id === "A")!;
|
|
89
|
+
const taskB = result.tasks.find((t) => t.id === "B")!;
|
|
90
|
+
const taskC = result.tasks.find((t) => t.id === "C")!;
|
|
91
|
+
|
|
92
|
+
expect(taskA.isCritical).toBe(true);
|
|
93
|
+
expect(taskB.slack).toBe(30); // B can start 30 min late
|
|
94
|
+
expect(taskC.isCritical).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("identifies bottlenecks by dependent count", () => {
|
|
98
|
+
// A blocks B, C, D
|
|
99
|
+
const tasks = [
|
|
100
|
+
createTask("A", { estimate: 30 }),
|
|
101
|
+
createTask("B", { estimate: 30, deps: ["A"] }),
|
|
102
|
+
createTask("C", { estimate: 30, deps: ["A"] }),
|
|
103
|
+
createTask("D", { estimate: 30, deps: ["A"] }),
|
|
104
|
+
];
|
|
105
|
+
const result = criticalPathAnalysis(tasks);
|
|
106
|
+
|
|
107
|
+
expect(result.bottlenecks.length).toBeGreaterThan(0);
|
|
108
|
+
expect(result.bottlenecks[0]!.id).toBe("A"); // A blocks the most tasks
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("findParallelTasks", () => {
|
|
113
|
+
test("returns empty for empty input", () => {
|
|
114
|
+
const result = findParallelTasks([]);
|
|
115
|
+
expect(result).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("returns single group for independent tasks", () => {
|
|
119
|
+
const tasks = [createTask("A"), createTask("B"), createTask("C")];
|
|
120
|
+
const result = findParallelTasks(tasks);
|
|
121
|
+
|
|
122
|
+
expect(result.length).toBe(1);
|
|
123
|
+
expect(result[0]!.length).toBe(3);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("excludes tasks with uncompleted dependencies", () => {
|
|
127
|
+
const tasks = [createTask("A"), createTask("B", { deps: ["A"] })];
|
|
128
|
+
const result = findParallelTasks(tasks);
|
|
129
|
+
|
|
130
|
+
// Only A is available (B is blocked)
|
|
131
|
+
expect(result.length).toBe(1);
|
|
132
|
+
expect(result[0]!.length).toBe(1);
|
|
133
|
+
expect(result[0]![0]!.id).toBe("A");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("includes task when dependency is completed", () => {
|
|
137
|
+
const tasks = [
|
|
138
|
+
createTask("A", { status: "completed" }),
|
|
139
|
+
createTask("B", { deps: ["A"] }),
|
|
140
|
+
createTask("C"),
|
|
141
|
+
];
|
|
142
|
+
const result = findParallelTasks(tasks);
|
|
143
|
+
|
|
144
|
+
// B and C can run in parallel
|
|
145
|
+
expect(result.length).toBe(1);
|
|
146
|
+
expect(result[0]!.map((t) => t.id).sort()).toEqual(["B", "C"]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("excludes completed tasks from result", () => {
|
|
150
|
+
const tasks = [createTask("A", { status: "completed" }), createTask("B")];
|
|
151
|
+
const result = findParallelTasks(tasks);
|
|
152
|
+
|
|
153
|
+
expect(result.length).toBe(1);
|
|
154
|
+
expect(result[0]!.length).toBe(1);
|
|
155
|
+
expect(result[0]![0]!.id).toBe("B");
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("suggestNextTask", () => {
|
|
160
|
+
test("returns null for empty input", () => {
|
|
161
|
+
const result = suggestNextTask([]);
|
|
162
|
+
expect(result).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("returns null when all tasks completed", () => {
|
|
166
|
+
const tasks = [createTask("A", { status: "completed" })];
|
|
167
|
+
const result = suggestNextTask(tasks);
|
|
168
|
+
expect(result).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("prefers critical path tasks", () => {
|
|
172
|
+
// A is critical (longer), B has slack
|
|
173
|
+
const tasks = [
|
|
174
|
+
createTask("A", { estimate: 60, priority: "low" }),
|
|
175
|
+
createTask("B", { estimate: 30, priority: "high" }),
|
|
176
|
+
createTask("C", { estimate: 30, deps: ["A", "B"] }),
|
|
177
|
+
];
|
|
178
|
+
const result = suggestNextTask(tasks);
|
|
179
|
+
|
|
180
|
+
// A is on critical path, should be suggested first
|
|
181
|
+
expect(result!.id).toBe("A");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("filters by context when specified", () => {
|
|
185
|
+
const tasks = [
|
|
186
|
+
createTask("A", { priority: "critical", contexts: ["office"] }),
|
|
187
|
+
createTask("B", { priority: "high", contexts: ["focus"] }),
|
|
188
|
+
];
|
|
189
|
+
const result = suggestNextTask(tasks, { contexts: ["focus"] });
|
|
190
|
+
|
|
191
|
+
expect(result!.id).toBe("B");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("filters by max time when specified", () => {
|
|
195
|
+
const tasks = [
|
|
196
|
+
createTask("A", { estimate: 120, priority: "critical" }),
|
|
197
|
+
createTask("B", { estimate: 30, priority: "high" }),
|
|
198
|
+
];
|
|
199
|
+
const result = suggestNextTask(tasks, { maxMinutes: 60 });
|
|
200
|
+
|
|
201
|
+
expect(result!.id).toBe("B");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("skips blocked tasks", () => {
|
|
205
|
+
const tasks = [
|
|
206
|
+
createTask("A", { priority: "low" }),
|
|
207
|
+
createTask("B", { priority: "critical", deps: ["A"] }),
|
|
208
|
+
];
|
|
209
|
+
const result = suggestNextTask(tasks);
|
|
210
|
+
|
|
211
|
+
// B is blocked, so A should be suggested
|
|
212
|
+
expect(result!.id).toBe("A");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("considers tasks with more dependents", () => {
|
|
216
|
+
const tasks = [
|
|
217
|
+
createTask("A", { priority: "medium" }),
|
|
218
|
+
createTask("B", { priority: "medium" }),
|
|
219
|
+
createTask("C", { priority: "medium", deps: ["A"] }),
|
|
220
|
+
createTask("D", { priority: "medium", deps: ["A"] }),
|
|
221
|
+
];
|
|
222
|
+
const result = suggestNextTask(tasks);
|
|
223
|
+
|
|
224
|
+
// A blocks more tasks (C and D), should be preferred over B
|
|
225
|
+
expect(result!.id).toBe("A");
|
|
226
|
+
});
|
|
227
|
+
});
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import type { Task } from "../schemas/task.js";
|
|
2
|
-
import {
|
|
3
|
-
topologicalSort,
|
|
4
|
-
findDependents,
|
|
5
|
-
priorityToNumber,
|
|
6
|
-
} from "./topological-sort.js";
|
|
2
|
+
import { topologicalSort, priorityToNumber } from "./topological-sort.js";
|
|
7
3
|
|
|
8
4
|
/**
|
|
9
5
|
* Task with computed CPM (Critical Path Method) values
|
|
@@ -43,9 +39,7 @@ function getTaskDuration(task: Task): number {
|
|
|
43
39
|
* Get blocked_by dependencies for a task
|
|
44
40
|
*/
|
|
45
41
|
function getBlockedByDeps(task: Task): string[] {
|
|
46
|
-
return (task.dependencies ?? [])
|
|
47
|
-
.filter((d) => d.type === "blocked_by")
|
|
48
|
-
.map((d) => d.taskId);
|
|
42
|
+
return (task.dependencies ?? []).filter((d) => d.type === "blocked_by").map((d) => d.taskId);
|
|
49
43
|
}
|
|
50
44
|
|
|
51
45
|
/**
|
|
@@ -110,7 +104,10 @@ function calculateProjectDuration(taskMap: Map<string, CPMTask>): number {
|
|
|
110
104
|
* This is O(n * d) where d is average dependencies, done once upfront
|
|
111
105
|
* Allows O(1) successor lookup instead of O(n) per task
|
|
112
106
|
*/
|
|
113
|
-
function buildSuccessorIndex(
|
|
107
|
+
function buildSuccessorIndex(
|
|
108
|
+
sortedTasks: Task[],
|
|
109
|
+
taskMap: Map<string, CPMTask>
|
|
110
|
+
): Map<string, CPMTask[]> {
|
|
114
111
|
const successorIndex = new Map<string, CPMTask[]>();
|
|
115
112
|
|
|
116
113
|
// Initialize empty arrays for all tasks
|
|
@@ -244,9 +241,7 @@ function extractCriticalPath(sortedTasks: Task[], taskMap: Map<string, CPMTask>)
|
|
|
244
241
|
* Find top bottlenecks (critical tasks blocking the most downstream work)
|
|
245
242
|
*/
|
|
246
243
|
function findBottlenecks(criticalPath: CPMTask[], limit = 5): CPMTask[] {
|
|
247
|
-
return [...criticalPath]
|
|
248
|
-
.sort((a, b) => b.dependentCount - a.dependentCount)
|
|
249
|
-
.slice(0, limit);
|
|
244
|
+
return [...criticalPath].sort((a, b) => b.dependentCount - a.dependentCount).slice(0, limit);
|
|
250
245
|
}
|
|
251
246
|
|
|
252
247
|
/**
|
|
@@ -262,9 +257,7 @@ function findBottlenecks(criticalPath: CPMTask[], limit = 5): CPMTask[] {
|
|
|
262
257
|
*/
|
|
263
258
|
export function criticalPathAnalysis(tasks: Task[]): CPMResult {
|
|
264
259
|
// Filter to only pending/in_progress tasks
|
|
265
|
-
const activeTasks = tasks.filter(
|
|
266
|
-
(t) => t.status === "pending" || t.status === "in_progress"
|
|
267
|
-
);
|
|
260
|
+
const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
|
|
268
261
|
|
|
269
262
|
if (activeTasks.length === 0) {
|
|
270
263
|
return {
|
|
@@ -318,16 +311,12 @@ export function criticalPathAnalysis(tasks: Task[]): CPMResult {
|
|
|
318
311
|
* Optimized to O(n + e) where n = number of tasks, e = number of dependencies
|
|
319
312
|
*/
|
|
320
313
|
export function findParallelTasks(tasks: Task[]): Task[][] {
|
|
321
|
-
const activeTasks = tasks.filter(
|
|
322
|
-
(t) => t.status === "pending" || t.status === "in_progress"
|
|
323
|
-
);
|
|
314
|
+
const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
|
|
324
315
|
|
|
325
316
|
if (activeTasks.length === 0) return [];
|
|
326
317
|
|
|
327
318
|
// Find tasks with no uncompleted dependencies
|
|
328
|
-
const completedIds = new Set(
|
|
329
|
-
tasks.filter((t) => t.status === "completed").map((t) => t.id)
|
|
330
|
-
);
|
|
319
|
+
const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
|
|
331
320
|
|
|
332
321
|
const available = activeTasks.filter((task) => {
|
|
333
322
|
const deps = getBlockedByDeps(task);
|
|
@@ -376,9 +365,7 @@ export function findParallelTasks(tasks: Task[]): Task[][] {
|
|
|
376
365
|
if (processed.has(other.id)) continue;
|
|
377
366
|
|
|
378
367
|
// O(1) check: tasks are independent if neither depends on the other
|
|
379
|
-
const independent =
|
|
380
|
-
!conflicting.has(other.id) &&
|
|
381
|
-
!dependsOn.get(other.id)?.has(task.id);
|
|
368
|
+
const independent = !conflicting.has(other.id) && !dependsOn.get(other.id)?.has(task.id);
|
|
382
369
|
|
|
383
370
|
if (independent) {
|
|
384
371
|
group.push(other);
|
|
@@ -402,9 +389,7 @@ export function suggestNextTask(
|
|
|
402
389
|
maxMinutes?: number;
|
|
403
390
|
} = {}
|
|
404
391
|
): Task | null {
|
|
405
|
-
const activeTasks = tasks.filter(
|
|
406
|
-
(t) => t.status === "pending" || t.status === "in_progress"
|
|
407
|
-
);
|
|
392
|
+
const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
|
|
408
393
|
|
|
409
394
|
if (activeTasks.length === 0) return null;
|
|
410
395
|
|
|
@@ -412,9 +397,7 @@ export function suggestNextTask(
|
|
|
412
397
|
const cpm = criticalPathAnalysis(tasks);
|
|
413
398
|
|
|
414
399
|
// Filter by availability (all dependencies completed)
|
|
415
|
-
const completedIds = new Set(
|
|
416
|
-
tasks.filter((t) => t.status === "completed").map((t) => t.id)
|
|
417
|
-
);
|
|
400
|
+
const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
|
|
418
401
|
|
|
419
402
|
let candidates = cpm.tasks.filter((task) => {
|
|
420
403
|
const deps = getBlockedByDeps(task);
|
|
@@ -434,9 +417,7 @@ export function suggestNextTask(
|
|
|
434
417
|
|
|
435
418
|
// Filter by time if specified
|
|
436
419
|
if (options.maxMinutes) {
|
|
437
|
-
const timeFiltered = candidates.filter(
|
|
438
|
-
(t) => getTaskDuration(t) <= options.maxMinutes!
|
|
439
|
-
);
|
|
420
|
+
const timeFiltered = candidates.filter((t) => getTaskDuration(t) <= options.maxMinutes!);
|
|
440
421
|
if (timeFiltered.length > 0) {
|
|
441
422
|
candidates = timeFiltered;
|
|
442
423
|
}
|
|
@@ -459,4 +440,3 @@ export function suggestNextTask(
|
|
|
459
440
|
scored.sort((a, b) => b.score - a.score);
|
|
460
441
|
return scored[0]?.task ?? null;
|
|
461
442
|
}
|
|
462
|
-
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
DependencyErrorCode,
|
|
4
|
+
validateDependency,
|
|
5
|
+
findInvalidDependencies,
|
|
6
|
+
findSelfDependencies,
|
|
7
|
+
detectCircularDependencies,
|
|
8
|
+
checkDependencyIntegrity,
|
|
9
|
+
} from "./dependency-integrity.js";
|
|
10
|
+
import type { Task } from "../schemas/task.js";
|
|
11
|
+
|
|
12
|
+
// Helper to create mock tasks
|
|
13
|
+
function createTask(
|
|
14
|
+
id: string,
|
|
15
|
+
title: string = `Task ${id}`,
|
|
16
|
+
deps: Array<{ taskId: string; type: "blocked_by" | "blocks" | "related" }> = []
|
|
17
|
+
): Task {
|
|
18
|
+
return {
|
|
19
|
+
id,
|
|
20
|
+
title,
|
|
21
|
+
status: "pending",
|
|
22
|
+
priority: "medium",
|
|
23
|
+
workspace: "test-workspace",
|
|
24
|
+
createdAt: new Date().toISOString(),
|
|
25
|
+
updatedAt: new Date().toISOString(),
|
|
26
|
+
dependencies: deps,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("validateDependency", () => {
|
|
31
|
+
test("returns valid for a valid dependency", () => {
|
|
32
|
+
const tasks = [createTask("A"), createTask("B")];
|
|
33
|
+
const result = validateDependency({
|
|
34
|
+
taskId: "B",
|
|
35
|
+
blockedBy: "A",
|
|
36
|
+
tasks,
|
|
37
|
+
});
|
|
38
|
+
expect(result.valid).toBe(true);
|
|
39
|
+
expect(result.errorCode).toBeUndefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("rejects self-dependency", () => {
|
|
43
|
+
const tasks = [createTask("A")];
|
|
44
|
+
const result = validateDependency({
|
|
45
|
+
taskId: "A",
|
|
46
|
+
blockedBy: "A",
|
|
47
|
+
tasks,
|
|
48
|
+
});
|
|
49
|
+
expect(result.valid).toBe(false);
|
|
50
|
+
expect(result.errorCode).toBe(DependencyErrorCode.SELF_DEPENDENCY);
|
|
51
|
+
expect(result.errorMessage).toContain("cannot depend on itself");
|
|
52
|
+
expect(result.suggestion).toBeDefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("rejects when task not found", () => {
|
|
56
|
+
const tasks = [createTask("A")];
|
|
57
|
+
const result = validateDependency({
|
|
58
|
+
taskId: "B",
|
|
59
|
+
blockedBy: "A",
|
|
60
|
+
tasks,
|
|
61
|
+
});
|
|
62
|
+
expect(result.valid).toBe(false);
|
|
63
|
+
expect(result.errorCode).toBe(DependencyErrorCode.TASK_NOT_FOUND);
|
|
64
|
+
expect(result.errorMessage).toContain("Task not found");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("rejects when blocker not found", () => {
|
|
68
|
+
const tasks = [createTask("A")];
|
|
69
|
+
const result = validateDependency({
|
|
70
|
+
taskId: "A",
|
|
71
|
+
blockedBy: "B",
|
|
72
|
+
tasks,
|
|
73
|
+
});
|
|
74
|
+
expect(result.valid).toBe(false);
|
|
75
|
+
expect(result.errorCode).toBe(DependencyErrorCode.BLOCKER_NOT_FOUND);
|
|
76
|
+
expect(result.errorMessage).toContain("Blocking task not found");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("rejects duplicate dependency", () => {
|
|
80
|
+
const tasks = [
|
|
81
|
+
createTask("A"),
|
|
82
|
+
createTask("B", "Task B", [{ taskId: "A", type: "blocked_by" }]),
|
|
83
|
+
];
|
|
84
|
+
const result = validateDependency({
|
|
85
|
+
taskId: "B",
|
|
86
|
+
blockedBy: "A",
|
|
87
|
+
tasks,
|
|
88
|
+
});
|
|
89
|
+
expect(result.valid).toBe(false);
|
|
90
|
+
expect(result.errorCode).toBe(DependencyErrorCode.DUPLICATE_DEPENDENCY);
|
|
91
|
+
expect(result.errorMessage).toContain("already blocked by");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("allows duplicate when checkDuplicates is false", () => {
|
|
95
|
+
const tasks = [
|
|
96
|
+
createTask("A"),
|
|
97
|
+
createTask("B", "Task B", [{ taskId: "A", type: "blocked_by" }]),
|
|
98
|
+
];
|
|
99
|
+
const result = validateDependency({
|
|
100
|
+
taskId: "B",
|
|
101
|
+
blockedBy: "A",
|
|
102
|
+
tasks,
|
|
103
|
+
checkDuplicates: false,
|
|
104
|
+
});
|
|
105
|
+
expect(result.valid).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("rejects circular dependency", () => {
|
|
109
|
+
const tasks = [
|
|
110
|
+
createTask("A", "Task A", [{ taskId: "B", type: "blocked_by" }]),
|
|
111
|
+
createTask("B"),
|
|
112
|
+
];
|
|
113
|
+
// B is blocking A, so A blocking B would create a cycle
|
|
114
|
+
const result = validateDependency({
|
|
115
|
+
taskId: "B",
|
|
116
|
+
blockedBy: "A",
|
|
117
|
+
tasks,
|
|
118
|
+
});
|
|
119
|
+
expect(result.valid).toBe(false);
|
|
120
|
+
expect(result.errorCode).toBe(DependencyErrorCode.CIRCULAR_DEPENDENCY);
|
|
121
|
+
expect(result.errorMessage).toContain("cycle");
|
|
122
|
+
expect(result.suggestion).toContain("depends on");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("rejects indirect circular dependency", () => {
|
|
126
|
+
const tasks = [
|
|
127
|
+
createTask("A", "Task A", [{ taskId: "B", type: "blocked_by" }]),
|
|
128
|
+
createTask("B", "Task B", [{ taskId: "C", type: "blocked_by" }]),
|
|
129
|
+
createTask("C"),
|
|
130
|
+
];
|
|
131
|
+
// A <- B <- C, adding C <- A would create cycle
|
|
132
|
+
const result = validateDependency({
|
|
133
|
+
taskId: "C",
|
|
134
|
+
blockedBy: "A",
|
|
135
|
+
tasks,
|
|
136
|
+
});
|
|
137
|
+
expect(result.valid).toBe(false);
|
|
138
|
+
expect(result.errorCode).toBe(DependencyErrorCode.CIRCULAR_DEPENDENCY);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("findInvalidDependencies", () => {
|
|
143
|
+
test("returns empty array when all dependencies are valid", () => {
|
|
144
|
+
const tasks = [
|
|
145
|
+
createTask("A"),
|
|
146
|
+
createTask("B", "Task B", [{ taskId: "A", type: "blocked_by" }]),
|
|
147
|
+
];
|
|
148
|
+
const result = findInvalidDependencies(tasks);
|
|
149
|
+
expect(result).toEqual([]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("finds orphaned dependency references", () => {
|
|
153
|
+
const tasks = [createTask("A", "Task A", [{ taskId: "non-existent", type: "blocked_by" }])];
|
|
154
|
+
const result = findInvalidDependencies(tasks);
|
|
155
|
+
expect(result.length).toBe(1);
|
|
156
|
+
expect(result[0]!.taskId).toBe("A");
|
|
157
|
+
expect(result[0]!.taskTitle).toBe("Task A");
|
|
158
|
+
expect(result[0]!.invalidDependencyId).toBe("non-existent");
|
|
159
|
+
expect(result[0]!.type).toBe("blocked_by");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("finds multiple invalid references across tasks", () => {
|
|
163
|
+
const tasks = [
|
|
164
|
+
createTask("A", "Task A", [{ taskId: "deleted-1", type: "blocked_by" }]),
|
|
165
|
+
createTask("B", "Task B", [
|
|
166
|
+
{ taskId: "A", type: "blocked_by" },
|
|
167
|
+
{ taskId: "deleted-2", type: "blocks" },
|
|
168
|
+
]),
|
|
169
|
+
];
|
|
170
|
+
const result = findInvalidDependencies(tasks);
|
|
171
|
+
expect(result.length).toBe(2);
|
|
172
|
+
expect(result.map((r) => r.invalidDependencyId).sort()).toEqual(["deleted-1", "deleted-2"]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("returns empty array for tasks without dependencies", () => {
|
|
176
|
+
const tasks = [createTask("A"), createTask("B")];
|
|
177
|
+
const result = findInvalidDependencies(tasks);
|
|
178
|
+
expect(result).toEqual([]);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("findSelfDependencies", () => {
|
|
183
|
+
test("returns empty array when no self-dependencies exist", () => {
|
|
184
|
+
const tasks = [
|
|
185
|
+
createTask("A"),
|
|
186
|
+
createTask("B", "Task B", [{ taskId: "A", type: "blocked_by" }]),
|
|
187
|
+
];
|
|
188
|
+
const result = findSelfDependencies(tasks);
|
|
189
|
+
expect(result).toEqual([]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("finds task with self-dependency", () => {
|
|
193
|
+
const tasks = [createTask("A", "Task A", [{ taskId: "A", type: "blocked_by" }])];
|
|
194
|
+
const result = findSelfDependencies(tasks);
|
|
195
|
+
expect(result).toEqual(["A"]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("finds multiple self-dependencies", () => {
|
|
199
|
+
const tasks = [
|
|
200
|
+
createTask("A", "Task A", [{ taskId: "A", type: "blocked_by" }]),
|
|
201
|
+
createTask("B"),
|
|
202
|
+
createTask("C", "Task C", [{ taskId: "C", type: "blocks" }]),
|
|
203
|
+
];
|
|
204
|
+
const result = findSelfDependencies(tasks);
|
|
205
|
+
expect(result.sort()).toEqual(["A", "C"]);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("detectCircularDependencies", () => {
|
|
210
|
+
test("returns empty array when no cycles exist", () => {
|
|
211
|
+
const tasks = [
|
|
212
|
+
createTask("A"),
|
|
213
|
+
createTask("B", "Task B", [{ taskId: "A", type: "blocked_by" }]),
|
|
214
|
+
createTask("C", "Task C", [{ taskId: "B", type: "blocked_by" }]),
|
|
215
|
+
];
|
|
216
|
+
const result = detectCircularDependencies(tasks);
|
|
217
|
+
expect(result).toEqual([]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("detects direct circular dependency (A -> B -> A)", () => {
|
|
221
|
+
const tasks = [
|
|
222
|
+
createTask("A", "Task A", [{ taskId: "B", type: "blocked_by" }]),
|
|
223
|
+
createTask("B", "Task B", [{ taskId: "A", type: "blocked_by" }]),
|
|
224
|
+
];
|
|
225
|
+
const result = detectCircularDependencies(tasks);
|
|
226
|
+
expect(result.length).toBe(1);
|
|
227
|
+
expect(result[0]!.sort()).toEqual(["A", "B"]);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("detects longer cycle (A -> B -> C -> A)", () => {
|
|
231
|
+
const tasks = [
|
|
232
|
+
createTask("A", "Task A", [{ taskId: "B", type: "blocked_by" }]),
|
|
233
|
+
createTask("B", "Task B", [{ taskId: "C", type: "blocked_by" }]),
|
|
234
|
+
createTask("C", "Task C", [{ taskId: "A", type: "blocked_by" }]),
|
|
235
|
+
];
|
|
236
|
+
const result = detectCircularDependencies(tasks);
|
|
237
|
+
expect(result.length).toBe(1);
|
|
238
|
+
expect(result[0]!.sort()).toEqual(["A", "B", "C"]);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("ignores non-blocking dependency types for cycle detection", () => {
|
|
242
|
+
const tasks = [
|
|
243
|
+
createTask("A", "Task A", [{ taskId: "B", type: "related" }]),
|
|
244
|
+
createTask("B", "Task B", [{ taskId: "A", type: "related" }]),
|
|
245
|
+
];
|
|
246
|
+
const result = detectCircularDependencies(tasks);
|
|
247
|
+
expect(result).toEqual([]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("handles tasks with missing dependency targets", () => {
|
|
251
|
+
const tasks = [createTask("A", "Task A", [{ taskId: "non-existent", type: "blocked_by" }])];
|
|
252
|
+
const result = detectCircularDependencies(tasks);
|
|
253
|
+
expect(result).toEqual([]);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("checkDependencyIntegrity", () => {
|
|
258
|
+
test("returns valid for healthy task set", () => {
|
|
259
|
+
const tasks = [
|
|
260
|
+
createTask("A"),
|
|
261
|
+
createTask("B", "Task B", [{ taskId: "A", type: "blocked_by" }]),
|
|
262
|
+
createTask("C", "Task C", [{ taskId: "B", type: "blocked_by" }]),
|
|
263
|
+
];
|
|
264
|
+
const result = checkDependencyIntegrity(tasks);
|
|
265
|
+
expect(result.valid).toBe(true);
|
|
266
|
+
expect(result.totalTasks).toBe(3);
|
|
267
|
+
expect(result.totalDependencies).toBe(2);
|
|
268
|
+
expect(result.issues.selfDependencies).toEqual([]);
|
|
269
|
+
expect(result.issues.invalidReferences).toEqual([]);
|
|
270
|
+
expect(result.issues.circularDependencies).toEqual([]);
|
|
271
|
+
expect(result.summary).toContain("All 2 dependencies");
|
|
272
|
+
expect(result.summary).toContain("valid");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("detects self-dependency issues", () => {
|
|
276
|
+
const tasks = [createTask("A", "Task A", [{ taskId: "A", type: "blocked_by" }])];
|
|
277
|
+
const result = checkDependencyIntegrity(tasks);
|
|
278
|
+
expect(result.valid).toBe(false);
|
|
279
|
+
expect(result.issues.selfDependencies).toEqual(["A"]);
|
|
280
|
+
expect(result.summary).toContain("self-dependencies");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("detects invalid reference issues", () => {
|
|
284
|
+
const tasks = [createTask("A", "Task A", [{ taskId: "deleted", type: "blocked_by" }])];
|
|
285
|
+
const result = checkDependencyIntegrity(tasks);
|
|
286
|
+
expect(result.valid).toBe(false);
|
|
287
|
+
expect(result.issues.invalidReferences.length).toBe(1);
|
|
288
|
+
expect(result.summary).toContain("invalid references");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("detects circular dependency issues", () => {
|
|
292
|
+
const tasks = [
|
|
293
|
+
createTask("A", "Task A", [{ taskId: "B", type: "blocked_by" }]),
|
|
294
|
+
createTask("B", "Task B", [{ taskId: "A", type: "blocked_by" }]),
|
|
295
|
+
];
|
|
296
|
+
const result = checkDependencyIntegrity(tasks);
|
|
297
|
+
expect(result.valid).toBe(false);
|
|
298
|
+
expect(result.issues.circularDependencies.length).toBe(1);
|
|
299
|
+
expect(result.summary).toContain("circular dependency");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("reports multiple issue types", () => {
|
|
303
|
+
const tasks = [
|
|
304
|
+
createTask("A", "Task A", [
|
|
305
|
+
{ taskId: "A", type: "blocked_by" }, // self-dependency
|
|
306
|
+
{ taskId: "deleted", type: "blocked_by" }, // invalid reference
|
|
307
|
+
]),
|
|
308
|
+
createTask("B", "Task B", [{ taskId: "C", type: "blocked_by" }]),
|
|
309
|
+
createTask("C", "Task C", [{ taskId: "B", type: "blocked_by" }]), // circular
|
|
310
|
+
];
|
|
311
|
+
const result = checkDependencyIntegrity(tasks);
|
|
312
|
+
expect(result.valid).toBe(false);
|
|
313
|
+
expect(result.issues.selfDependencies.length).toBeGreaterThan(0);
|
|
314
|
+
expect(result.issues.invalidReferences.length).toBeGreaterThan(0);
|
|
315
|
+
expect(result.issues.circularDependencies.length).toBeGreaterThan(0);
|
|
316
|
+
expect(result.summary).toContain("self-dependencies");
|
|
317
|
+
expect(result.summary).toContain("invalid references");
|
|
318
|
+
expect(result.summary).toContain("circular dependency");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("handles empty task array", () => {
|
|
322
|
+
const result = checkDependencyIntegrity([]);
|
|
323
|
+
expect(result.valid).toBe(true);
|
|
324
|
+
expect(result.totalTasks).toBe(0);
|
|
325
|
+
expect(result.totalDependencies).toBe(0);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("handles tasks with no dependencies", () => {
|
|
329
|
+
const tasks = [createTask("A"), createTask("B"), createTask("C")];
|
|
330
|
+
const result = checkDependencyIntegrity(tasks);
|
|
331
|
+
expect(result.valid).toBe(true);
|
|
332
|
+
expect(result.totalTasks).toBe(3);
|
|
333
|
+
expect(result.totalDependencies).toBe(0);
|
|
334
|
+
});
|
|
335
|
+
});
|