@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,411 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import {
3
- MAX_HIERARCHY_DEPTH,
4
- getTaskLevel,
5
- validateHierarchyDepth,
6
- getAncestorIds,
7
- getDescendantIds,
8
- getChildTasks,
9
- getRootTask,
10
- buildTaskTree,
11
- } from "./hierarchy.js";
12
- import type { Task } from "../schemas/task.js";
13
-
14
- // Helper to create mock tasks
15
- function createTask(
16
- id: string,
17
- parentId?: string,
18
- overrides: Partial<Task> = {}
19
- ): Task {
20
- return {
21
- id,
22
- title: `Task ${id}`,
23
- status: "pending",
24
- priority: "medium",
25
- workspace: "test-workspace",
26
- createdAt: new Date().toISOString(),
27
- updatedAt: new Date().toISOString(),
28
- parentId,
29
- ...overrides,
30
- };
31
- }
32
-
33
- describe("MAX_HIERARCHY_DEPTH", () => {
34
- test("is set to 3", () => {
35
- expect(MAX_HIERARCHY_DEPTH).toBe(3);
36
- });
37
- });
38
-
39
- describe("getTaskLevel", () => {
40
- test("returns -1 for non-existent task", () => {
41
- const tasks = [createTask("A")];
42
- expect(getTaskLevel(tasks, "nonexistent")).toBe(-1);
43
- });
44
-
45
- test("returns 0 for root task (no parent)", () => {
46
- const tasks = [createTask("A")];
47
- expect(getTaskLevel(tasks, "A")).toBe(0);
48
- });
49
-
50
- test("returns 1 for direct child", () => {
51
- const tasks = [createTask("A"), createTask("B", "A")];
52
- expect(getTaskLevel(tasks, "B")).toBe(1);
53
- });
54
-
55
- test("returns 2 for grandchild", () => {
56
- const tasks = [createTask("A"), createTask("B", "A"), createTask("C", "B")];
57
- expect(getTaskLevel(tasks, "C")).toBe(2);
58
- });
59
-
60
- test("returns 3 for great-grandchild", () => {
61
- const tasks = [
62
- createTask("A"),
63
- createTask("B", "A"),
64
- createTask("C", "B"),
65
- createTask("D", "C"),
66
- ];
67
- expect(getTaskLevel(tasks, "D")).toBe(3);
68
- });
69
-
70
- test("handles missing parent gracefully", () => {
71
- // Task B references non-existent parent "missing"
72
- const tasks = [createTask("A"), createTask("B", "missing")];
73
- // Should return 0 since parent lookup fails immediately
74
- expect(getTaskLevel(tasks, "B")).toBe(0);
75
- });
76
-
77
- test("handles broken chain (middle parent missing)", () => {
78
- // C -> B -> A, but B is missing
79
- const tasks = [createTask("A"), createTask("C", "B")];
80
- // C's parent "B" doesn't exist, so level stays 0
81
- expect(getTaskLevel(tasks, "C")).toBe(0);
82
- });
83
- });
84
-
85
- describe("validateHierarchyDepth", () => {
86
- test("returns false for non-existent parent", () => {
87
- const tasks = [createTask("A")];
88
- expect(validateHierarchyDepth(tasks, "nonexistent")).toBe(false);
89
- });
90
-
91
- test("returns true when parent is at level 0", () => {
92
- const tasks = [createTask("A")];
93
- // Can add child at level 1
94
- expect(validateHierarchyDepth(tasks, "A")).toBe(true);
95
- });
96
-
97
- test("returns true when parent is at level 1", () => {
98
- const tasks = [createTask("A"), createTask("B", "A")];
99
- // Can add child at level 2
100
- expect(validateHierarchyDepth(tasks, "B")).toBe(true);
101
- });
102
-
103
- test("returns true when parent is at level 2", () => {
104
- const tasks = [createTask("A"), createTask("B", "A"), createTask("C", "B")];
105
- // Can add child at level 3 (MAX_HIERARCHY_DEPTH)
106
- expect(validateHierarchyDepth(tasks, "C")).toBe(true);
107
- });
108
-
109
- test("returns false when parent is at level 3 (max depth)", () => {
110
- const tasks = [
111
- createTask("A"),
112
- createTask("B", "A"),
113
- createTask("C", "B"),
114
- createTask("D", "C"),
115
- ];
116
- // D is at level 3, cannot add child at level 4
117
- expect(validateHierarchyDepth(tasks, "D")).toBe(false);
118
- });
119
- });
120
-
121
- describe("getAncestorIds", () => {
122
- test("returns empty array for non-existent task", () => {
123
- const tasks = [createTask("A")];
124
- expect(getAncestorIds(tasks, "nonexistent")).toEqual([]);
125
- });
126
-
127
- test("returns empty array for root task", () => {
128
- const tasks = [createTask("A")];
129
- expect(getAncestorIds(tasks, "A")).toEqual([]);
130
- });
131
-
132
- test("returns single ancestor for direct child", () => {
133
- const tasks = [createTask("A"), createTask("B", "A")];
134
- expect(getAncestorIds(tasks, "B")).toEqual(["A"]);
135
- });
136
-
137
- test("returns ancestors ordered from immediate parent to root", () => {
138
- const tasks = [
139
- createTask("A"),
140
- createTask("B", "A"),
141
- createTask("C", "B"),
142
- createTask("D", "C"),
143
- ];
144
- expect(getAncestorIds(tasks, "D")).toEqual(["C", "B", "A"]);
145
- });
146
-
147
- test("handles missing ancestor in chain", () => {
148
- // C -> B -> missing
149
- const tasks = [createTask("B", "missing"), createTask("C", "B")];
150
- // Returns B and "missing" (the parentId), then stops when "missing" lookup fails
151
- expect(getAncestorIds(tasks, "C")).toEqual(["B", "missing"]);
152
- });
153
- });
154
-
155
- describe("getDescendantIds", () => {
156
- test("returns empty array for task with no children", () => {
157
- const tasks = [createTask("A"), createTask("B")];
158
- expect(getDescendantIds(tasks, "A")).toEqual([]);
159
- });
160
-
161
- test("returns direct children", () => {
162
- const tasks = [createTask("A"), createTask("B", "A"), createTask("C", "A")];
163
- const descendants = getDescendantIds(tasks, "A");
164
- expect(descendants.sort()).toEqual(["B", "C"]);
165
- });
166
-
167
- test("returns all descendants (children and grandchildren)", () => {
168
- const tasks = [
169
- createTask("A"),
170
- createTask("B", "A"),
171
- createTask("C", "A"),
172
- createTask("D", "B"),
173
- createTask("E", "B"),
174
- ];
175
- const descendants = getDescendantIds(tasks, "A");
176
- expect(descendants.sort()).toEqual(["B", "C", "D", "E"]);
177
- });
178
-
179
- test("returns descendants in BFS order", () => {
180
- const tasks = [
181
- createTask("A"),
182
- createTask("B", "A"),
183
- createTask("C", "A"),
184
- createTask("D", "B"),
185
- ];
186
- const descendants = getDescendantIds(tasks, "A");
187
- // BFS: first level (B, C), then second level (D)
188
- // B and C come before D
189
- const dIndex = descendants.indexOf("D");
190
- const bIndex = descendants.indexOf("B");
191
- const cIndex = descendants.indexOf("C");
192
- expect(bIndex).toBeLessThan(dIndex);
193
- expect(cIndex).toBeLessThan(dIndex);
194
- });
195
-
196
- test("returns empty array for non-existent task", () => {
197
- const tasks = [createTask("A")];
198
- expect(getDescendantIds(tasks, "nonexistent")).toEqual([]);
199
- });
200
- });
201
-
202
- describe("getChildTasks", () => {
203
- test("returns empty array for task with no children", () => {
204
- const tasks = [createTask("A"), createTask("B")];
205
- expect(getChildTasks(tasks, "A")).toEqual([]);
206
- });
207
-
208
- test("returns only direct children", () => {
209
- const tasks = [
210
- createTask("A"),
211
- createTask("B", "A"),
212
- createTask("C", "A"),
213
- createTask("D", "B"), // grandchild, should not be included
214
- ];
215
- const children = getChildTasks(tasks, "A");
216
- expect(children.map((t) => t.id).sort()).toEqual(["B", "C"]);
217
- });
218
-
219
- test("returns empty array for non-existent parent", () => {
220
- const tasks = [createTask("A")];
221
- expect(getChildTasks(tasks, "nonexistent")).toEqual([]);
222
- });
223
-
224
- test("returns full task objects", () => {
225
- const tasks = [createTask("A"), createTask("B", "A", { title: "Custom Title" })];
226
- const children = getChildTasks(tasks, "A");
227
- expect(children.length).toBe(1);
228
- expect(children[0]!.id).toBe("B");
229
- expect(children[0]!.title).toBe("Custom Title");
230
- });
231
- });
232
-
233
- describe("getRootTask", () => {
234
- test("returns null for non-existent task", () => {
235
- const tasks = [createTask("A")];
236
- expect(getRootTask(tasks, "nonexistent")).toBeNull();
237
- });
238
-
239
- test("returns the task itself if it has no parent", () => {
240
- const tasks = [createTask("A")];
241
- const root = getRootTask(tasks, "A");
242
- expect(root?.id).toBe("A");
243
- });
244
-
245
- test("returns root for direct child", () => {
246
- const tasks = [createTask("A"), createTask("B", "A")];
247
- const root = getRootTask(tasks, "B");
248
- expect(root?.id).toBe("A");
249
- });
250
-
251
- test("returns root for deeply nested task", () => {
252
- const tasks = [
253
- createTask("A"),
254
- createTask("B", "A"),
255
- createTask("C", "B"),
256
- createTask("D", "C"),
257
- ];
258
- const root = getRootTask(tasks, "D");
259
- expect(root?.id).toBe("A");
260
- });
261
-
262
- test("handles broken chain (returns earliest found task)", () => {
263
- // C -> B -> missing
264
- const tasks = [createTask("B", "missing"), createTask("C", "B")];
265
- const root = getRootTask(tasks, "C");
266
- // B's parent "missing" doesn't exist, so B is the root
267
- expect(root?.id).toBe("B");
268
- });
269
- });
270
-
271
- describe("buildTaskTree", () => {
272
- test("returns empty array for empty task list", () => {
273
- expect(buildTaskTree([])).toEqual([]);
274
- });
275
-
276
- test("returns single root task as tree", () => {
277
- const tasks = [createTask("A")];
278
- const tree = buildTaskTree(tasks);
279
- expect(tree.length).toBe(1);
280
- expect(tree[0]!.task.id).toBe("A");
281
- expect(tree[0]!.children).toEqual([]);
282
- });
283
-
284
- test("builds tree with children", () => {
285
- const tasks = [createTask("A"), createTask("B", "A"), createTask("C", "A")];
286
- const tree = buildTaskTree(tasks);
287
- expect(tree.length).toBe(1);
288
- expect(tree[0]!.task.id).toBe("A");
289
- expect(tree[0]!.children.length).toBe(2);
290
- expect(tree[0]!.children.map((c) => c.task.id).sort()).toEqual(["B", "C"]);
291
- });
292
-
293
- test("builds nested tree structure", () => {
294
- const tasks = [
295
- createTask("A"),
296
- createTask("B", "A"),
297
- createTask("C", "B"),
298
- createTask("D", "C"),
299
- ];
300
- const tree = buildTaskTree(tasks);
301
- expect(tree.length).toBe(1);
302
- expect(tree[0]!.task.id).toBe("A");
303
- expect(tree[0]!.children.length).toBe(1);
304
- expect(tree[0]!.children[0]!.task.id).toBe("B");
305
- expect(tree[0]!.children[0]!.children.length).toBe(1);
306
- expect(tree[0]!.children[0]!.children[0]!.task.id).toBe("C");
307
- expect(tree[0]!.children[0]!.children[0]!.children.length).toBe(1);
308
- expect(tree[0]!.children[0]!.children[0]!.children[0]!.task.id).toBe("D");
309
- });
310
-
311
- test("returns multiple root tasks", () => {
312
- const tasks = [createTask("A"), createTask("B"), createTask("C")];
313
- const tree = buildTaskTree(tasks);
314
- expect(tree.length).toBe(3);
315
- expect(tree.map((n) => n.task.id).sort()).toEqual(["A", "B", "C"]);
316
- });
317
-
318
- test("builds tree from specific root", () => {
319
- const tasks = [
320
- createTask("A"),
321
- createTask("B", "A"),
322
- createTask("C", "B"),
323
- createTask("X"),
324
- createTask("Y", "X"),
325
- ];
326
- const tree = buildTaskTree(tasks, "A");
327
- expect(tree.length).toBe(1);
328
- expect(tree[0]!.task.id).toBe("A");
329
- expect(tree[0]!.children.length).toBe(1);
330
- expect(tree[0]!.children[0]!.task.id).toBe("B");
331
- });
332
-
333
- test("returns empty array for non-existent root", () => {
334
- const tasks = [createTask("A")];
335
- const tree = buildTaskTree(tasks, "nonexistent");
336
- expect(tree).toEqual([]);
337
- });
338
-
339
- test("can build subtree from middle of hierarchy", () => {
340
- const tasks = [
341
- createTask("A"),
342
- createTask("B", "A"),
343
- createTask("C", "B"),
344
- createTask("D", "B"),
345
- ];
346
- const tree = buildTaskTree(tasks, "B");
347
- expect(tree.length).toBe(1);
348
- expect(tree[0]!.task.id).toBe("B");
349
- expect(tree[0]!.children.length).toBe(2);
350
- expect(tree[0]!.children.map((c) => c.task.id).sort()).toEqual(["C", "D"]);
351
- });
352
-
353
- test("respects maxDepth parameter", () => {
354
- const tasks = [
355
- createTask("A"),
356
- createTask("B", "A"),
357
- createTask("C", "B"),
358
- createTask("D", "C"),
359
- ];
360
- // maxDepth of 2 should include A (depth 0), B (depth 1), but stop children at C
361
- const tree = buildTaskTree(tasks, "A", 2);
362
- expect(tree.length).toBe(1);
363
- expect(tree[0]!.task.id).toBe("A");
364
- expect(tree[0]!.children.length).toBe(1);
365
- expect(tree[0]!.children[0]!.task.id).toBe("B");
366
- // C should be included but with no children (depth 2 is the limit)
367
- expect(tree[0]!.children[0]!.children.length).toBe(1);
368
- expect(tree[0]!.children[0]!.children[0]!.task.id).toBe("C");
369
- expect(tree[0]!.children[0]!.children[0]!.children).toEqual([]);
370
- });
371
-
372
- test("handles circular references gracefully", () => {
373
- // Create tasks with circular reference: A -> B -> C -> A
374
- const taskA = createTask("A", "C");
375
- const taskB = createTask("B", "A");
376
- const taskC = createTask("C", "B");
377
- const tasks = [taskA, taskB, taskC];
378
-
379
- // Should not throw or hang - circular ref is detected
380
- const tree = buildTaskTree(tasks);
381
- // All tasks have parents, so no "root" tasks exist
382
- expect(tree).toEqual([]);
383
- });
384
-
385
- test("handles self-referencing task", () => {
386
- // Task references itself as parent
387
- const selfRef = createTask("A", "A");
388
- const tasks = [selfRef];
389
-
390
- // No root tasks (A has parent), should return empty
391
- const tree = buildTaskTree(tasks);
392
- expect(tree).toEqual([]);
393
- });
394
-
395
- test("handles circular reference when building from specific root", () => {
396
- // A -> B -> A (circular)
397
- const taskA = createTask("A");
398
- const taskB = createTask("B", "A");
399
- // Manually create circular: make A's children include B which points back
400
- // by creating a scenario where childrenMap would loop
401
- const tasks = [taskA, taskB];
402
- // Simulate circular by modifying childrenMap behavior won't work directly,
403
- // but we can test that visited set prevents infinite recursion
404
- // Let's create a more direct test case
405
- const tree = buildTaskTree(tasks, "A");
406
- expect(tree.length).toBe(1);
407
- expect(tree[0]!.task.id).toBe("A");
408
- expect(tree[0]!.children.length).toBe(1);
409
- expect(tree[0]!.children[0]!.task.id).toBe("B");
410
- });
411
- });
@@ -1,235 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import {
3
- generateId,
4
- generateTaskId,
5
- generateInboxId,
6
- isValidTaskId,
7
- isValidInboxId,
8
- validateTaskId,
9
- validateInboxId,
10
- assertValidTaskId,
11
- assertValidInboxId,
12
- InvalidIdError,
13
- } from "./id.js";
14
-
15
- describe("ID Generation", () => {
16
- describe("generateId", () => {
17
- test("generates ID without prefix", () => {
18
- const id = generateId();
19
- expect(id.length).toBe(12);
20
- expect(/^[a-z0-9]+$/.test(id)).toBe(true);
21
- });
22
-
23
- test("generates ID with prefix", () => {
24
- const id = generateId("test");
25
- expect(id).toMatch(/^test_[a-z0-9]+$/);
26
- });
27
-
28
- test("generates unique IDs", () => {
29
- const ids = new Set(Array.from({ length: 100 }, () => generateId("test")));
30
- expect(ids.size).toBe(100);
31
- });
32
- });
33
-
34
- describe("generateTaskId", () => {
35
- test("generates task ID with correct prefix", () => {
36
- const id = generateTaskId();
37
- expect(id).toMatch(/^task_[a-z0-9]+$/);
38
- });
39
- });
40
-
41
- describe("generateInboxId", () => {
42
- test("generates inbox ID with correct prefix", () => {
43
- const id = generateInboxId();
44
- expect(id).toMatch(/^inbox_[a-z0-9]+$/);
45
- });
46
- });
47
- });
48
-
49
- describe("Simple ID Validation (boolean)", () => {
50
- describe("isValidTaskId", () => {
51
- test("returns true for valid task IDs", () => {
52
- expect(isValidTaskId("task_abc123")).toBe(true);
53
- expect(isValidTaskId("task_a")).toBe(true);
54
- expect(isValidTaskId("task_123")).toBe(true);
55
- expect(isValidTaskId("task_abc123def456")).toBe(true);
56
- });
57
-
58
- test("returns false for invalid task IDs", () => {
59
- expect(isValidTaskId("")).toBe(false);
60
- expect(isValidTaskId("task_")).toBe(false);
61
- expect(isValidTaskId("abc123")).toBe(false);
62
- expect(isValidTaskId("proj_abc123")).toBe(false);
63
- expect(isValidTaskId("task_ABC123")).toBe(false);
64
- expect(isValidTaskId("task_abc-123")).toBe(false);
65
- expect(isValidTaskId("task_abc.123")).toBe(false);
66
- expect(isValidTaskId("task_abc/123")).toBe(false);
67
- expect(isValidTaskId("task_abc 123")).toBe(false);
68
- });
69
- });
70
-
71
- describe("isValidInboxId", () => {
72
- test("returns true for valid inbox IDs", () => {
73
- expect(isValidInboxId("inbox_abc123")).toBe(true);
74
- expect(isValidInboxId("inbox_a")).toBe(true);
75
- });
76
-
77
- test("returns false for invalid inbox IDs", () => {
78
- expect(isValidInboxId("")).toBe(false);
79
- expect(isValidInboxId("in_abc123")).toBe(false);
80
- expect(isValidInboxId("task_abc123")).toBe(false);
81
- });
82
- });
83
- });
84
-
85
- describe("Detailed ID Validation", () => {
86
- describe("validateTaskId", () => {
87
- test("returns valid: true for valid task IDs", () => {
88
- expect(validateTaskId("task_abc123")).toEqual({ valid: true });
89
- expect(validateTaskId("task_a1b2c3")).toEqual({ valid: true });
90
- });
91
-
92
- test("returns error for null/undefined", () => {
93
- const result = validateTaskId(null);
94
- expect(result.valid).toBe(false);
95
- expect(result.reason).toContain("required");
96
-
97
- const result2 = validateTaskId(undefined);
98
- expect(result2.valid).toBe(false);
99
- });
100
-
101
- test("returns error for non-string", () => {
102
- const result = validateTaskId(123);
103
- expect(result.valid).toBe(false);
104
- expect(result.reason).toContain("string");
105
- expect(result.reason).toContain("number");
106
- });
107
-
108
- test("returns error for empty string", () => {
109
- const result = validateTaskId("");
110
- expect(result.valid).toBe(false);
111
- expect(result.reason).toContain("empty");
112
- });
113
-
114
- test("returns error for missing prefix", () => {
115
- const result = validateTaskId("abc123");
116
- expect(result.valid).toBe(false);
117
- expect(result.reason).toContain("task_");
118
- });
119
-
120
- test("returns error for wrong prefix", () => {
121
- const result = validateTaskId("proj_abc123");
122
- expect(result.valid).toBe(false);
123
- expect(result.reason).toContain("task_");
124
- });
125
-
126
- test("returns error for invalid characters", () => {
127
- const result = validateTaskId("task_abc-123");
128
- expect(result.valid).toBe(false);
129
- expect(result.reason).toContain("invalid characters");
130
- });
131
- });
132
-
133
- describe("validateInboxId", () => {
134
- test("returns valid: true for valid inbox IDs", () => {
135
- expect(validateInboxId("inbox_abc123")).toEqual({ valid: true });
136
- });
137
-
138
- test("returns error for missing prefix", () => {
139
- const result = validateInboxId("in_abc");
140
- expect(result.valid).toBe(false);
141
- expect(result.reason).toContain("inbox_");
142
- });
143
- });
144
- });
145
-
146
- describe("Assert Functions", () => {
147
- describe("assertValidTaskId", () => {
148
- test("does not throw for valid ID", () => {
149
- expect(() => assertValidTaskId("task_abc123")).not.toThrow();
150
- });
151
-
152
- test("throws InvalidIdError for invalid ID", () => {
153
- expect(() => assertValidTaskId("")).toThrow(InvalidIdError);
154
- expect(() => assertValidTaskId("abc123")).toThrow(InvalidIdError);
155
- expect(() => assertValidTaskId(null)).toThrow(InvalidIdError);
156
- });
157
-
158
- test("InvalidIdError contains correct properties", () => {
159
- try {
160
- assertValidTaskId("invalid");
161
- } catch (error) {
162
- expect(error).toBeInstanceOf(InvalidIdError);
163
- if (error instanceof InvalidIdError) {
164
- expect(error.idType).toBe("task");
165
- expect(error.invalidValue).toBe("invalid");
166
- expect(error.reason).toBeDefined();
167
- expect(error.message).toContain("task");
168
- }
169
- }
170
- });
171
- });
172
-
173
- describe("assertValidInboxId", () => {
174
- test("does not throw for valid ID", () => {
175
- expect(() => assertValidInboxId("inbox_abc123")).not.toThrow();
176
- });
177
-
178
- test("throws InvalidIdError for invalid ID", () => {
179
- expect(() => assertValidInboxId("in_abc")).toThrow(InvalidIdError);
180
- });
181
- });
182
- });
183
-
184
- describe("InvalidIdError", () => {
185
- test("extends Error", () => {
186
- const error = new InvalidIdError("task", "bad_id", "test reason");
187
- expect(error).toBeInstanceOf(Error);
188
- expect(error).toBeInstanceOf(InvalidIdError);
189
- });
190
-
191
- test("has correct name", () => {
192
- const error = new InvalidIdError("task", "bad_id", "test reason");
193
- expect(error.name).toBe("InvalidIdError");
194
- });
195
-
196
- test("has correct message format", () => {
197
- const error = new InvalidIdError("task", "bad_id", "test reason");
198
- expect(error.message).toBe("Invalid task ID: test reason");
199
- });
200
-
201
- test("exposes idType, invalidValue, and reason", () => {
202
- const error = new InvalidIdError("task", "wrong", "missing prefix");
203
- expect(error.idType).toBe("task");
204
- expect(error.invalidValue).toBe("wrong");
205
- expect(error.reason).toBe("missing prefix");
206
- });
207
- });
208
-
209
- describe("Security: ID Injection Prevention", () => {
210
- test("rejects path traversal attempts", () => {
211
- expect(validateTaskId("task_../etc/passwd").valid).toBe(false);
212
- expect(validateTaskId("task_..%2F..%2Fetc").valid).toBe(false);
213
- });
214
-
215
- test("rejects null bytes", () => {
216
- expect(validateTaskId("task_abc\0def").valid).toBe(false);
217
- });
218
-
219
- test("rejects SQL injection attempts", () => {
220
- expect(validateTaskId("task_'; DROP TABLE tasks;--").valid).toBe(false);
221
- expect(validateTaskId("task_1 OR 1=1").valid).toBe(false);
222
- });
223
-
224
- test("rejects script injection attempts", () => {
225
- expect(validateTaskId("task_<script>alert(1)</script>").valid).toBe(false);
226
- });
227
-
228
- test("only allows safe alphanumeric characters", () => {
229
- // The regex ^task_[a-z0-9]+$ only allows lowercase letters and numbers
230
- expect(validateTaskId("task_abc123").valid).toBe(true);
231
- expect(validateTaskId("task_UPPERCASE").valid).toBe(false);
232
- expect(validateTaskId("task_with_underscore").valid).toBe(false);
233
- expect(validateTaskId("task_with-dash").valid).toBe(false);
234
- });
235
- });