mcp-sunsama 0.5.2 → 0.6.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # mcp-sunsama
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7c90fef: feat: add update-task-backlog tool for moving tasks to backlog
8
+
9
+ - Add new update-task-backlog tool that moves tasks to the backlog
10
+ - Update updateTaskSnoozeDateSchema to require newDay parameter
11
+ - Add comprehensive test coverage for the new tool
12
+ - Update documentation in README.md and CLAUDE.md
13
+
3
14
  ## 0.5.2
4
15
 
5
16
  ### Patch Changes
package/CLAUDE.md CHANGED
@@ -127,7 +127,7 @@ Optional:
127
127
  ### Task Operations
128
128
  Full CRUD support:
129
129
  - **Read**: `get-tasks-by-day`, `get-tasks-backlog`, `get-archived-tasks`, `get-streams`
130
- - **Write**: `create-task`, `update-task-complete`, `delete-task`
130
+ - **Write**: `create-task`, `update-task-complete`, `update-task-snooze-date`, `update-task-backlog`, `delete-task`
131
131
 
132
132
  Task read operations support response trimming. `get-tasks-by-day` includes completion filtering. `get-archived-tasks` includes enhanced pagination with hasMore flag for LLM decision-making.
133
133
 
package/README.md CHANGED
@@ -93,7 +93,8 @@ Add this configuration to your Claude Desktop MCP settings:
93
93
  - `get-tasks-backlog` - Get backlog tasks
94
94
  - `get-archived-tasks` - Get archived tasks with pagination (includes hasMore flag for LLM context)
95
95
  - `update-task-complete` - Mark tasks as complete
96
- - `update-task-snooze-date` - Reschedule tasks to different dates or move to backlog
96
+ - `update-task-snooze-date` - Reschedule tasks to different dates
97
+ - `update-task-backlog` - Move tasks to the backlog
97
98
  - `delete-task` - Delete tasks permanently
98
99
 
99
100
  ### User & Stream Operations
package/dist/main.js CHANGED
@@ -3,7 +3,7 @@ import { FastMCP } from "fastmcp";
3
3
  import { httpStreamAuthenticator } from "./auth/http.js";
4
4
  import { initializeStdioAuth } from "./auth/stdio.js";
5
5
  import { getTransportConfig } from "./config/transport.js";
6
- import { createTaskSchema, deleteTaskSchema, getArchivedTasksSchema, getStreamsSchema, getTasksBacklogSchema, getTasksByDaySchema, getUserSchema, updateTaskCompleteSchema, updateTaskSnoozeDateSchema } from "./schemas.js";
6
+ import { createTaskSchema, deleteTaskSchema, getArchivedTasksSchema, getStreamsSchema, getTasksBacklogSchema, getTasksByDaySchema, getUserSchema, updateTaskBacklogSchema, updateTaskCompleteSchema, updateTaskSnoozeDateSchema } from "./schemas.js";
7
7
  import { getSunsamaClient } from "./utils/client-resolver.js";
8
8
  import { filterTasksByCompletion } from "./utils/task-filters.js";
9
9
  import { trimTasksForResponse } from "./utils/task-trimmer.js";
@@ -16,7 +16,7 @@ if (transportConfig.transportType === "stdio") {
16
16
  }
17
17
  const server = new FastMCP({
18
18
  name: "Sunsama API Server",
19
- version: "0.5.2",
19
+ version: "0.6.0",
20
20
  instructions: `
21
21
  This MCP server provides access to the Sunsama API for task and project management.
22
22
 
@@ -420,6 +420,56 @@ server.addTool({
420
420
  }
421
421
  }
422
422
  });
423
+ server.addTool({
424
+ name: "update-task-backlog",
425
+ description: "Move a task to the backlog",
426
+ parameters: updateTaskBacklogSchema,
427
+ execute: async (args, { session, log }) => {
428
+ try {
429
+ // Extract parameters
430
+ const { taskId, timezone, limitResponsePayload } = args;
431
+ log.info("Moving task to backlog", {
432
+ taskId: taskId,
433
+ timezone: timezone,
434
+ limitResponsePayload: limitResponsePayload
435
+ });
436
+ // Get the appropriate client based on transport type
437
+ const sunsamaClient = getSunsamaClient(session);
438
+ // Build options object
439
+ const options = {};
440
+ if (timezone)
441
+ options.timezone = timezone;
442
+ if (limitResponsePayload !== undefined)
443
+ options.limitResponsePayload = limitResponsePayload;
444
+ // Call sunsamaClient.updateTaskSnoozeDate(taskId, null, options) to move to backlog
445
+ const result = await sunsamaClient.updateTaskSnoozeDate(taskId, null, options);
446
+ log.info("Successfully moved task to backlog", {
447
+ taskId: taskId,
448
+ success: result.success
449
+ });
450
+ return {
451
+ content: [
452
+ {
453
+ type: "text",
454
+ text: JSON.stringify({
455
+ success: result.success,
456
+ taskId: taskId,
457
+ movedToBacklog: true,
458
+ updatedFields: result.updatedFields
459
+ })
460
+ }
461
+ ]
462
+ };
463
+ }
464
+ catch (error) {
465
+ log.error("Failed to move task to backlog", {
466
+ taskId: args.taskId,
467
+ error: error instanceof Error ? error.message : 'Unknown error'
468
+ });
469
+ throw new Error(`Failed to move task to backlog: ${error instanceof Error ? error.message : 'Unknown error'}`);
470
+ }
471
+ }
472
+ });
423
473
  // Stream Operations
424
474
  server.addTool({
425
475
  name: "get-streams",
package/dist/schemas.d.ts CHANGED
@@ -94,17 +94,30 @@ export declare const deleteTaskSchema: z.ZodObject<{
94
94
  }>;
95
95
  export declare const updateTaskSnoozeDateSchema: z.ZodObject<{
96
96
  taskId: z.ZodString;
97
- newDay: z.ZodUnion<[z.ZodString, z.ZodNull]>;
97
+ newDay: z.ZodString;
98
+ timezone: z.ZodOptional<z.ZodString>;
99
+ limitResponsePayload: z.ZodOptional<z.ZodBoolean>;
100
+ }, "strip", z.ZodTypeAny, {
101
+ taskId: string;
102
+ newDay: string;
103
+ timezone?: string | undefined;
104
+ limitResponsePayload?: boolean | undefined;
105
+ }, {
106
+ taskId: string;
107
+ newDay: string;
108
+ timezone?: string | undefined;
109
+ limitResponsePayload?: boolean | undefined;
110
+ }>;
111
+ export declare const updateTaskBacklogSchema: z.ZodObject<{
112
+ taskId: z.ZodString;
98
113
  timezone: z.ZodOptional<z.ZodString>;
99
114
  limitResponsePayload: z.ZodOptional<z.ZodBoolean>;
100
115
  }, "strip", z.ZodTypeAny, {
101
116
  taskId: string;
102
- newDay: string | null;
103
117
  timezone?: string | undefined;
104
118
  limitResponsePayload?: boolean | undefined;
105
119
  }, {
106
120
  taskId: string;
107
- newDay: string | null;
108
121
  timezone?: string | undefined;
109
122
  limitResponsePayload?: boolean | undefined;
110
123
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"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"}
package/dist/schemas.js CHANGED
@@ -56,10 +56,13 @@ export const deleteTaskSchema = z.object({
56
56
  // Update task snooze date parameters
57
57
  export const updateTaskSnoozeDateSchema = z.object({
58
58
  taskId: z.string().min(1, "Task ID is required").describe("The ID of the task to reschedule"),
59
- newDay: z.union([
60
- z.string().date("Must be a valid date in YYYY-MM-DD format"),
61
- z.null()
62
- ]).describe("Target date in YYYY-MM-DD format, or null to move to backlog"),
59
+ newDay: z.string().date("Must be a valid date in YYYY-MM-DD format").describe("Target date in YYYY-MM-DD format"),
60
+ timezone: z.string().optional().describe("Timezone string (e.g., 'America/New_York'). If not provided, uses user's default timezone"),
61
+ limitResponsePayload: z.boolean().optional().describe("Whether to limit the response payload size"),
62
+ });
63
+ // Update task backlog parameters
64
+ export const updateTaskBacklogSchema = z.object({
65
+ taskId: z.string().min(1, "Task ID is required").describe("The ID of the task to move to backlog"),
63
66
  timezone: z.string().optional().describe("Timezone string (e.g., 'America/New_York'). If not provided, uses user's default timezone"),
64
67
  limitResponsePayload: z.boolean().optional().describe("Whether to limit the response payload size"),
65
68
  });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=schemas.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.test.d.ts","sourceRoot":"","sources":["../src/schemas.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,486 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { completionFilterSchema, getTasksByDaySchema, getTasksBacklogSchema, getArchivedTasksSchema, getUserSchema, getStreamsSchema, createTaskSchema, updateTaskCompleteSchema, deleteTaskSchema, updateTaskSnoozeDateSchema, updateTaskBacklogSchema, userProfileSchema, groupSchema, userSchema, taskSchema, streamSchema, userResponseSchema, tasksResponseSchema, streamsResponseSchema, errorResponseSchema, } from "./schemas.js";
3
+ describe("Tool Parameter Schemas", () => {
4
+ describe("completionFilterSchema", () => {
5
+ test("should accept valid completion filter values", () => {
6
+ expect(() => completionFilterSchema.parse("all")).not.toThrow();
7
+ expect(() => completionFilterSchema.parse("incomplete")).not.toThrow();
8
+ expect(() => completionFilterSchema.parse("completed")).not.toThrow();
9
+ });
10
+ test("should reject invalid completion filter values", () => {
11
+ expect(() => completionFilterSchema.parse("invalid")).toThrow();
12
+ expect(() => completionFilterSchema.parse("")).toThrow();
13
+ expect(() => completionFilterSchema.parse(null)).toThrow();
14
+ expect(() => completionFilterSchema.parse(undefined)).toThrow();
15
+ });
16
+ });
17
+ describe("getTasksByDaySchema", () => {
18
+ test("should accept valid day format", () => {
19
+ const validInput = {
20
+ day: "2024-01-15",
21
+ timezone: "America/New_York",
22
+ completionFilter: "all",
23
+ };
24
+ expect(() => getTasksByDaySchema.parse(validInput)).not.toThrow();
25
+ });
26
+ test("should accept minimal required input", () => {
27
+ const minimalInput = { day: "2024-01-15" };
28
+ expect(() => getTasksByDaySchema.parse(minimalInput)).not.toThrow();
29
+ });
30
+ test("should reject invalid day formats", () => {
31
+ expect(() => getTasksByDaySchema.parse({ day: "2024/01/15" })).toThrow();
32
+ expect(() => getTasksByDaySchema.parse({ day: "01-15-2024" })).toThrow();
33
+ expect(() => getTasksByDaySchema.parse({ day: "2024-1-15" })).toThrow();
34
+ expect(() => getTasksByDaySchema.parse({ day: "2024-01-5" })).toThrow();
35
+ expect(() => getTasksByDaySchema.parse({ day: "" })).toThrow();
36
+ expect(() => getTasksByDaySchema.parse({})).toThrow();
37
+ });
38
+ test("should reject invalid completion filters", () => {
39
+ expect(() => getTasksByDaySchema.parse({
40
+ day: "2024-01-15",
41
+ completionFilter: "invalid",
42
+ })).toThrow();
43
+ });
44
+ });
45
+ describe("getTasksBacklogSchema", () => {
46
+ test("should accept empty object", () => {
47
+ expect(() => getTasksBacklogSchema.parse({})).not.toThrow();
48
+ });
49
+ test("should ignore extra properties", () => {
50
+ expect(() => getTasksBacklogSchema.parse({ extra: "property" })).not.toThrow();
51
+ });
52
+ });
53
+ describe("getArchivedTasksSchema", () => {
54
+ test("should accept valid pagination parameters", () => {
55
+ const validInput = { offset: 0, limit: 100 };
56
+ expect(() => getArchivedTasksSchema.parse(validInput)).not.toThrow();
57
+ });
58
+ test("should accept empty object", () => {
59
+ expect(() => getArchivedTasksSchema.parse({})).not.toThrow();
60
+ });
61
+ test("should accept valid ranges", () => {
62
+ expect(() => getArchivedTasksSchema.parse({ offset: 0 })).not.toThrow();
63
+ expect(() => getArchivedTasksSchema.parse({ limit: 1 })).not.toThrow();
64
+ expect(() => getArchivedTasksSchema.parse({ limit: 1000 })).not.toThrow();
65
+ });
66
+ test("should reject invalid offset values", () => {
67
+ expect(() => getArchivedTasksSchema.parse({ offset: -1 })).toThrow();
68
+ expect(() => getArchivedTasksSchema.parse({ offset: 1.5 })).toThrow();
69
+ });
70
+ test("should reject invalid limit values", () => {
71
+ expect(() => getArchivedTasksSchema.parse({ limit: 0 })).toThrow();
72
+ expect(() => getArchivedTasksSchema.parse({ limit: 1001 })).toThrow();
73
+ expect(() => getArchivedTasksSchema.parse({ limit: -1 })).toThrow();
74
+ expect(() => getArchivedTasksSchema.parse({ limit: 1.5 })).toThrow();
75
+ });
76
+ });
77
+ describe("getUserSchema", () => {
78
+ test("should accept empty object", () => {
79
+ expect(() => getUserSchema.parse({})).not.toThrow();
80
+ });
81
+ });
82
+ describe("getStreamsSchema", () => {
83
+ test("should accept empty object", () => {
84
+ expect(() => getStreamsSchema.parse({})).not.toThrow();
85
+ });
86
+ });
87
+ describe("createTaskSchema", () => {
88
+ test("should accept valid task creation input", () => {
89
+ const validInput = {
90
+ text: "Test task",
91
+ notes: "Task notes",
92
+ streamIds: ["stream1", "stream2"],
93
+ timeEstimate: 60,
94
+ dueDate: "2024-01-15T10:00:00Z",
95
+ snoozeUntil: "2024-01-16T09:00:00Z",
96
+ private: true,
97
+ taskId: "custom-task-id",
98
+ };
99
+ expect(() => createTaskSchema.parse(validInput)).not.toThrow();
100
+ });
101
+ test("should accept minimal required input", () => {
102
+ const minimalInput = { text: "Test task" };
103
+ expect(() => createTaskSchema.parse(minimalInput)).not.toThrow();
104
+ });
105
+ test("should reject empty text", () => {
106
+ expect(() => createTaskSchema.parse({ text: "" })).toThrow();
107
+ expect(() => createTaskSchema.parse({})).toThrow();
108
+ });
109
+ test("should reject invalid time estimate", () => {
110
+ expect(() => createTaskSchema.parse({ text: "Test", timeEstimate: 0 })).toThrow();
111
+ expect(() => createTaskSchema.parse({ text: "Test", timeEstimate: -1 })).toThrow();
112
+ expect(() => createTaskSchema.parse({ text: "Test", timeEstimate: 1.5 })).toThrow();
113
+ });
114
+ });
115
+ describe("updateTaskCompleteSchema", () => {
116
+ test("should accept valid task completion input", () => {
117
+ const validInput = {
118
+ taskId: "task-123",
119
+ completeOn: "2024-01-15T10:00:00Z",
120
+ limitResponsePayload: true,
121
+ };
122
+ expect(() => updateTaskCompleteSchema.parse(validInput)).not.toThrow();
123
+ });
124
+ test("should accept minimal required input", () => {
125
+ const minimalInput = { taskId: "task-123" };
126
+ expect(() => updateTaskCompleteSchema.parse(minimalInput)).not.toThrow();
127
+ });
128
+ test("should reject empty task ID", () => {
129
+ expect(() => updateTaskCompleteSchema.parse({ taskId: "" })).toThrow();
130
+ expect(() => updateTaskCompleteSchema.parse({})).toThrow();
131
+ });
132
+ });
133
+ describe("deleteTaskSchema", () => {
134
+ test("should accept valid task deletion input", () => {
135
+ const validInput = {
136
+ taskId: "task-123",
137
+ limitResponsePayload: true,
138
+ wasTaskMerged: false,
139
+ };
140
+ expect(() => deleteTaskSchema.parse(validInput)).not.toThrow();
141
+ });
142
+ test("should accept minimal required input", () => {
143
+ const minimalInput = { taskId: "task-123" };
144
+ expect(() => deleteTaskSchema.parse(minimalInput)).not.toThrow();
145
+ });
146
+ test("should reject empty task ID", () => {
147
+ expect(() => deleteTaskSchema.parse({ taskId: "" })).toThrow();
148
+ expect(() => deleteTaskSchema.parse({})).toThrow();
149
+ });
150
+ });
151
+ describe("updateTaskSnoozeDateSchema", () => {
152
+ test("should accept valid date input", () => {
153
+ const validInput = {
154
+ taskId: "task-123",
155
+ newDay: "2024-01-15",
156
+ timezone: "America/New_York",
157
+ limitResponsePayload: true,
158
+ };
159
+ expect(() => updateTaskSnoozeDateSchema.parse(validInput)).not.toThrow();
160
+ });
161
+ test("should accept minimal required input", () => {
162
+ const minimalInput = { taskId: "task-123", newDay: "2024-01-15" };
163
+ expect(() => updateTaskSnoozeDateSchema.parse(minimalInput)).not.toThrow();
164
+ });
165
+ test("should reject empty task ID", () => {
166
+ expect(() => updateTaskSnoozeDateSchema.parse({ taskId: "", newDay: "2024-01-15" })).toThrow();
167
+ expect(() => updateTaskSnoozeDateSchema.parse({ newDay: "2024-01-15" })).toThrow();
168
+ });
169
+ test("should reject invalid date formats", () => {
170
+ expect(() => updateTaskSnoozeDateSchema.parse({
171
+ taskId: "task-123",
172
+ newDay: "2024/01/15",
173
+ })).toThrow();
174
+ expect(() => updateTaskSnoozeDateSchema.parse({
175
+ taskId: "task-123",
176
+ newDay: "01-15-2024",
177
+ })).toThrow();
178
+ expect(() => updateTaskSnoozeDateSchema.parse({
179
+ taskId: "task-123",
180
+ newDay: "invalid-date",
181
+ })).toThrow();
182
+ });
183
+ test("should reject missing newDay", () => {
184
+ expect(() => updateTaskSnoozeDateSchema.parse({ taskId: "task-123" })).toThrow();
185
+ });
186
+ });
187
+ describe("updateTaskBacklogSchema", () => {
188
+ test("should accept valid task backlog input", () => {
189
+ const validInput = {
190
+ taskId: "task-123",
191
+ timezone: "America/New_York",
192
+ limitResponsePayload: true,
193
+ };
194
+ expect(() => updateTaskBacklogSchema.parse(validInput)).not.toThrow();
195
+ });
196
+ test("should accept minimal required input", () => {
197
+ const minimalInput = { taskId: "task-123" };
198
+ expect(() => updateTaskBacklogSchema.parse(minimalInput)).not.toThrow();
199
+ });
200
+ test("should reject empty task ID", () => {
201
+ expect(() => updateTaskBacklogSchema.parse({ taskId: "" })).toThrow();
202
+ expect(() => updateTaskBacklogSchema.parse({})).toThrow();
203
+ });
204
+ });
205
+ });
206
+ describe("Response Schemas", () => {
207
+ describe("userProfileSchema", () => {
208
+ test("should accept valid user profile", () => {
209
+ const validProfile = {
210
+ _id: "user-123",
211
+ email: "test@example.com",
212
+ firstName: "John",
213
+ lastName: "Doe",
214
+ timezone: "America/New_York",
215
+ avatarUrl: "https://example.com/avatar.jpg",
216
+ };
217
+ expect(() => userProfileSchema.parse(validProfile)).not.toThrow();
218
+ });
219
+ test("should accept profile without optional fields", () => {
220
+ const minimalProfile = {
221
+ _id: "user-123",
222
+ email: "test@example.com",
223
+ firstName: "John",
224
+ lastName: "Doe",
225
+ timezone: "America/New_York",
226
+ };
227
+ expect(() => userProfileSchema.parse(minimalProfile)).not.toThrow();
228
+ });
229
+ test("should reject invalid email", () => {
230
+ const invalidProfile = {
231
+ _id: "user-123",
232
+ email: "invalid-email",
233
+ firstName: "John",
234
+ lastName: "Doe",
235
+ timezone: "America/New_York",
236
+ };
237
+ expect(() => userProfileSchema.parse(invalidProfile)).toThrow();
238
+ });
239
+ test("should reject invalid avatar URL", () => {
240
+ const invalidProfile = {
241
+ _id: "user-123",
242
+ email: "test@example.com",
243
+ firstName: "John",
244
+ lastName: "Doe",
245
+ timezone: "America/New_York",
246
+ avatarUrl: "invalid-url",
247
+ };
248
+ expect(() => userProfileSchema.parse(invalidProfile)).toThrow();
249
+ });
250
+ });
251
+ describe("groupSchema", () => {
252
+ test("should accept valid group", () => {
253
+ const validGroup = {
254
+ groupId: "group-123",
255
+ name: "Test Group",
256
+ role: "admin",
257
+ };
258
+ expect(() => groupSchema.parse(validGroup)).not.toThrow();
259
+ });
260
+ test("should accept group without optional role", () => {
261
+ const minimalGroup = {
262
+ groupId: "group-123",
263
+ name: "Test Group",
264
+ };
265
+ expect(() => groupSchema.parse(minimalGroup)).not.toThrow();
266
+ });
267
+ });
268
+ describe("userSchema", () => {
269
+ test("should accept valid user", () => {
270
+ const validUser = {
271
+ _id: "user-123",
272
+ email: "test@example.com",
273
+ profile: {
274
+ _id: "user-123",
275
+ email: "test@example.com",
276
+ firstName: "John",
277
+ lastName: "Doe",
278
+ timezone: "America/New_York",
279
+ },
280
+ primaryGroup: {
281
+ groupId: "group-123",
282
+ name: "Test Group",
283
+ },
284
+ };
285
+ expect(() => userSchema.parse(validUser)).not.toThrow();
286
+ });
287
+ test("should accept user without primary group", () => {
288
+ const userWithoutGroup = {
289
+ _id: "user-123",
290
+ email: "test@example.com",
291
+ profile: {
292
+ _id: "user-123",
293
+ email: "test@example.com",
294
+ firstName: "John",
295
+ lastName: "Doe",
296
+ timezone: "America/New_York",
297
+ },
298
+ };
299
+ expect(() => userSchema.parse(userWithoutGroup)).not.toThrow();
300
+ });
301
+ });
302
+ describe("taskSchema", () => {
303
+ test("should accept valid task", () => {
304
+ const validTask = {
305
+ _id: "task-123",
306
+ title: "Test Task",
307
+ description: "Task description",
308
+ status: "active",
309
+ createdAt: "2024-01-15T10:00:00Z",
310
+ updatedAt: "2024-01-15T11:00:00Z",
311
+ scheduledDate: "2024-01-16",
312
+ completedAt: "2024-01-16T12:00:00Z",
313
+ streamId: "stream-123",
314
+ userId: "user-123",
315
+ groupId: "group-123",
316
+ };
317
+ expect(() => taskSchema.parse(validTask)).not.toThrow();
318
+ });
319
+ test("should accept task with minimal required fields", () => {
320
+ const minimalTask = {
321
+ _id: "task-123",
322
+ title: "Test Task",
323
+ status: "active",
324
+ createdAt: "2024-01-15T10:00:00Z",
325
+ updatedAt: "2024-01-15T11:00:00Z",
326
+ userId: "user-123",
327
+ groupId: "group-123",
328
+ };
329
+ expect(() => taskSchema.parse(minimalTask)).not.toThrow();
330
+ });
331
+ });
332
+ describe("streamSchema", () => {
333
+ test("should accept valid stream", () => {
334
+ const validStream = {
335
+ _id: "stream-123",
336
+ name: "Test Stream",
337
+ color: "#FF0000",
338
+ groupId: "group-123",
339
+ isActive: true,
340
+ createdAt: "2024-01-15T10:00:00Z",
341
+ updatedAt: "2024-01-15T11:00:00Z",
342
+ };
343
+ expect(() => streamSchema.parse(validStream)).not.toThrow();
344
+ });
345
+ test("should accept stream without optional color", () => {
346
+ const minimalStream = {
347
+ _id: "stream-123",
348
+ name: "Test Stream",
349
+ groupId: "group-123",
350
+ isActive: true,
351
+ createdAt: "2024-01-15T10:00:00Z",
352
+ updatedAt: "2024-01-15T11:00:00Z",
353
+ };
354
+ expect(() => streamSchema.parse(minimalStream)).not.toThrow();
355
+ });
356
+ });
357
+ describe("userResponseSchema", () => {
358
+ test("should accept valid user response", () => {
359
+ const validResponse = {
360
+ user: {
361
+ _id: "user-123",
362
+ email: "test@example.com",
363
+ profile: {
364
+ _id: "user-123",
365
+ email: "test@example.com",
366
+ firstName: "John",
367
+ lastName: "Doe",
368
+ timezone: "America/New_York",
369
+ },
370
+ },
371
+ };
372
+ expect(() => userResponseSchema.parse(validResponse)).not.toThrow();
373
+ });
374
+ });
375
+ describe("tasksResponseSchema", () => {
376
+ test("should accept valid tasks response", () => {
377
+ const validResponse = {
378
+ tasks: [
379
+ {
380
+ _id: "task-123",
381
+ title: "Test Task",
382
+ status: "active",
383
+ createdAt: "2024-01-15T10:00:00Z",
384
+ updatedAt: "2024-01-15T11:00:00Z",
385
+ userId: "user-123",
386
+ groupId: "group-123",
387
+ },
388
+ ],
389
+ count: 1,
390
+ };
391
+ expect(() => tasksResponseSchema.parse(validResponse)).not.toThrow();
392
+ });
393
+ test("should accept empty tasks response", () => {
394
+ const emptyResponse = {
395
+ tasks: [],
396
+ count: 0,
397
+ };
398
+ expect(() => tasksResponseSchema.parse(emptyResponse)).not.toThrow();
399
+ });
400
+ });
401
+ describe("streamsResponseSchema", () => {
402
+ test("should accept valid streams response", () => {
403
+ const validResponse = {
404
+ streams: [
405
+ {
406
+ _id: "stream-123",
407
+ name: "Test Stream",
408
+ groupId: "group-123",
409
+ isActive: true,
410
+ createdAt: "2024-01-15T10:00:00Z",
411
+ updatedAt: "2024-01-15T11:00:00Z",
412
+ },
413
+ ],
414
+ count: 1,
415
+ };
416
+ expect(() => streamsResponseSchema.parse(validResponse)).not.toThrow();
417
+ });
418
+ });
419
+ describe("errorResponseSchema", () => {
420
+ test("should accept valid error response", () => {
421
+ const validError = {
422
+ error: "ValidationError",
423
+ message: "Invalid input provided",
424
+ code: "400",
425
+ };
426
+ expect(() => errorResponseSchema.parse(validError)).not.toThrow();
427
+ });
428
+ test("should accept error without optional code", () => {
429
+ const minimalError = {
430
+ error: "ValidationError",
431
+ message: "Invalid input provided",
432
+ };
433
+ expect(() => errorResponseSchema.parse(minimalError)).not.toThrow();
434
+ });
435
+ });
436
+ });
437
+ describe("Edge Cases and Error Handling", () => {
438
+ test("should handle null values appropriately", () => {
439
+ // updateTaskBacklogSchema should accept all parameters as optional except taskId
440
+ expect(() => updateTaskBacklogSchema.parse({
441
+ taskId: "task-123"
442
+ })).not.toThrow();
443
+ // Other schemas should reject null where not expected
444
+ expect(() => getTasksByDaySchema.parse({ day: null })).toThrow();
445
+ expect(() => createTaskSchema.parse({ text: null })).toThrow();
446
+ });
447
+ test("should handle undefined values appropriately", () => {
448
+ // Optional fields should accept undefined
449
+ expect(() => getTasksByDaySchema.parse({
450
+ day: "2024-01-15",
451
+ timezone: undefined,
452
+ completionFilter: undefined,
453
+ })).not.toThrow();
454
+ // Required fields should reject undefined
455
+ expect(() => getTasksByDaySchema.parse({ day: undefined })).toThrow();
456
+ expect(() => createTaskSchema.parse({ text: undefined })).toThrow();
457
+ });
458
+ test("should handle type coercion correctly", () => {
459
+ // Numbers as strings should be rejected where numbers are expected
460
+ expect(() => getArchivedTasksSchema.parse({ offset: "0", limit: "100" })).toThrow();
461
+ // Boolean strings should be rejected where booleans are expected
462
+ expect(() => createTaskSchema.parse({ text: "Test", private: "true" })).toThrow();
463
+ });
464
+ test("should validate string formats correctly", () => {
465
+ // Email validation
466
+ expect(() => userProfileSchema.parse({
467
+ _id: "user-123",
468
+ email: "@example.com",
469
+ firstName: "John",
470
+ lastName: "Doe",
471
+ timezone: "America/New_York",
472
+ })).toThrow();
473
+ // URL validation
474
+ expect(() => userProfileSchema.parse({
475
+ _id: "user-123",
476
+ email: "test@example.com",
477
+ firstName: "John",
478
+ lastName: "Doe",
479
+ timezone: "America/New_York",
480
+ avatarUrl: "not-a-url",
481
+ })).toThrow();
482
+ // Date regex validation
483
+ expect(() => getTasksByDaySchema.parse({ day: "2024-13-01" })).not.toThrow(); // Regex allows invalid month/day numbers
484
+ expect(() => getTasksByDaySchema.parse({ day: "24-01-01" })).toThrow(); // But rejects wrong format
485
+ });
486
+ });