@critical-path/client 0.20.4 → 0.20.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/src/index.test.ts DELETED
@@ -1,415 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { CriticalPathClient } from './index.js';
3
-
4
- describe('@critical-path/client Tests', () => {
5
- it('instantiates client and accepts custom mock fetch', async () => {
6
- const mockFetch = async (url: string | URL | Request, init?: RequestInit) => {
7
- const urlStr = url.toString();
8
- if (urlStr.endsWith('/projects') && (!init?.method || init.method.toUpperCase() === 'GET')) {
9
- return new Response(JSON.stringify({ projects: [{ id: 'p1', key: 'TEST', name: 'Mock Proj' }] }), { status: 200 });
10
- }
11
- return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 });
12
- };
13
-
14
- const client = new CriticalPathClient({
15
- baseUrl: 'http://localhost:3000/api/critical-path',
16
- fetch: mockFetch as typeof fetch
17
- });
18
-
19
- const projects = await client.getProjects();
20
- expect(projects.length).toBe(1);
21
- expect(projects[0].name).toBe('Mock Proj');
22
- });
23
-
24
- it('fetches workflows and task allowed transitions via client SDK', async () => {
25
- const mockFetch = async (url: string | URL | Request, init?: RequestInit) => {
26
- const urlStr = url.toString();
27
- if (urlStr.endsWith('/workflows')) {
28
- return new Response(JSON.stringify({ workflows: [{ id: 'wf1', name: 'SDK Workflow' }] }), { status: 200 });
29
- }
30
- if (urlStr.endsWith('/tasks/t1/transitions')) {
31
- return new Response(JSON.stringify({
32
- allowedNextStatuses: ['in_progress', 'canceled'],
33
- allowedPreviousStatuses: ['backlog']
34
- }), { status: 200 });
35
- }
36
- return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 });
37
- };
38
-
39
- const client = new CriticalPathClient({
40
- baseUrl: 'http://localhost:3000/api/critical-path',
41
- fetch: mockFetch as typeof fetch
42
- });
43
-
44
- const workflows = await client.getWorkflows();
45
- expect(workflows).toHaveLength(1);
46
- expect(workflows[0].name).toBe('SDK Workflow');
47
-
48
- const transitions = await client.getAllowedTaskTransitions('t1');
49
- expect(transitions).toEqual(['in_progress', 'canceled']);
50
-
51
- const previousTransitions = await client.getAllowedPreviousTaskTransitions('t1');
52
- expect(previousTransitions).toEqual(['backlog']);
53
- });
54
-
55
- it('handles comments and attachments through client SDK methods', async () => {
56
- const mockFetch = async (url: string | URL | Request, init?: RequestInit) => {
57
- const urlStr = url.toString();
58
- const method = init?.method?.toUpperCase() || 'GET';
59
-
60
- if (urlStr.includes('/comments?taskId=t1') && method === 'GET') {
61
- return new Response(JSON.stringify({ comments: [{ id: 'c1', taskId: 't1', content: 'SDK Comment', authorId: 'u1', authorType: 'user' }] }), { status: 200 });
62
- }
63
- if (urlStr.endsWith('/comments') && method === 'POST') {
64
- const body = JSON.parse(init?.body as string);
65
- return new Response(JSON.stringify({ comment: { id: 'c2', ...body } }), { status: 201 });
66
- }
67
- if (urlStr.endsWith('/comments/c1') && method === 'PATCH') {
68
- const body = JSON.parse(init?.body as string);
69
- return new Response(JSON.stringify({ comment: { id: 'c1', content: body.content } }), { status: 200 });
70
- }
71
- if (urlStr.endsWith('/comments/c1/reactions') && method === 'POST') {
72
- const body = JSON.parse(init?.body as string);
73
- return new Response(JSON.stringify({ comment: { id: 'c1', reactions: [body] } }), { status: 200 });
74
- }
75
- if (urlStr.split('?')[0].endsWith('/comments/c1/reactions') && method === 'DELETE') {
76
- return new Response(JSON.stringify({ comment: { id: 'c1', reactions: [] } }), { status: 200 });
77
- }
78
- if (urlStr.endsWith('/comments/c1') && method === 'DELETE') {
79
- return new Response(JSON.stringify({ success: true }), { status: 200 });
80
- }
81
- if (urlStr.includes('/attachments?taskId=t1') && method === 'GET') {
82
- return new Response(JSON.stringify({ attachments: [{ id: 'a1', filename: 'SDK Doc', mimeType: 'text/plain', sizeBytes: 50, url: 'https://cdn.example.com/a1' }] }), { status: 200 });
83
- }
84
- if (urlStr.endsWith('/attachments') && method === 'POST') {
85
- const body = JSON.parse(init?.body as string);
86
- return new Response(JSON.stringify({ attachment: { id: 'a2', ...body } }), { status: 201 });
87
- }
88
- if (urlStr.endsWith('/attachments/upload') && method === 'POST') {
89
- const body = JSON.parse(init?.body as string);
90
- return new Response(JSON.stringify({ attachment: { id: 'a_up', ...body, url: 'https://cdn.example.com/uploaded.png' } }), { status: 201 });
91
- }
92
- if (urlStr.endsWith('/attachments/a1') && method === 'DELETE') {
93
- return new Response(JSON.stringify({ success: true }), { status: 200 });
94
- }
95
- return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 });
96
- };
97
-
98
- const client = new CriticalPathClient({
99
- baseUrl: 'http://localhost:3000/api/critical-path',
100
- fetch: mockFetch as typeof fetch
101
- });
102
-
103
- const comments = await client.getComments('t1');
104
- expect(comments).toHaveLength(1);
105
- expect(comments[0].content).toBe('SDK Comment');
106
-
107
- const createdComment = await client.addComment({ taskId: 't1', content: 'New comment' });
108
- expect(createdComment.id).toBe('c2');
109
-
110
- const updatedComment = await client.updateComment('c1', { content: 'Updated' });
111
- expect(updatedComment.content).toBe('Updated');
112
-
113
- const reacted = await client.addCommentReaction('c1', { emoji: '🙌' });
114
- expect(reacted.reactions).toHaveLength(1);
115
- expect(reacted.reactions?.[0].emoji).toBe('🙌');
116
-
117
- const unreacted = await client.removeCommentReaction('c1', { emoji: '🙌' });
118
- expect(unreacted.reactions).toHaveLength(0);
119
-
120
- const deletedComment = await client.deleteComment('c1');
121
- expect(deletedComment).toBe(true);
122
-
123
- const attachments = await client.getAttachments({ taskId: 't1' });
124
- expect(attachments).toHaveLength(1);
125
- expect(attachments[0].filename).toBe('SDK Doc');
126
-
127
- const createdAtt = await client.createAttachment({ filename: 'file.txt', mimeType: 'text/plain', sizeBytes: 12, url: 'https://example.com/file.txt' });
128
- expect(createdAtt.id).toBe('a2');
129
-
130
- const uploadedAtt = await client.uploadAttachmentFile({ filename: 'uploaded.png', data: 'base64data' });
131
- expect(uploadedAtt.id).toBe('a_up');
132
- expect(uploadedAtt.url).toBe('https://cdn.example.com/uploaded.png');
133
-
134
- const deletedAtt = await client.deleteAttachment('a1');
135
- expect(deletedAtt).toBe(true);
136
- });
137
-
138
- it('handles deliverables CRUD and summary rollups via client SDK', async () => {
139
- const mockDeliverable = {
140
- id: 'd1',
141
- projectId: 'p1',
142
- title: 'Hero Cut 30s',
143
- status: 'planned' as const,
144
- format: 'ProRes 422',
145
- outputUrls: [],
146
- createdAt: '2026-09-04T00:00:00Z',
147
- updatedAt: '2026-09-04T00:00:00Z'
148
- };
149
-
150
- const mockSummary = {
151
- deliverable: mockDeliverable,
152
- totalTasks: 4,
153
- completedTasks: 2,
154
- activeTasks: 2,
155
- progressPercentage: 50,
156
- estimatedHours: 20,
157
- loggedHours: 12
158
- };
159
-
160
- const mockFetch = async (url: string | URL | Request, init?: RequestInit) => {
161
- const urlStr = url.toString();
162
- const method = init?.method?.toUpperCase() || 'GET';
163
-
164
- if (urlStr.includes('/deliverables?projectId=p1') && method === 'GET') {
165
- return new Response(JSON.stringify({ deliverables: [mockDeliverable] }), { status: 200 });
166
- }
167
- if (urlStr.endsWith('/deliverables/d1/summary') && method === 'GET') {
168
- return new Response(JSON.stringify({ summary: mockSummary }), { status: 200 });
169
- }
170
- if (urlStr.endsWith('/deliverables/d1') && method === 'GET') {
171
- return new Response(JSON.stringify({ deliverable: mockDeliverable }), { status: 200 });
172
- }
173
- if (urlStr.endsWith('/deliverables') && method === 'POST') {
174
- const body = JSON.parse(init?.body as string);
175
- return new Response(JSON.stringify({ deliverable: { id: 'd2', ...body } }), { status: 201 });
176
- }
177
- if (urlStr.endsWith('/deliverables/d1') && method === 'PATCH') {
178
- const body = JSON.parse(init?.body as string);
179
- return new Response(JSON.stringify({ deliverable: { ...mockDeliverable, ...body } }), { status: 200 });
180
- }
181
- if (urlStr.endsWith('/deliverables/d1') && method === 'DELETE') {
182
- return new Response(JSON.stringify({ success: true }), { status: 200 });
183
- }
184
- return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 });
185
- };
186
-
187
- const client = new CriticalPathClient({
188
- baseUrl: 'http://localhost:3000/api/critical-path',
189
- fetch: mockFetch as typeof fetch
190
- });
191
-
192
- const list = await client.getDeliverables('p1');
193
- expect(list).toHaveLength(1);
194
- expect(list[0].title).toBe('Hero Cut 30s');
195
-
196
- const single = await client.getDeliverable('d1');
197
- expect(single.id).toBe('d1');
198
-
199
- const summary = await client.getDeliverableSummary('d1');
200
- expect(summary.totalTasks).toBe(4);
201
- expect(summary.progressPercentage).toBe(50);
202
-
203
- const created = await client.createDeliverable({
204
- projectId: 'p1',
205
- title: 'Hero Cut 15s',
206
- format: 'ProRes 422'
207
- });
208
- expect(created.id).toBe('d2');
209
-
210
- const updated = await client.updateDeliverable('d1', { status: 'delivered' });
211
- expect(updated.status).toBe('delivered');
212
-
213
- const deleted = await client.deleteDeliverable('d1');
214
- expect(deleted).toBe(true);
215
- });
216
-
217
- it('retrieves task lifecycle state and dependencies via client SDK', async () => {
218
- const mockState = {
219
- semanticStatus: 'not_started' as const,
220
- isReady: false,
221
- isBlocked: true,
222
- blockingTaskIds: ['task_upstream'],
223
- isOverdue: false,
224
- isUpcoming: false,
225
- isUnplanned: true,
226
- isUnassigned: false,
227
- isStalled: false,
228
- isOverEstimate: false,
229
- isPaceWarning: false,
230
- isDone: false,
231
- isActive: false,
232
- isCancelled: false
233
- };
234
-
235
- const mockFetch = async (url: string | URL | Request) => {
236
- const urlStr = url.toString();
237
- if (urlStr.endsWith('/tasks/t1/lifecycle')) {
238
- return new Response(JSON.stringify({ state: mockState }), { status: 200 });
239
- }
240
- if (urlStr.endsWith('/tasks/t1/dependencies')) {
241
- return new Response(JSON.stringify({
242
- graph: {
243
- taskId: 't1',
244
- upstreamTasks: [{ id: 'task_upstream', title: 'API', status: 'in_progress' }],
245
- downstreamTasks: [],
246
- dependencies: [{ id: 'dep1', taskId: 't1', dependsOnTaskId: 'task_upstream', type: 'blocking' }]
247
- }
248
- }), { status: 200 });
249
- }
250
- return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 });
251
- };
252
-
253
- const client = new CriticalPathClient({
254
- baseUrl: 'http://localhost:3000/api/critical-path',
255
- fetch: mockFetch as typeof fetch
256
- });
257
-
258
- const lifecycle = await client.getTaskLifecycleState('t1');
259
- expect(lifecycle.isBlocked).toBe(true);
260
- expect(lifecycle.isReady).toBe(false);
261
- expect(lifecycle.blockingTaskIds).toEqual(['task_upstream']);
262
-
263
- const deps = await client.getTaskDependencies('t1');
264
- expect(deps.upstreamTasks).toHaveLength(1);
265
- expect(deps.upstreamTasks[0].id).toBe('task_upstream');
266
- expect(deps.dependencies).toHaveLength(1);
267
- });
268
-
269
- it('fetches critical path and timeline ladder through client SDK', async () => {
270
- const mockFetch = async (url: string | URL | Request) => {
271
- const urlStr = url.toString();
272
-
273
- if (urlStr.endsWith('/projects/p1/critical-path')) {
274
- return new Response(JSON.stringify({
275
- analysis: {
276
- projectId: 'p1',
277
- calculatedAt: '2026-09-11T10:00:00Z',
278
- totalDurationHours: 18,
279
- criticalTaskIds: ['t1', 't2'],
280
- tasks: []
281
- }
282
- }), { status: 200 });
283
- }
284
-
285
- if (urlStr.includes('/projects/p1/ladder?level=macro')) {
286
- return new Response(JSON.stringify({
287
- ladder: {
288
- projectId: 'p1',
289
- generatedAt: '2026-09-11T10:00:00Z',
290
- level: 'macro',
291
- macro: {
292
- projectId: 'p1',
293
- projectName: 'Client Project',
294
- totalDurationHours: 18,
295
- criticalPathDurationHours: 18,
296
- overallProgressPercentage: 50,
297
- health: 'on_track',
298
- totalTasks: 2,
299
- completedTasks: 1,
300
- inProgressTasks: 1,
301
- blockedTasks: 0,
302
- totalEstimatedHours: 18,
303
- totalLoggedHours: 9,
304
- phases: []
305
- }
306
- }
307
- }), { status: 200 });
308
- }
309
-
310
- if (urlStr.endsWith('/tasks/t2/ladder')) {
311
- return new Response(JSON.stringify({
312
- taskLadder: {
313
- taskId: 't2',
314
- standard: { id: 't2', title: 'Task 2', status: 'in_progress', priority: 'high' },
315
- concrete: { taskId: 't2', attachments: [], deliverables: [], todos: [], timeEntries: [], dailyEffortDistribution: [], activities: [] }
316
- }
317
- }), { status: 200 });
318
- }
319
-
320
- if (urlStr.includes('/projects/p1/workload')) {
321
- return new Response(JSON.stringify({
322
- workload: {
323
- projectId: 'p1',
324
- startDate: '2026-09-01',
325
- endDate: '2026-09-14',
326
- interval: 'week',
327
- groupBy: 'assignee',
328
- metric: 'blended',
329
- seriesKeys: ['u1', 'u2'],
330
- seriesLabels: { u1: 'Alice', u2: 'Bob' },
331
- buckets: [
332
- { date: '2026-09-01', timestamp: 1788220800000, totalHours: 20, values: { u1: 12, u2: 8 } }
333
- ],
334
- totalHours: 20
335
- }
336
- }), { status: 200 });
337
- }
338
-
339
- return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 });
340
- };
341
-
342
- const client = new CriticalPathClient({
343
- baseUrl: 'http://localhost:3000/api/critical-path',
344
- fetch: mockFetch as typeof fetch
345
- });
346
-
347
- const cpm = await client.calculateCriticalPath('p1');
348
- expect(cpm.totalDurationHours).toBe(18);
349
- expect(cpm.criticalTaskIds).toEqual(['t1', 't2']);
350
-
351
- const ladder = await client.getTimelineLadder('p1', { level: 'macro' });
352
- expect(ladder.macro?.projectName).toBe('Client Project');
353
- expect(ladder.level).toBe('macro');
354
-
355
- const taskLadder = await client.getTaskLadder('t2');
356
- expect(taskLadder.taskId).toBe('t2');
357
- expect(taskLadder.standard.title).toBe('Task 2');
358
-
359
- const workload = await client.getWorkloadDistribution('p1', { interval: 'week', metric: 'blended' });
360
- expect(workload.projectId).toBe('p1');
361
- expect(workload.totalHours).toBe(20);
362
- expect(workload.seriesKeys).toEqual(['u1', 'u2']);
363
- expect(workload.buckets[0].values.u1).toBe(12);
364
- });
365
-
366
- it('posts agent status updates via client.updateStatus', async () => {
367
- let capturedBody: any = null;
368
- const mockFetch = async (url: string | URL | Request, init?: RequestInit) => {
369
- const urlStr = url.toString();
370
- if (urlStr.endsWith('/status') && init?.method === 'POST') {
371
- capturedBody = JSON.parse(init.body as string);
372
- return new Response(JSON.stringify({ success: true, status: capturedBody.status, timestamp: 123456789 }), { status: 200 });
373
- }
374
- return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 });
375
- };
376
-
377
- const client = new CriticalPathClient({
378
- baseUrl: 'http://localhost:3000/api/critical-path',
379
- fetch: mockFetch as typeof fetch
380
- });
381
-
382
- const result = await client.updateStatus('Running unit tests', {
383
- taskId: 't1',
384
- projectId: 'p1',
385
- details: 'vitest run 15 passed'
386
- });
387
-
388
- expect(result.success).toBe(true);
389
- expect(result.status).toBe('Running unit tests');
390
- expect(capturedBody).toEqual({
391
- status: 'Running unit tests',
392
- taskId: 't1',
393
- projectId: 'p1',
394
- details: 'vitest run 15 passed',
395
- isEngaged: true
396
- });
397
- });
398
-
399
- it('resolves delete methods to false on 404 and still throws other errors', async () => {
400
- let status = 404;
401
- const mockFetch = async () =>
402
- new Response(JSON.stringify({ error: status === 404 ? 'Not found' : 'Internal Server Error' }), {
403
- status,
404
- headers: { 'Content-Type': 'application/json' }
405
- });
406
- const client = new CriticalPathClient({ baseUrl: 'http://localhost/api', fetch: mockFetch as typeof fetch });
407
-
408
- expect(await client.deleteTask('missing')).toBe(false);
409
- expect(await client.deleteComment('missing')).toBe(false);
410
-
411
- status = 500;
412
- await expect(client.deleteTask('t1')).rejects.toThrow('Internal Server Error');
413
- });
414
- });
415
-