@task-mcp/shared 1.0.3 → 1.0.6
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 +50 -26
- package/dist/algorithms/critical-path.js.map +1 -1
- package/dist/algorithms/dependency-integrity.d.ts +73 -0
- package/dist/algorithms/dependency-integrity.d.ts.map +1 -0
- package/dist/algorithms/dependency-integrity.js +189 -0
- package/dist/algorithms/dependency-integrity.js.map +1 -0
- package/dist/algorithms/index.d.ts +2 -0
- package/dist/algorithms/index.d.ts.map +1 -1
- package/dist/algorithms/index.js +2 -0
- package/dist/algorithms/index.js.map +1 -1
- 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 +296 -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 +338 -0
- package/dist/algorithms/tech-analysis.test.js.map +1 -0
- package/dist/algorithms/topological-sort.d.ts.map +1 -1
- package/dist/algorithms/topological-sort.js +60 -8
- package/dist/algorithms/topological-sort.js.map +1 -1
- package/dist/schemas/inbox.d.ts +24 -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 +3 -1
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +9 -1
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/response-format.d.ts +79 -0
- package/dist/schemas/response-format.d.ts.map +1 -0
- package/dist/schemas/response-format.js +17 -0
- package/dist/schemas/response-format.js.map +1 -0
- package/dist/schemas/task.d.ts +57 -0
- package/dist/schemas/task.d.ts.map +1 -1
- package/dist/schemas/task.js +34 -0
- package/dist/schemas/task.js.map +1 -1
- package/dist/utils/date.d.ts.map +1 -1
- package/dist/utils/date.js +17 -2
- package/dist/utils/date.js.map +1 -1
- package/dist/utils/hierarchy.d.ts +75 -0
- package/dist/utils/hierarchy.d.ts.map +1 -0
- package/dist/utils/hierarchy.js +179 -0
- package/dist/utils/hierarchy.js.map +1 -0
- package/dist/utils/id.d.ts +51 -1
- package/dist/utils/id.d.ts.map +1 -1
- package/dist/utils/id.js +124 -4
- package/dist/utils/id.js.map +1 -1
- 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 +228 -0
- package/dist/utils/id.test.js.map +1 -0
- package/dist/utils/index.d.ts +4 -2
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +7 -2
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/natural-language.d.ts +45 -0
- package/dist/utils/natural-language.d.ts.map +1 -1
- package/dist/utils/natural-language.js +86 -0
- package/dist/utils/natural-language.js.map +1 -1
- package/dist/utils/projection.d.ts +65 -0
- package/dist/utils/projection.d.ts.map +1 -0
- package/dist/utils/projection.js +181 -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 +400 -0
- package/dist/utils/projection.test.js.map +1 -0
- package/package.json +1 -1
- package/src/algorithms/critical-path.ts +56 -24
- package/src/algorithms/dependency-integrity.ts +270 -0
- package/src/algorithms/index.ts +28 -0
- package/src/algorithms/tech-analysis.test.ts +413 -0
- package/src/algorithms/tech-analysis.ts +412 -0
- package/src/algorithms/topological-sort.ts +66 -9
- package/src/schemas/inbox.ts +32 -0
- package/src/schemas/index.ts +31 -0
- package/src/schemas/response-format.ts +108 -0
- package/src/schemas/task.ts +50 -0
- package/src/utils/date.ts +18 -2
- package/src/utils/hierarchy.ts +224 -0
- package/src/utils/id.test.ts +281 -0
- package/src/utils/id.ts +139 -4
- package/src/utils/index.ts +46 -2
- package/src/utils/natural-language.ts +113 -0
- package/src/utils/projection.test.ts +505 -0
- package/src/utils/projection.ts +251 -0
package/src/schemas/task.ts
CHANGED
|
@@ -42,6 +42,43 @@ export const Recurrence = type({
|
|
|
42
42
|
});
|
|
43
43
|
export type Recurrence = typeof Recurrence.infer;
|
|
44
44
|
|
|
45
|
+
// Complexity factors that contribute to task difficulty
|
|
46
|
+
export const ComplexityFactor = type(
|
|
47
|
+
"'cross_cutting' | 'state_management' | 'error_handling' | 'performance' | 'security' | 'external_dependency' | 'data_migration' | 'breaking_change' | 'unclear_requirements' | 'coordination'"
|
|
48
|
+
);
|
|
49
|
+
export type ComplexityFactor = typeof ComplexityFactor.infer;
|
|
50
|
+
|
|
51
|
+
// Complexity analysis result (populated by Claude)
|
|
52
|
+
export const ComplexityAnalysis = type({
|
|
53
|
+
"score?": "number", // 1-10 complexity score
|
|
54
|
+
"factors?": ComplexityFactor.array(),
|
|
55
|
+
"suggestedSubtasks?": "number", // 0-10 recommended subtask count
|
|
56
|
+
"rationale?": "string",
|
|
57
|
+
"analyzedAt?": "string",
|
|
58
|
+
});
|
|
59
|
+
export type ComplexityAnalysis = typeof ComplexityAnalysis.infer;
|
|
60
|
+
|
|
61
|
+
// Tech area categories for ordering
|
|
62
|
+
export const TechArea = type(
|
|
63
|
+
"'schema' | 'backend' | 'frontend' | 'infra' | 'devops' | 'test' | 'docs' | 'refactor'"
|
|
64
|
+
);
|
|
65
|
+
export type TechArea = typeof TechArea.infer;
|
|
66
|
+
|
|
67
|
+
// Risk level for changes
|
|
68
|
+
export const RiskLevel = type("'low' | 'medium' | 'high' | 'critical'");
|
|
69
|
+
export type RiskLevel = typeof RiskLevel.infer;
|
|
70
|
+
|
|
71
|
+
// Tech stack analysis result (populated by Claude)
|
|
72
|
+
export const TechStackAnalysis = type({
|
|
73
|
+
"areas?": TechArea.array(),
|
|
74
|
+
"hasBreakingChange?": "boolean",
|
|
75
|
+
"riskLevel?": RiskLevel,
|
|
76
|
+
"affectedComponents?": "string[]",
|
|
77
|
+
"rationale?": "string",
|
|
78
|
+
"analyzedAt?": "string",
|
|
79
|
+
});
|
|
80
|
+
export type TechStackAnalysis = typeof TechStackAnalysis.infer;
|
|
81
|
+
|
|
45
82
|
// Core Task schema
|
|
46
83
|
export const Task = type({
|
|
47
84
|
id: "string",
|
|
@@ -51,6 +88,10 @@ export const Task = type({
|
|
|
51
88
|
priority: Priority,
|
|
52
89
|
projectId: "string",
|
|
53
90
|
|
|
91
|
+
// Hierarchy (subtask support)
|
|
92
|
+
"parentId?": "string", // Parent task ID for subtasks
|
|
93
|
+
"level?": "number", // Hierarchy depth (0=root, 1=subtask, 2=sub-subtask, max 3)
|
|
94
|
+
|
|
54
95
|
// Dependencies
|
|
55
96
|
"dependencies?": Dependency.array(),
|
|
56
97
|
|
|
@@ -78,6 +119,10 @@ export const Task = type({
|
|
|
78
119
|
"slack?": "number", // Minutes of slack time
|
|
79
120
|
"earliestStart?": "number", // Minutes from project start
|
|
80
121
|
"latestStart?": "number",
|
|
122
|
+
|
|
123
|
+
// Analysis fields (populated by Claude)
|
|
124
|
+
"complexity?": ComplexityAnalysis,
|
|
125
|
+
"techStack?": TechStackAnalysis,
|
|
81
126
|
});
|
|
82
127
|
export type Task = typeof Task.infer;
|
|
83
128
|
|
|
@@ -87,6 +132,7 @@ export const TaskCreateInput = type({
|
|
|
87
132
|
"description?": "string",
|
|
88
133
|
"projectId?": "string",
|
|
89
134
|
"priority?": Priority,
|
|
135
|
+
"parentId?": "string", // Parent task ID for creating subtasks
|
|
90
136
|
"dependencies?": Dependency.array(),
|
|
91
137
|
"estimate?": TimeEstimate,
|
|
92
138
|
"dueDate?": "string",
|
|
@@ -104,6 +150,7 @@ export const TaskUpdateInput = type({
|
|
|
104
150
|
"status?": TaskStatus,
|
|
105
151
|
"priority?": Priority,
|
|
106
152
|
"projectId?": "string",
|
|
153
|
+
"parentId?": "string", // Parent task ID for moving task in hierarchy
|
|
107
154
|
"dependencies?": Dependency.array(),
|
|
108
155
|
"estimate?": TimeEstimate,
|
|
109
156
|
"actualMinutes?": "number",
|
|
@@ -112,5 +159,8 @@ export const TaskUpdateInput = type({
|
|
|
112
159
|
"contexts?": "string[]",
|
|
113
160
|
"tags?": "string[]",
|
|
114
161
|
"recurrence?": Recurrence,
|
|
162
|
+
// Analysis fields
|
|
163
|
+
"complexity?": ComplexityAnalysis,
|
|
164
|
+
"techStack?": TechStackAnalysis,
|
|
115
165
|
});
|
|
116
166
|
export type TaskUpdateInput = typeof TaskUpdateInput.infer;
|
package/src/utils/date.ts
CHANGED
|
@@ -67,10 +67,26 @@ export function parseRelativeDate(input: string): Date | null {
|
|
|
67
67
|
return d;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// Try parsing as
|
|
70
|
+
// Try parsing as YYYY-MM-DD format (local timezone, no UTC shift)
|
|
71
|
+
const isoDateMatch = input.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
72
|
+
if (isoDateMatch) {
|
|
73
|
+
const [, yearStr, monthStr, dayStr] = isoDateMatch;
|
|
74
|
+
const year = parseInt(yearStr!, 10);
|
|
75
|
+
const month = parseInt(monthStr!, 10) - 1; // 0-indexed
|
|
76
|
+
const day = parseInt(dayStr!, 10);
|
|
77
|
+
const d = new Date(year, month, day);
|
|
78
|
+
// Validate the date is valid (e.g., not Feb 30)
|
|
79
|
+
if (d.getFullYear() === year && d.getMonth() === month && d.getDate() === day) {
|
|
80
|
+
return d;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Try parsing other date formats (fallback)
|
|
71
85
|
const parsed = new Date(input);
|
|
72
86
|
if (!isNaN(parsed.getTime())) {
|
|
73
|
-
|
|
87
|
+
// For non-YYYY-MM-DD formats, normalize to local midnight
|
|
88
|
+
const d = new Date(parsed.getFullYear(), parsed.getMonth(), parsed.getDate());
|
|
89
|
+
return d;
|
|
74
90
|
}
|
|
75
91
|
|
|
76
92
|
return null;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { Task } from "../schemas/task.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Maximum allowed hierarchy depth (0-indexed)
|
|
5
|
+
* 0 = root task
|
|
6
|
+
* 1 = subtask
|
|
7
|
+
* 2 = sub-subtask
|
|
8
|
+
* 3 = sub-sub-subtask (maximum)
|
|
9
|
+
*/
|
|
10
|
+
export const MAX_HIERARCHY_DEPTH = 3;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Calculates the hierarchy level of a task.
|
|
14
|
+
* Level 0 = root task (no parent)
|
|
15
|
+
* Level 1 = direct subtask
|
|
16
|
+
* Level 2 = sub-subtask
|
|
17
|
+
* etc.
|
|
18
|
+
*
|
|
19
|
+
* @param tasks - Array of all tasks
|
|
20
|
+
* @param taskId - ID of the task to get level for
|
|
21
|
+
* @returns The hierarchy level (0 for root), or -1 if task not found
|
|
22
|
+
*/
|
|
23
|
+
export function getTaskLevel(tasks: Task[], taskId: string): number {
|
|
24
|
+
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
25
|
+
const task = taskMap.get(taskId);
|
|
26
|
+
|
|
27
|
+
if (!task) {
|
|
28
|
+
return -1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let level = 0;
|
|
32
|
+
let currentTask = task;
|
|
33
|
+
|
|
34
|
+
while (currentTask.parentId) {
|
|
35
|
+
const parent = taskMap.get(currentTask.parentId);
|
|
36
|
+
if (!parent) {
|
|
37
|
+
break; // Parent not found, stop traversal
|
|
38
|
+
}
|
|
39
|
+
level++;
|
|
40
|
+
currentTask = parent;
|
|
41
|
+
|
|
42
|
+
// Safety check to prevent infinite loops
|
|
43
|
+
if (level > MAX_HIERARCHY_DEPTH + 1) {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return level;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validates whether adding a child task under the given parent
|
|
53
|
+
* would exceed the maximum hierarchy depth.
|
|
54
|
+
*
|
|
55
|
+
* @param tasks - Array of all tasks
|
|
56
|
+
* @param parentId - ID of the proposed parent task
|
|
57
|
+
* @returns true if a child can be added, false if it would exceed max depth
|
|
58
|
+
*/
|
|
59
|
+
export function validateHierarchyDepth(
|
|
60
|
+
tasks: Task[],
|
|
61
|
+
parentId: string
|
|
62
|
+
): boolean {
|
|
63
|
+
const parentLevel = getTaskLevel(tasks, parentId);
|
|
64
|
+
|
|
65
|
+
if (parentLevel === -1) {
|
|
66
|
+
return false; // Parent task not found
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Child would be at parentLevel + 1
|
|
70
|
+
// If parent is at level 3, child would be at level 4 which exceeds max
|
|
71
|
+
return parentLevel < MAX_HIERARCHY_DEPTH;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Gets all ancestor task IDs for a given task (from immediate parent to root).
|
|
76
|
+
*
|
|
77
|
+
* @param tasks - Array of all tasks
|
|
78
|
+
* @param taskId - ID of the task
|
|
79
|
+
* @returns Array of ancestor task IDs, ordered from immediate parent to root
|
|
80
|
+
*/
|
|
81
|
+
export function getAncestorIds(tasks: Task[], taskId: string): string[] {
|
|
82
|
+
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
83
|
+
const task = taskMap.get(taskId);
|
|
84
|
+
|
|
85
|
+
if (!task) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const ancestors: string[] = [];
|
|
90
|
+
let currentTask = task;
|
|
91
|
+
|
|
92
|
+
while (currentTask.parentId) {
|
|
93
|
+
ancestors.push(currentTask.parentId);
|
|
94
|
+
const parent = taskMap.get(currentTask.parentId);
|
|
95
|
+
if (!parent) {
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
currentTask = parent;
|
|
99
|
+
|
|
100
|
+
// Safety check
|
|
101
|
+
if (ancestors.length > MAX_HIERARCHY_DEPTH + 1) {
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return ancestors;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Gets all descendant task IDs for a given task (children, grandchildren, etc.).
|
|
111
|
+
*
|
|
112
|
+
* @param tasks - Array of all tasks
|
|
113
|
+
* @param taskId - ID of the parent task
|
|
114
|
+
* @returns Array of descendant task IDs
|
|
115
|
+
*/
|
|
116
|
+
export function getDescendantIds(tasks: Task[], taskId: string): string[] {
|
|
117
|
+
const descendants: string[] = [];
|
|
118
|
+
const childrenMap = new Map<string, Task[]>();
|
|
119
|
+
|
|
120
|
+
// Build parent -> children map
|
|
121
|
+
for (const task of tasks) {
|
|
122
|
+
if (task.parentId) {
|
|
123
|
+
const children = childrenMap.get(task.parentId) || [];
|
|
124
|
+
children.push(task);
|
|
125
|
+
childrenMap.set(task.parentId, children);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// BFS to collect all descendants
|
|
130
|
+
const queue = [taskId];
|
|
131
|
+
while (queue.length > 0) {
|
|
132
|
+
const currentId = queue.shift()!;
|
|
133
|
+
const children = childrenMap.get(currentId) || [];
|
|
134
|
+
for (const child of children) {
|
|
135
|
+
descendants.push(child.id);
|
|
136
|
+
queue.push(child.id);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return descendants;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Gets direct children of a task.
|
|
145
|
+
*
|
|
146
|
+
* @param tasks - Array of all tasks
|
|
147
|
+
* @param taskId - ID of the parent task
|
|
148
|
+
* @returns Array of direct child tasks
|
|
149
|
+
*/
|
|
150
|
+
export function getChildTasks(tasks: Task[], taskId: string): Task[] {
|
|
151
|
+
return tasks.filter((t) => t.parentId === taskId);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Gets the root task of a hierarchy.
|
|
156
|
+
*
|
|
157
|
+
* @param tasks - Array of all tasks
|
|
158
|
+
* @param taskId - ID of any task in the hierarchy
|
|
159
|
+
* @returns The root task, or null if not found
|
|
160
|
+
*/
|
|
161
|
+
export function getRootTask(tasks: Task[], taskId: string): Task | null {
|
|
162
|
+
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
163
|
+
let currentTask = taskMap.get(taskId);
|
|
164
|
+
|
|
165
|
+
if (!currentTask) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
while (currentTask.parentId) {
|
|
170
|
+
const parent = taskMap.get(currentTask.parentId);
|
|
171
|
+
if (!parent) {
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
currentTask = parent;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return currentTask;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Builds a tree structure from flat task list.
|
|
182
|
+
*
|
|
183
|
+
* @param tasks - Array of all tasks
|
|
184
|
+
* @param rootId - Optional root task ID (if not provided, returns all root tasks)
|
|
185
|
+
* @returns Tree structure with children arrays
|
|
186
|
+
*/
|
|
187
|
+
export interface TaskTreeNode {
|
|
188
|
+
task: Task;
|
|
189
|
+
children: TaskTreeNode[];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function buildTaskTree(tasks: Task[], rootId?: string): TaskTreeNode[] {
|
|
193
|
+
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
194
|
+
const childrenMap = new Map<string, Task[]>();
|
|
195
|
+
|
|
196
|
+
// Build parent -> children map
|
|
197
|
+
for (const task of tasks) {
|
|
198
|
+
if (task.parentId) {
|
|
199
|
+
const children = childrenMap.get(task.parentId) || [];
|
|
200
|
+
children.push(task);
|
|
201
|
+
childrenMap.set(task.parentId, children);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function buildNode(task: Task): TaskTreeNode {
|
|
206
|
+
const children = childrenMap.get(task.id) || [];
|
|
207
|
+
return {
|
|
208
|
+
task,
|
|
209
|
+
children: children.map(buildNode),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (rootId) {
|
|
214
|
+
const rootTask = taskMap.get(rootId);
|
|
215
|
+
if (!rootTask) {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
return [buildNode(rootTask)];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Return all root tasks (no parent)
|
|
222
|
+
const rootTasks = tasks.filter((t) => !t.parentId);
|
|
223
|
+
return rootTasks.map(buildNode);
|
|
224
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
generateId,
|
|
4
|
+
generateTaskId,
|
|
5
|
+
generateProjectId,
|
|
6
|
+
generateInboxId,
|
|
7
|
+
isValidTaskId,
|
|
8
|
+
isValidProjectId,
|
|
9
|
+
isValidInboxId,
|
|
10
|
+
validateTaskId,
|
|
11
|
+
validateProjectId,
|
|
12
|
+
validateInboxId,
|
|
13
|
+
assertValidTaskId,
|
|
14
|
+
assertValidProjectId,
|
|
15
|
+
assertValidInboxId,
|
|
16
|
+
InvalidIdError,
|
|
17
|
+
} from "./id.js";
|
|
18
|
+
|
|
19
|
+
describe("ID Generation", () => {
|
|
20
|
+
describe("generateId", () => {
|
|
21
|
+
test("generates ID without prefix", () => {
|
|
22
|
+
const id = generateId();
|
|
23
|
+
expect(id.length).toBe(12);
|
|
24
|
+
expect(/^[a-z0-9]+$/.test(id)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("generates ID with prefix", () => {
|
|
28
|
+
const id = generateId("test");
|
|
29
|
+
expect(id).toMatch(/^test_[a-z0-9]+$/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("generates unique IDs", () => {
|
|
33
|
+
const ids = new Set(Array.from({ length: 100 }, () => generateId("test")));
|
|
34
|
+
expect(ids.size).toBe(100);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("generateTaskId", () => {
|
|
39
|
+
test("generates task ID with correct prefix", () => {
|
|
40
|
+
const id = generateTaskId();
|
|
41
|
+
expect(id).toMatch(/^task_[a-z0-9]+$/);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("generateProjectId", () => {
|
|
46
|
+
test("generates project ID with correct prefix", () => {
|
|
47
|
+
const id = generateProjectId();
|
|
48
|
+
expect(id).toMatch(/^proj_[a-z0-9]+$/);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("generateInboxId", () => {
|
|
53
|
+
test("generates inbox ID with correct prefix", () => {
|
|
54
|
+
const id = generateInboxId();
|
|
55
|
+
expect(id).toMatch(/^inbox_[a-z0-9]+$/);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("Simple ID Validation (boolean)", () => {
|
|
61
|
+
describe("isValidTaskId", () => {
|
|
62
|
+
test("returns true for valid task IDs", () => {
|
|
63
|
+
expect(isValidTaskId("task_abc123")).toBe(true);
|
|
64
|
+
expect(isValidTaskId("task_a")).toBe(true);
|
|
65
|
+
expect(isValidTaskId("task_123")).toBe(true);
|
|
66
|
+
expect(isValidTaskId("task_abc123def456")).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("returns false for invalid task IDs", () => {
|
|
70
|
+
expect(isValidTaskId("")).toBe(false);
|
|
71
|
+
expect(isValidTaskId("task_")).toBe(false);
|
|
72
|
+
expect(isValidTaskId("abc123")).toBe(false);
|
|
73
|
+
expect(isValidTaskId("proj_abc123")).toBe(false);
|
|
74
|
+
expect(isValidTaskId("task_ABC123")).toBe(false);
|
|
75
|
+
expect(isValidTaskId("task_abc-123")).toBe(false);
|
|
76
|
+
expect(isValidTaskId("task_abc.123")).toBe(false);
|
|
77
|
+
expect(isValidTaskId("task_abc/123")).toBe(false);
|
|
78
|
+
expect(isValidTaskId("task_abc 123")).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("isValidProjectId", () => {
|
|
83
|
+
test("returns true for valid project IDs", () => {
|
|
84
|
+
expect(isValidProjectId("proj_abc123")).toBe(true);
|
|
85
|
+
expect(isValidProjectId("proj_a")).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("returns false for invalid project IDs", () => {
|
|
89
|
+
expect(isValidProjectId("")).toBe(false);
|
|
90
|
+
expect(isValidProjectId("project_abc123")).toBe(false);
|
|
91
|
+
expect(isValidProjectId("task_abc123")).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("isValidInboxId", () => {
|
|
96
|
+
test("returns true for valid inbox IDs", () => {
|
|
97
|
+
expect(isValidInboxId("inbox_abc123")).toBe(true);
|
|
98
|
+
expect(isValidInboxId("inbox_a")).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("returns false for invalid inbox IDs", () => {
|
|
102
|
+
expect(isValidInboxId("")).toBe(false);
|
|
103
|
+
expect(isValidInboxId("in_abc123")).toBe(false);
|
|
104
|
+
expect(isValidInboxId("task_abc123")).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("Detailed ID Validation", () => {
|
|
110
|
+
describe("validateTaskId", () => {
|
|
111
|
+
test("returns valid: true for valid task IDs", () => {
|
|
112
|
+
expect(validateTaskId("task_abc123")).toEqual({ valid: true });
|
|
113
|
+
expect(validateTaskId("task_a1b2c3")).toEqual({ valid: true });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("returns error for null/undefined", () => {
|
|
117
|
+
const result = validateTaskId(null);
|
|
118
|
+
expect(result.valid).toBe(false);
|
|
119
|
+
expect(result.reason).toContain("required");
|
|
120
|
+
|
|
121
|
+
const result2 = validateTaskId(undefined);
|
|
122
|
+
expect(result2.valid).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("returns error for non-string", () => {
|
|
126
|
+
const result = validateTaskId(123);
|
|
127
|
+
expect(result.valid).toBe(false);
|
|
128
|
+
expect(result.reason).toContain("string");
|
|
129
|
+
expect(result.reason).toContain("number");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("returns error for empty string", () => {
|
|
133
|
+
const result = validateTaskId("");
|
|
134
|
+
expect(result.valid).toBe(false);
|
|
135
|
+
expect(result.reason).toContain("empty");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("returns error for missing prefix", () => {
|
|
139
|
+
const result = validateTaskId("abc123");
|
|
140
|
+
expect(result.valid).toBe(false);
|
|
141
|
+
expect(result.reason).toContain("task_");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("returns error for wrong prefix", () => {
|
|
145
|
+
const result = validateTaskId("proj_abc123");
|
|
146
|
+
expect(result.valid).toBe(false);
|
|
147
|
+
expect(result.reason).toContain("task_");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("returns error for invalid characters", () => {
|
|
151
|
+
const result = validateTaskId("task_abc-123");
|
|
152
|
+
expect(result.valid).toBe(false);
|
|
153
|
+
expect(result.reason).toContain("invalid characters");
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("validateProjectId", () => {
|
|
158
|
+
test("returns valid: true for valid project IDs", () => {
|
|
159
|
+
expect(validateProjectId("proj_abc123")).toEqual({ valid: true });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("returns error for missing prefix", () => {
|
|
163
|
+
const result = validateProjectId("project_abc");
|
|
164
|
+
expect(result.valid).toBe(false);
|
|
165
|
+
expect(result.reason).toContain("proj_");
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("validateInboxId", () => {
|
|
170
|
+
test("returns valid: true for valid inbox IDs", () => {
|
|
171
|
+
expect(validateInboxId("inbox_abc123")).toEqual({ valid: true });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("returns error for missing prefix", () => {
|
|
175
|
+
const result = validateInboxId("in_abc");
|
|
176
|
+
expect(result.valid).toBe(false);
|
|
177
|
+
expect(result.reason).toContain("inbox_");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("Assert Functions", () => {
|
|
183
|
+
describe("assertValidTaskId", () => {
|
|
184
|
+
test("does not throw for valid ID", () => {
|
|
185
|
+
expect(() => assertValidTaskId("task_abc123")).not.toThrow();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("throws InvalidIdError for invalid ID", () => {
|
|
189
|
+
expect(() => assertValidTaskId("")).toThrow(InvalidIdError);
|
|
190
|
+
expect(() => assertValidTaskId("abc123")).toThrow(InvalidIdError);
|
|
191
|
+
expect(() => assertValidTaskId(null)).toThrow(InvalidIdError);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("InvalidIdError contains correct properties", () => {
|
|
195
|
+
try {
|
|
196
|
+
assertValidTaskId("invalid");
|
|
197
|
+
} catch (error) {
|
|
198
|
+
expect(error).toBeInstanceOf(InvalidIdError);
|
|
199
|
+
if (error instanceof InvalidIdError) {
|
|
200
|
+
expect(error.idType).toBe("task");
|
|
201
|
+
expect(error.invalidValue).toBe("invalid");
|
|
202
|
+
expect(error.reason).toBeDefined();
|
|
203
|
+
expect(error.message).toContain("task");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("assertValidProjectId", () => {
|
|
210
|
+
test("does not throw for valid ID", () => {
|
|
211
|
+
expect(() => assertValidProjectId("proj_abc123")).not.toThrow();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("throws InvalidIdError for invalid ID", () => {
|
|
215
|
+
expect(() => assertValidProjectId("project_abc")).toThrow(InvalidIdError);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe("assertValidInboxId", () => {
|
|
220
|
+
test("does not throw for valid ID", () => {
|
|
221
|
+
expect(() => assertValidInboxId("inbox_abc123")).not.toThrow();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("throws InvalidIdError for invalid ID", () => {
|
|
225
|
+
expect(() => assertValidInboxId("in_abc")).toThrow(InvalidIdError);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("InvalidIdError", () => {
|
|
231
|
+
test("extends Error", () => {
|
|
232
|
+
const error = new InvalidIdError("task", "bad_id", "test reason");
|
|
233
|
+
expect(error).toBeInstanceOf(Error);
|
|
234
|
+
expect(error).toBeInstanceOf(InvalidIdError);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("has correct name", () => {
|
|
238
|
+
const error = new InvalidIdError("task", "bad_id", "test reason");
|
|
239
|
+
expect(error.name).toBe("InvalidIdError");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("has correct message format", () => {
|
|
243
|
+
const error = new InvalidIdError("task", "bad_id", "test reason");
|
|
244
|
+
expect(error.message).toBe("Invalid task ID: test reason");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("exposes idType, invalidValue, and reason", () => {
|
|
248
|
+
const error = new InvalidIdError("project", "wrong", "missing prefix");
|
|
249
|
+
expect(error.idType).toBe("project");
|
|
250
|
+
expect(error.invalidValue).toBe("wrong");
|
|
251
|
+
expect(error.reason).toBe("missing prefix");
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe("Security: ID Injection Prevention", () => {
|
|
256
|
+
test("rejects path traversal attempts", () => {
|
|
257
|
+
expect(validateTaskId("task_../etc/passwd").valid).toBe(false);
|
|
258
|
+
expect(validateTaskId("task_..%2F..%2Fetc").valid).toBe(false);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("rejects null bytes", () => {
|
|
262
|
+
expect(validateTaskId("task_abc\0def").valid).toBe(false);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("rejects SQL injection attempts", () => {
|
|
266
|
+
expect(validateTaskId("task_'; DROP TABLE tasks;--").valid).toBe(false);
|
|
267
|
+
expect(validateTaskId("task_1 OR 1=1").valid).toBe(false);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("rejects script injection attempts", () => {
|
|
271
|
+
expect(validateTaskId("task_<script>alert(1)</script>").valid).toBe(false);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("only allows safe alphanumeric characters", () => {
|
|
275
|
+
// The regex ^task_[a-z0-9]+$ only allows lowercase letters and numbers
|
|
276
|
+
expect(validateTaskId("task_abc123").valid).toBe(true);
|
|
277
|
+
expect(validateTaskId("task_UPPERCASE").valid).toBe(false);
|
|
278
|
+
expect(validateTaskId("task_with_underscore").valid).toBe(false);
|
|
279
|
+
expect(validateTaskId("task_with-dash").valid).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
});
|