@task-mcp/shared 0.1.0

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 (81) hide show
  1. package/dist/algorithms/critical-path.d.ts +46 -0
  2. package/dist/algorithms/critical-path.d.ts.map +1 -0
  3. package/dist/algorithms/critical-path.js +308 -0
  4. package/dist/algorithms/critical-path.js.map +1 -0
  5. package/dist/algorithms/critical-path.test.d.ts +2 -0
  6. package/dist/algorithms/critical-path.test.d.ts.map +1 -0
  7. package/dist/algorithms/critical-path.test.js +194 -0
  8. package/dist/algorithms/critical-path.test.js.map +1 -0
  9. package/dist/algorithms/index.d.ts +3 -0
  10. package/dist/algorithms/index.d.ts.map +1 -0
  11. package/dist/algorithms/index.js +3 -0
  12. package/dist/algorithms/index.js.map +1 -0
  13. package/dist/algorithms/topological-sort.d.ts +41 -0
  14. package/dist/algorithms/topological-sort.d.ts.map +1 -0
  15. package/dist/algorithms/topological-sort.js +168 -0
  16. package/dist/algorithms/topological-sort.js.map +1 -0
  17. package/dist/algorithms/topological-sort.test.d.ts +2 -0
  18. package/dist/algorithms/topological-sort.test.d.ts.map +1 -0
  19. package/dist/algorithms/topological-sort.test.js +162 -0
  20. package/dist/algorithms/topological-sort.test.js.map +1 -0
  21. package/dist/index.d.ts +4 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +7 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/schemas/index.d.ts +4 -0
  26. package/dist/schemas/index.d.ts.map +1 -0
  27. package/dist/schemas/index.js +7 -0
  28. package/dist/schemas/index.js.map +1 -0
  29. package/dist/schemas/project.d.ts +55 -0
  30. package/dist/schemas/project.d.ts.map +1 -0
  31. package/dist/schemas/project.js +48 -0
  32. package/dist/schemas/project.js.map +1 -0
  33. package/dist/schemas/task.d.ts +124 -0
  34. package/dist/schemas/task.d.ts.map +1 -0
  35. package/dist/schemas/task.js +89 -0
  36. package/dist/schemas/task.js.map +1 -0
  37. package/dist/schemas/view.d.ts +44 -0
  38. package/dist/schemas/view.d.ts.map +1 -0
  39. package/dist/schemas/view.js +33 -0
  40. package/dist/schemas/view.js.map +1 -0
  41. package/dist/utils/date.d.ts +25 -0
  42. package/dist/utils/date.d.ts.map +1 -0
  43. package/dist/utils/date.js +103 -0
  44. package/dist/utils/date.js.map +1 -0
  45. package/dist/utils/date.test.d.ts +2 -0
  46. package/dist/utils/date.test.d.ts.map +1 -0
  47. package/dist/utils/date.test.js +138 -0
  48. package/dist/utils/date.test.js.map +1 -0
  49. package/dist/utils/id.d.ts +27 -0
  50. package/dist/utils/id.d.ts.map +1 -0
  51. package/dist/utils/id.js +41 -0
  52. package/dist/utils/id.js.map +1 -0
  53. package/dist/utils/index.d.ts +4 -0
  54. package/dist/utils/index.d.ts.map +1 -0
  55. package/dist/utils/index.js +4 -0
  56. package/dist/utils/index.js.map +1 -0
  57. package/dist/utils/natural-language.d.ts +12 -0
  58. package/dist/utils/natural-language.d.ts.map +1 -0
  59. package/dist/utils/natural-language.js +112 -0
  60. package/dist/utils/natural-language.js.map +1 -0
  61. package/dist/utils/natural-language.test.d.ts +2 -0
  62. package/dist/utils/natural-language.test.d.ts.map +1 -0
  63. package/dist/utils/natural-language.test.js +132 -0
  64. package/dist/utils/natural-language.test.js.map +1 -0
  65. package/package.json +46 -0
  66. package/src/algorithms/critical-path.test.ts +241 -0
  67. package/src/algorithms/critical-path.ts +413 -0
  68. package/src/algorithms/index.ts +17 -0
  69. package/src/algorithms/topological-sort.test.ts +190 -0
  70. package/src/algorithms/topological-sort.ts +204 -0
  71. package/src/index.ts +8 -0
  72. package/src/schemas/index.ts +30 -0
  73. package/src/schemas/project.ts +62 -0
  74. package/src/schemas/task.ts +116 -0
  75. package/src/schemas/view.ts +46 -0
  76. package/src/utils/date.test.ts +160 -0
  77. package/src/utils/date.ts +119 -0
  78. package/src/utils/id.ts +45 -0
  79. package/src/utils/index.ts +3 -0
  80. package/src/utils/natural-language.test.ts +154 -0
  81. package/src/utils/natural-language.ts +125 -0
@@ -0,0 +1,204 @@
1
+ import type { Task } from "../schemas/task.js";
2
+
3
+ /**
4
+ * Node representation for graph algorithms
5
+ */
6
+ export interface TaskNode {
7
+ id: string;
8
+ dependencies: string[]; // IDs of tasks this depends on (blocked_by)
9
+ priority: number; // Numeric priority for tie-breaking (higher = more important)
10
+ estimate: number; // Duration in minutes
11
+ }
12
+
13
+ /**
14
+ * Convert priority string to numeric value
15
+ */
16
+ export function priorityToNumber(priority: string): number {
17
+ const map: Record<string, number> = {
18
+ critical: 4,
19
+ high: 3,
20
+ medium: 2,
21
+ low: 1,
22
+ };
23
+ return map[priority] ?? 2;
24
+ }
25
+
26
+ /**
27
+ * Convert Task to TaskNode
28
+ */
29
+ export function taskToNode(task: Task): TaskNode {
30
+ return {
31
+ id: task.id,
32
+ dependencies: (task.dependencies ?? [])
33
+ .filter((d) => d.type === "blocked_by")
34
+ .map((d) => d.taskId),
35
+ priority: priorityToNumber(task.priority),
36
+ estimate: task.estimate?.expected ?? 0,
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Topological sort using Kahn's algorithm with priority tie-breaking
42
+ *
43
+ * Returns tasks in optimal execution order:
44
+ * 1. Respects dependencies (blocked_by relationships)
45
+ * 2. Higher priority tasks come first when dependencies allow
46
+ *
47
+ * @throws Error if circular dependency detected
48
+ */
49
+ export function topologicalSort(tasks: Task[]): Task[] {
50
+ const nodes = tasks.map(taskToNode);
51
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
52
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
53
+
54
+ // Calculate in-degree for each node
55
+ const inDegree = new Map<string, number>();
56
+ const adjacency = new Map<string, string[]>();
57
+
58
+ for (const node of nodes) {
59
+ inDegree.set(node.id, 0);
60
+ adjacency.set(node.id, []);
61
+ }
62
+
63
+ // Build graph: if A is blocked_by B, then B -> A (B must come before A)
64
+ for (const node of nodes) {
65
+ for (const depId of node.dependencies) {
66
+ if (nodeMap.has(depId)) {
67
+ const adj = adjacency.get(depId);
68
+ if (adj) adj.push(node.id);
69
+ inDegree.set(node.id, (inDegree.get(node.id) ?? 0) + 1);
70
+ }
71
+ }
72
+ }
73
+
74
+ // Initialize queue with nodes that have no dependencies
75
+ const queue: TaskNode[] = [];
76
+ for (const node of nodes) {
77
+ if (inDegree.get(node.id) === 0) {
78
+ queue.push(node);
79
+ }
80
+ }
81
+
82
+ // Sort by priority (higher first)
83
+ queue.sort((a, b) => b.priority - a.priority);
84
+
85
+ const result: Task[] = [];
86
+
87
+ while (queue.length > 0) {
88
+ const current = queue.shift()!;
89
+ const task = taskMap.get(current.id);
90
+ if (task) {
91
+ result.push(task);
92
+ }
93
+
94
+ // Update neighbors
95
+ for (const neighborId of adjacency.get(current.id)!) {
96
+ const newDegree = inDegree.get(neighborId)! - 1;
97
+ inDegree.set(neighborId, newDegree);
98
+
99
+ if (newDegree === 0) {
100
+ const neighborNode = nodeMap.get(neighborId)!;
101
+ queue.push(neighborNode);
102
+ // Re-sort to maintain priority order
103
+ queue.sort((a, b) => b.priority - a.priority);
104
+ }
105
+ }
106
+ }
107
+
108
+ // Check for cycles
109
+ if (result.length !== tasks.length) {
110
+ const remaining = tasks.filter((t) => !result.some((r) => r.id === t.id));
111
+ const cycleIds = remaining.map((t) => t.id).join(", ");
112
+ throw new Error(`Circular dependency detected among tasks: ${cycleIds}`);
113
+ }
114
+
115
+ return result;
116
+ }
117
+
118
+ /**
119
+ * Detect if adding a dependency would create a cycle
120
+ */
121
+ export function wouldCreateCycle(
122
+ tasks: Task[],
123
+ fromId: string,
124
+ toId: string
125
+ ): boolean {
126
+ // Create a temporary task list with the new dependency
127
+ const tempTasks = tasks.map((t) => {
128
+ if (t.id === fromId) {
129
+ return {
130
+ ...t,
131
+ dependencies: [
132
+ ...(t.dependencies ?? []),
133
+ { taskId: toId, type: "blocked_by" as const },
134
+ ],
135
+ };
136
+ }
137
+ return t;
138
+ });
139
+
140
+ try {
141
+ topologicalSort(tempTasks);
142
+ return false;
143
+ } catch {
144
+ return true;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Find all tasks that depend on a given task (directly or transitively)
150
+ */
151
+ export function findDependents(tasks: Task[], taskId: string): Task[] {
152
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
153
+ const visited = new Set<string>();
154
+ const result: Task[] = [];
155
+
156
+ function dfs(id: string) {
157
+ for (const task of tasks) {
158
+ const deps = (task.dependencies ?? [])
159
+ .filter((d) => d.type === "blocked_by")
160
+ .map((d) => d.taskId);
161
+
162
+ if (deps.includes(id) && !visited.has(task.id)) {
163
+ visited.add(task.id);
164
+ result.push(task);
165
+ dfs(task.id);
166
+ }
167
+ }
168
+ }
169
+
170
+ dfs(taskId);
171
+ return result;
172
+ }
173
+
174
+ /**
175
+ * Find all tasks that a given task depends on (directly or transitively)
176
+ */
177
+ export function findDependencies(tasks: Task[], taskId: string): Task[] {
178
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
179
+ const visited = new Set<string>();
180
+ const result: Task[] = [];
181
+
182
+ function dfs(id: string) {
183
+ const task = taskMap.get(id);
184
+ if (!task) return;
185
+
186
+ const deps = (task.dependencies ?? [])
187
+ .filter((d) => d.type === "blocked_by")
188
+ .map((d) => d.taskId);
189
+
190
+ for (const depId of deps) {
191
+ if (!visited.has(depId)) {
192
+ visited.add(depId);
193
+ const depTask = taskMap.get(depId);
194
+ if (depTask) {
195
+ result.push(depTask);
196
+ dfs(depId);
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ dfs(taskId);
203
+ return result;
204
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ // Schemas
2
+ export * from "./schemas/index.js";
3
+
4
+ // Algorithms
5
+ export * from "./algorithms/index.js";
6
+
7
+ // Utilities
8
+ export * from "./utils/index.js";
@@ -0,0 +1,30 @@
1
+ // Task schemas
2
+ export {
3
+ Priority,
4
+ TaskStatus,
5
+ DependencyType,
6
+ Dependency,
7
+ TimeEstimate,
8
+ Recurrence,
9
+ Task,
10
+ TaskCreateInput,
11
+ TaskUpdateInput,
12
+ } from "./task.js";
13
+
14
+ // Project schemas
15
+ export {
16
+ ProjectStatus,
17
+ Context,
18
+ Project,
19
+ ProjectCreateInput,
20
+ ProjectUpdateInput,
21
+ } from "./project.js";
22
+
23
+ // View schemas
24
+ export {
25
+ SmartViewFilter,
26
+ SortField,
27
+ SortOrder,
28
+ SmartView,
29
+ BuiltInView,
30
+ } from "./view.js";
@@ -0,0 +1,62 @@
1
+ import { type } from "arktype";
2
+ import { Priority } from "./task.js";
3
+
4
+ // Project status
5
+ export const ProjectStatus = type(
6
+ "'active' | 'on_hold' | 'completed' | 'archived'"
7
+ );
8
+ export type ProjectStatus = typeof ProjectStatus.infer;
9
+
10
+ // Context definition
11
+ export const Context = type({
12
+ name: "string",
13
+ "color?": "string", // hex color
14
+ "description?": "string",
15
+ });
16
+ export type Context = typeof Context.infer;
17
+
18
+ // Project schema
19
+ export const Project = type({
20
+ id: "string",
21
+ name: "string",
22
+ "description?": "string",
23
+ status: ProjectStatus,
24
+
25
+ // Project-level settings
26
+ "defaultPriority?": Priority,
27
+ "contexts?": Context.array(),
28
+
29
+ // Metadata
30
+ createdAt: "string",
31
+ updatedAt: "string",
32
+ "targetDate?": "string",
33
+
34
+ // Computed stats
35
+ "completionPercentage?": "number",
36
+ "criticalPathLength?": "number", // Total minutes on critical path
37
+ "blockedTaskCount?": "number",
38
+ "totalTasks?": "number",
39
+ "completedTasks?": "number",
40
+ });
41
+ export type Project = typeof Project.infer;
42
+
43
+ // Project creation input
44
+ export const ProjectCreateInput = type({
45
+ name: "string",
46
+ "description?": "string",
47
+ "defaultPriority?": Priority,
48
+ "contexts?": Context.array(),
49
+ "targetDate?": "string",
50
+ });
51
+ export type ProjectCreateInput = typeof ProjectCreateInput.infer;
52
+
53
+ // Project update input
54
+ export const ProjectUpdateInput = type({
55
+ "name?": "string",
56
+ "description?": "string",
57
+ "status?": ProjectStatus,
58
+ "defaultPriority?": Priority,
59
+ "contexts?": Context.array(),
60
+ "targetDate?": "string",
61
+ });
62
+ export type ProjectUpdateInput = typeof ProjectUpdateInput.infer;
@@ -0,0 +1,116 @@
1
+ import { type } from "arktype";
2
+
3
+ // Priority levels
4
+ export const Priority = type("'critical' | 'high' | 'medium' | 'low'");
5
+ export type Priority = typeof Priority.infer;
6
+
7
+ // Task status with clear state machine
8
+ export const TaskStatus = type(
9
+ "'pending' | 'in_progress' | 'blocked' | 'completed' | 'cancelled'"
10
+ );
11
+ export type TaskStatus = typeof TaskStatus.infer;
12
+
13
+ // Dependency relationship types
14
+ export const DependencyType = type(
15
+ "'blocks' | 'blocked_by' | 'related'"
16
+ );
17
+ export type DependencyType = typeof DependencyType.infer;
18
+
19
+ // A single dependency link
20
+ export const Dependency = type({
21
+ taskId: "string",
22
+ type: DependencyType,
23
+ "reason?": "string",
24
+ });
25
+ export type Dependency = typeof Dependency.infer;
26
+
27
+ // Time estimation
28
+ export const TimeEstimate = type({
29
+ "optimistic?": "number", // minutes
30
+ "expected?": "number", // minutes
31
+ "pessimistic?": "number", // minutes
32
+ "confidence?": "'low' | 'medium' | 'high'",
33
+ });
34
+ export type TimeEstimate = typeof TimeEstimate.infer;
35
+
36
+ // Recurrence pattern
37
+ export const Recurrence = type({
38
+ pattern: "'daily' | 'weekly' | 'monthly' | 'after_completion'",
39
+ "interval?": "number", // every N days/weeks/months
40
+ "daysOfWeek?": "number[]", // 0-6 for weekly
41
+ "endDate?": "string",
42
+ });
43
+ export type Recurrence = typeof Recurrence.infer;
44
+
45
+ // Core Task schema
46
+ export const Task = type({
47
+ id: "string",
48
+ title: "string",
49
+ "description?": "string",
50
+ status: TaskStatus,
51
+ priority: Priority,
52
+ projectId: "string",
53
+
54
+ // Dependencies
55
+ "dependencies?": Dependency.array(),
56
+
57
+ // Time tracking
58
+ "estimate?": TimeEstimate,
59
+ "actualMinutes?": "number",
60
+ "dueDate?": "string", // ISO date string
61
+ "startDate?": "string", // When task can start
62
+ "startedAt?": "string",
63
+ "completedAt?": "string",
64
+
65
+ // Organization
66
+ "contexts?": "string[]", // e.g., ["focus", "review"]
67
+ "tags?": "string[]",
68
+
69
+ // Recurrence
70
+ "recurrence?": Recurrence,
71
+
72
+ // Metadata
73
+ createdAt: "string",
74
+ updatedAt: "string",
75
+
76
+ // Computed fields (populated at runtime)
77
+ "criticalPath?": "boolean",
78
+ "slack?": "number", // Minutes of slack time
79
+ "earliestStart?": "number", // Minutes from project start
80
+ "latestStart?": "number",
81
+ });
82
+ export type Task = typeof Task.infer;
83
+
84
+ // Task creation input (minimal required fields)
85
+ export const TaskCreateInput = type({
86
+ title: "string",
87
+ "description?": "string",
88
+ "projectId?": "string",
89
+ "priority?": Priority,
90
+ "dependencies?": Dependency.array(),
91
+ "estimate?": TimeEstimate,
92
+ "dueDate?": "string",
93
+ "startDate?": "string",
94
+ "contexts?": "string[]",
95
+ "tags?": "string[]",
96
+ "recurrence?": Recurrence,
97
+ });
98
+ export type TaskCreateInput = typeof TaskCreateInput.infer;
99
+
100
+ // Task update input
101
+ export const TaskUpdateInput = type({
102
+ "title?": "string",
103
+ "description?": "string",
104
+ "status?": TaskStatus,
105
+ "priority?": Priority,
106
+ "projectId?": "string",
107
+ "dependencies?": Dependency.array(),
108
+ "estimate?": TimeEstimate,
109
+ "actualMinutes?": "number",
110
+ "dueDate?": "string",
111
+ "startDate?": "string",
112
+ "contexts?": "string[]",
113
+ "tags?": "string[]",
114
+ "recurrence?": Recurrence,
115
+ });
116
+ export type TaskUpdateInput = typeof TaskUpdateInput.infer;
@@ -0,0 +1,46 @@
1
+ import { type } from "arktype";
2
+ import { Priority, TaskStatus } from "./task.js";
3
+
4
+ // Smart View filter
5
+ export const SmartViewFilter = type({
6
+ "statuses?": TaskStatus.array(),
7
+ "priorities?": Priority.array(),
8
+ "contexts?": "string[]",
9
+ "tags?": "string[]",
10
+ "projectIds?": "string[]",
11
+ "dueBefore?": "string",
12
+ "dueAfter?": "string",
13
+ "isBlocked?": "boolean",
14
+ "isCriticalPath?": "boolean",
15
+ "hasNoDependencies?": "boolean",
16
+ "search?": "string", // Search in title/description
17
+ });
18
+ export type SmartViewFilter = typeof SmartViewFilter.infer;
19
+
20
+ // Sort options
21
+ export const SortField = type(
22
+ "'priority' | 'dueDate' | 'createdAt' | 'criticalPath' | 'slack' | 'title'"
23
+ );
24
+ export type SortField = typeof SortField.infer;
25
+
26
+ export const SortOrder = type("'asc' | 'desc'");
27
+ export type SortOrder = typeof SortOrder.infer;
28
+
29
+ // Smart View definition
30
+ export const SmartView = type({
31
+ id: "string",
32
+ name: "string",
33
+ "description?": "string",
34
+ filter: SmartViewFilter,
35
+ "sortBy?": SortField,
36
+ "sortOrder?": SortOrder,
37
+ createdAt: "string",
38
+ updatedAt: "string",
39
+ });
40
+ export type SmartView = typeof SmartView.infer;
41
+
42
+ // Built-in view names
43
+ export const BuiltInView = type(
44
+ "'today' | 'this_week' | 'blocked' | 'critical_path' | 'quick_wins' | 'all'"
45
+ );
46
+ export type BuiltInView = typeof BuiltInView.infer;
@@ -0,0 +1,160 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { now, parseRelativeDate, formatDate, isToday, isPastDue, isWithinDays } from "./date.js";
3
+
4
+ describe("now", () => {
5
+ test("returns ISO timestamp string", () => {
6
+ const result = now();
7
+ expect(typeof result).toBe("string");
8
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
9
+ });
10
+
11
+ test("returns current time within tolerance", () => {
12
+ const before = Date.now();
13
+ const result = now();
14
+ const after = Date.now();
15
+
16
+ const resultTime = new Date(result).getTime();
17
+ expect(resultTime).toBeGreaterThanOrEqual(before);
18
+ expect(resultTime).toBeLessThanOrEqual(after);
19
+ });
20
+ });
21
+
22
+ describe("parseRelativeDate", () => {
23
+ test("parses 'today'", () => {
24
+ const result = parseRelativeDate("today");
25
+ expect(result).not.toBeNull();
26
+ const today = new Date();
27
+ expect(result!.getDate()).toBe(today.getDate());
28
+ });
29
+
30
+ test("parses '오늘' (Korean today)", () => {
31
+ const result = parseRelativeDate("오늘");
32
+ expect(result).not.toBeNull();
33
+ const today = new Date();
34
+ expect(result!.getDate()).toBe(today.getDate());
35
+ });
36
+
37
+ test("parses 'tomorrow'", () => {
38
+ const result = parseRelativeDate("tomorrow");
39
+ expect(result).not.toBeNull();
40
+ const tomorrow = new Date();
41
+ tomorrow.setDate(tomorrow.getDate() + 1);
42
+ expect(result!.getDate()).toBe(tomorrow.getDate());
43
+ });
44
+
45
+ test("parses '내일' (Korean tomorrow)", () => {
46
+ const result = parseRelativeDate("내일");
47
+ expect(result).not.toBeNull();
48
+ const tomorrow = new Date();
49
+ tomorrow.setDate(tomorrow.getDate() + 1);
50
+ expect(result!.getDate()).toBe(tomorrow.getDate());
51
+ });
52
+
53
+ test("parses 'yesterday'", () => {
54
+ const result = parseRelativeDate("yesterday");
55
+ expect(result).not.toBeNull();
56
+ const yesterday = new Date();
57
+ yesterday.setDate(yesterday.getDate() - 1);
58
+ expect(result!.getDate()).toBe(yesterday.getDate());
59
+ });
60
+
61
+ test("parses 'next week'", () => {
62
+ const result = parseRelativeDate("next week");
63
+ expect(result).not.toBeNull();
64
+ const nextWeek = new Date();
65
+ nextWeek.setDate(nextWeek.getDate() + 7);
66
+ expect(result!.getDate()).toBe(nextWeek.getDate());
67
+ });
68
+
69
+ test("parses 'in 3 days'", () => {
70
+ const result = parseRelativeDate("in 3 days");
71
+ expect(result).not.toBeNull();
72
+ const future = new Date();
73
+ future.setDate(future.getDate() + 3);
74
+ expect(result!.getDate()).toBe(future.getDate());
75
+ });
76
+
77
+ test("parses '5일 후' (Korean days later)", () => {
78
+ const result = parseRelativeDate("5일 후");
79
+ expect(result).not.toBeNull();
80
+ const future = new Date();
81
+ future.setDate(future.getDate() + 5);
82
+ expect(result!.getDate()).toBe(future.getDate());
83
+ });
84
+
85
+ test("parses weekday names", () => {
86
+ const result = parseRelativeDate("monday");
87
+ expect(result).not.toBeNull();
88
+ expect(result!.getDay()).toBe(1); // Monday = 1
89
+ });
90
+
91
+ test("parses ISO date string", () => {
92
+ const result = parseRelativeDate("2025-12-31");
93
+ expect(result).not.toBeNull();
94
+ expect(result!.getFullYear()).toBe(2025);
95
+ expect(result!.getMonth()).toBe(11); // December = 11
96
+ expect(result!.getDate()).toBe(31);
97
+ });
98
+
99
+ test("returns null for invalid input", () => {
100
+ const result = parseRelativeDate("invalid date string");
101
+ expect(result).toBeNull();
102
+ });
103
+ });
104
+
105
+ describe("formatDate", () => {
106
+ test("formats date as YYYY-MM-DD", () => {
107
+ const date = new Date("2025-06-15T10:30:00");
108
+ expect(formatDate(date)).toBe("2025-06-15");
109
+ });
110
+ });
111
+
112
+ describe("isToday", () => {
113
+ test("returns true for today", () => {
114
+ expect(isToday(new Date())).toBe(true);
115
+ });
116
+
117
+ test("returns false for tomorrow", () => {
118
+ const tomorrow = new Date();
119
+ tomorrow.setDate(tomorrow.getDate() + 1);
120
+ expect(isToday(tomorrow)).toBe(false);
121
+ });
122
+
123
+ test("accepts string input", () => {
124
+ expect(isToday(new Date().toISOString())).toBe(true);
125
+ });
126
+ });
127
+
128
+ describe("isPastDue", () => {
129
+ test("returns true for yesterday", () => {
130
+ const yesterday = new Date();
131
+ yesterday.setDate(yesterday.getDate() - 1);
132
+ expect(isPastDue(yesterday)).toBe(true);
133
+ });
134
+
135
+ test("returns false for tomorrow", () => {
136
+ const tomorrow = new Date();
137
+ tomorrow.setDate(tomorrow.getDate() + 1);
138
+ expect(isPastDue(tomorrow)).toBe(false);
139
+ });
140
+ });
141
+
142
+ describe("isWithinDays", () => {
143
+ test("returns true for date within range", () => {
144
+ const inThreeDays = new Date();
145
+ inThreeDays.setDate(inThreeDays.getDate() + 3);
146
+ expect(isWithinDays(inThreeDays, 7)).toBe(true);
147
+ });
148
+
149
+ test("returns false for date outside range", () => {
150
+ const inTenDays = new Date();
151
+ inTenDays.setDate(inTenDays.getDate() + 10);
152
+ expect(isWithinDays(inTenDays, 7)).toBe(false);
153
+ });
154
+
155
+ test("returns false for past date", () => {
156
+ const yesterday = new Date();
157
+ yesterday.setDate(yesterday.getDate() - 1);
158
+ expect(isWithinDays(yesterday, 7)).toBe(false);
159
+ });
160
+ });