@critical-path/svelte 0.18.0 → 0.18.2

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,885 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import {
3
- createCriticalPathClient,
4
- ProjectState,
5
- createProjectState,
6
- TaskState,
7
- createTaskState,
8
- WorkflowState,
9
- createWorkflowState,
10
- CommentState,
11
- createCommentState,
12
- AttachmentState,
13
- createAttachmentState,
14
- TaskActivityState,
15
- createTaskActivityState,
16
- DeliverableState,
17
- createDeliverableState,
18
- DeliverableSummaryState,
19
- createDeliverableSummaryState,
20
- KanbanState,
21
- createKanbanState,
22
- TaskTransitionsState,
23
- createTaskTransitionsState,
24
- WebMcpState,
25
- createWebMcpState,
26
- CriticalPathState,
27
- createCriticalPathState,
28
- TimelineLadderState,
29
- createTimelineLadderState,
30
- TaskMetricsState,
31
- createTaskMetricsState,
32
- WorkloadState,
33
- createWorkloadState
34
- } from './index.js';
35
- import type { CriticalPathClient } from '@critical-path/client';
36
- import type {
37
- Project,
38
- Task,
39
- Workflow,
40
- Comment,
41
- Attachment,
42
- Deliverable,
43
- DeliverableSummary,
44
- CriticalPathAnalysis,
45
- TimelineLadder,
46
- WorkloadDistribution
47
- } from '@critical-path/core';
48
-
49
- describe('@critical-path/svelte Svelte 5 Runes Test Suite', () => {
50
- it('exports client factory and Svelte 5 Runes state factories', () => {
51
- expect(createCriticalPathClient).toBeDefined();
52
- expect(createProjectState).toBeDefined();
53
- expect(createTaskState).toBeDefined();
54
- expect(createWorkflowState).toBeDefined();
55
- expect(createCommentState).toBeDefined();
56
- expect(createAttachmentState).toBeDefined();
57
- expect(createTaskActivityState).toBeDefined();
58
- expect(createDeliverableState).toBeDefined();
59
- expect(createDeliverableSummaryState).toBeDefined();
60
- expect(createKanbanState).toBeDefined();
61
- expect(createTaskTransitionsState).toBeDefined();
62
- expect(ProjectState).toBeDefined();
63
- expect(TaskState).toBeDefined();
64
- expect(WorkflowState).toBeDefined();
65
- expect(CommentState).toBeDefined();
66
- expect(AttachmentState).toBeDefined();
67
- expect(TaskActivityState).toBeDefined();
68
- expect(DeliverableState).toBeDefined();
69
- expect(DeliverableSummaryState).toBeDefined();
70
- expect(KanbanState).toBeDefined();
71
- expect(TaskTransitionsState).toBeDefined();
72
- expect(WebMcpState).toBeDefined();
73
- expect(createWebMcpState).toBeDefined();
74
- });
75
-
76
- describe('WorkflowState', () => {
77
- it('fetches and creates workflows', async () => {
78
- const mockWf: Workflow = {
79
- id: 'wf_1',
80
- name: 'Svelte Workflow',
81
- isDefault: true,
82
- defaultStatusKey: 'todo',
83
- statuses: [{ key: 'todo', label: 'To Do', category: 'not_started' }],
84
- transitions: [],
85
- createdAt: '2026-01-01',
86
- updatedAt: '2026-01-01'
87
- };
88
-
89
- const mockClient = {
90
- getWorkflows: vi.fn().mockResolvedValue([mockWf]),
91
- createWorkflow: vi.fn().mockResolvedValue(mockWf)
92
- } as unknown as CriticalPathClient;
93
-
94
- const wfState = createWorkflowState(mockClient);
95
- await wfState.fetch();
96
-
97
- expect(wfState.data).toEqual([mockWf]);
98
- });
99
- });
100
-
101
- describe('ProjectState', () => {
102
- it('initializes with default empty state', () => {
103
- const mockClient = {} as CriticalPathClient;
104
- const projectState = createProjectState(mockClient);
105
-
106
- expect(projectState.data).toEqual([]);
107
- expect(projectState.loading).toBe(false);
108
- expect(projectState.error).toBeNull();
109
- });
110
-
111
- it('fetches projects and updates state reactively', async () => {
112
- const mockProjects: Project[] = [
113
- { id: 'proj_1', name: 'Project 1', key: 'PRJ1', createdAt: '2026-01-01', updatedAt: '2026-01-01' }
114
- ];
115
- const mockClient = {
116
- getProjects: vi.fn().mockResolvedValue(mockProjects)
117
- } as unknown as CriticalPathClient;
118
-
119
- const projectState = new ProjectState(mockClient);
120
- await projectState.fetch();
121
-
122
- expect(mockClient.getProjects).toHaveBeenCalledOnce();
123
- expect(projectState.data).toEqual(mockProjects);
124
- expect(projectState.loading).toBe(false);
125
- expect(projectState.error).toBeNull();
126
- });
127
-
128
- it('creates project optimistically and rolls back on failure', async () => {
129
- const mockCreated: Project = {
130
- id: 'proj_2',
131
- name: 'Project 2',
132
- key: 'PRJ2',
133
- createdAt: '2026-01-01',
134
- updatedAt: '2026-01-01'
135
- };
136
- const mockClient = {
137
- createProject: vi.fn().mockResolvedValue(mockCreated)
138
- } as unknown as CriticalPathClient;
139
-
140
- const projectState = new ProjectState(mockClient);
141
- const res = await projectState.createProject({ name: 'Project 2', key: 'PRJ2' });
142
-
143
- expect(res).toEqual(mockCreated);
144
- expect(projectState.data).toEqual([mockCreated]);
145
-
146
- // Test error rollback
147
- const failingClient = {
148
- createProject: vi.fn().mockRejectedValue(new Error('Network error'))
149
- } as unknown as CriticalPathClient;
150
- const failingState = new ProjectState(failingClient);
151
-
152
- await expect(failingState.createProject({ name: 'Failed Proj', key: 'FAIL' })).rejects.toThrow('Network error');
153
- expect(failingState.data).toEqual([]);
154
- });
155
- });
156
-
157
- describe('TaskState', () => {
158
- it('fetches, creates, updates status, and deletes tasks with optimistic updates', async () => {
159
- const mockTask: Task = {
160
- id: 'task_1',
161
- projectId: 'proj_1',
162
- title: 'Task 1',
163
- status: 'todo',
164
- priority: 'medium',
165
- createdAt: '2026-01-01',
166
- updatedAt: '2026-01-01'
167
- };
168
- const updatedTask: Task = { ...mockTask, status: 'in_progress' };
169
-
170
- const mockClient = {
171
- getTasks: vi.fn().mockResolvedValue([mockTask]),
172
- createTask: vi.fn().mockResolvedValue(mockTask),
173
- updateTask: vi.fn().mockImplementation((taskId: string, updates: Partial<Task>) => Promise.resolve({ ...mockTask, ...updates })),
174
- deleteTask: vi.fn().mockResolvedValue(undefined)
175
- } as unknown as CriticalPathClient;
176
-
177
- const taskState = createTaskState(mockClient, 'proj_1');
178
- await taskState.fetch();
179
-
180
- expect(taskState.data).toEqual([mockTask]);
181
-
182
- // Update status
183
- await taskState.updateTaskStatus('task_1', 'in_progress');
184
- expect(taskState.data[0].status).toBe('in_progress');
185
-
186
- // Update task generic updates (e.g. todos)
187
- await taskState.updateTask('task_1', {
188
- todos: [{ id: 'todo_1', title: 'Checklist item', completed: true }]
189
- });
190
- expect(taskState.data[0].todos).toEqual([{ id: 'todo_1', title: 'Checklist item', completed: true }]);
191
-
192
- // Delete task
193
- await taskState.deleteTask('task_1');
194
- expect(taskState.data).toEqual([]);
195
- });
196
-
197
- it('rolls back task status update on server error', async () => {
198
- const mockTask: Task = {
199
- id: 'task_1',
200
- projectId: 'proj_1',
201
- title: 'Task 1',
202
- status: 'todo',
203
- priority: 'medium',
204
- createdAt: '2026-01-01',
205
- updatedAt: '2026-01-01'
206
- };
207
-
208
- const failingClient = {
209
- getTasks: vi.fn().mockResolvedValue([mockTask]),
210
- updateTask: vi.fn().mockRejectedValue(new Error('Server error'))
211
- } as unknown as CriticalPathClient;
212
-
213
- const taskState = createTaskState(failingClient, 'proj_1');
214
- await taskState.fetch();
215
-
216
- await expect(taskState.updateTaskStatus('task_1', 'in_progress')).rejects.toThrow('Server error');
217
- // Should be rolled back to 'todo'
218
- expect(taskState.data[0].status).toBe('todo');
219
- });
220
-
221
- it('rolls back task deletion on server error', async () => {
222
- const mockTask: Task = {
223
- id: 'task_1',
224
- projectId: 'proj_1',
225
- title: 'Task 1',
226
- status: 'todo',
227
- priority: 'medium',
228
- createdAt: '2026-01-01',
229
- updatedAt: '2026-01-01'
230
- };
231
-
232
- const failingClient = {
233
- getTasks: vi.fn().mockResolvedValue([mockTask]),
234
- deleteTask: vi.fn().mockRejectedValue(new Error('Delete failed'))
235
- } as unknown as CriticalPathClient;
236
-
237
- const taskState = createTaskState(failingClient, 'proj_1');
238
- await taskState.fetch();
239
-
240
- await expect(taskState.deleteTask('task_1')).rejects.toThrow('Delete failed');
241
- // Should restore deleted task
242
- expect(taskState.data).toEqual([mockTask]);
243
- });
244
-
245
- it('resolves in-flight temporary task when updated or deleted before creation completes', async () => {
246
- let resolveCreate: (value: Task) => void;
247
- const createPromise = new Promise<Task>((resolve) => {
248
- resolveCreate = resolve;
249
- });
250
-
251
- const mockClient = {
252
- getTasks: vi.fn().mockResolvedValue([]),
253
- createTask: vi.fn().mockReturnValue(createPromise),
254
- updateTask: vi.fn().mockImplementation((taskId: string, updates: Partial<Task>) =>
255
- Promise.resolve({ id: taskId, projectId: 'proj_1', title: 'Real Task', status: 'todo', priority: 'medium', createdAt: '2026-01-01', updatedAt: '2026-01-01', ...updates } as Task)
256
- ),
257
- deleteTask: vi.fn().mockResolvedValue(undefined)
258
- } as unknown as CriticalPathClient;
259
-
260
- const taskState = createTaskState(mockClient, 'proj_1');
261
- await taskState.fetch();
262
-
263
- // Start creation
264
- const creationAction = taskState.createTask({
265
- projectId: 'proj_1',
266
- title: 'Draft Task',
267
- status: 'todo',
268
- priority: 'medium'
269
- });
270
-
271
- // While in flight, optimistic task with temp_ ID is present
272
- expect(taskState.data.length).toBe(1);
273
- const tempId = taskState.data[0].id;
274
- expect(tempId.startsWith('temp_')).toBe(true);
275
-
276
- // Immediately update using tempId before creation resolves
277
- const updateAction = taskState.updateTask(tempId, { title: 'Renamed Draft Task' });
278
-
279
- // Local optimistic state is updated immediately
280
- expect(taskState.data[0].title).toBe('Renamed Draft Task');
281
-
282
- // Now resolve the server creation
283
- const serverTask: Task = {
284
- id: 'real_task_99',
285
- projectId: 'proj_1',
286
- title: 'Draft Task',
287
- status: 'todo',
288
- priority: 'medium',
289
- createdAt: '2026-01-01',
290
- updatedAt: '2026-01-01'
291
- };
292
- resolveCreate!(serverTask);
293
-
294
- await creationAction;
295
- await updateAction;
296
-
297
- // Verify server update was called with the real task ID, not the temp ID
298
- expect(mockClient.updateTask).toHaveBeenCalledWith('real_task_99', { title: 'Renamed Draft Task' });
299
-
300
- // And deleting with the tempId resolves to real_task_99
301
- await taskState.deleteTask(tempId);
302
- expect(mockClient.deleteTask).toHaveBeenCalledWith('real_task_99');
303
- expect(taskState.data.length).toBe(0);
304
- });
305
- });
306
-
307
- describe('CommentState', () => {
308
- it('fetches, creates, updates, and deletes comments', async () => {
309
- const mockComment = {
310
- id: 'cmt_1',
311
- taskId: 'task_1',
312
- content: 'Root comment',
313
- authorId: 'u1',
314
- authorType: 'user' as const,
315
- createdAt: '2026-01-01',
316
- updatedAt: '2026-01-01'
317
- };
318
- const updatedComment = { ...mockComment, content: 'Updated comment' };
319
- const reactedComment = { ...mockComment, reactions: [{ emoji: '🔥', userId: 'u2', createdAt: '2026-01-01' }] };
320
-
321
- const mockClient = {
322
- getComments: vi.fn().mockResolvedValue([mockComment]),
323
- addComment: vi.fn().mockResolvedValue(mockComment),
324
- updateComment: vi.fn().mockResolvedValue(updatedComment),
325
- deleteComment: vi.fn().mockResolvedValue(true),
326
- addCommentReaction: vi.fn().mockResolvedValue(reactedComment),
327
- removeCommentReaction: vi.fn().mockResolvedValue(mockComment)
328
- } as unknown as CriticalPathClient;
329
-
330
- const commentState = new CommentState(mockClient, 'task_1');
331
- await commentState.fetch();
332
-
333
- expect(commentState.data).toEqual([mockComment]);
334
-
335
- await commentState.updateComment('cmt_1', { content: 'Updated comment' });
336
- expect(commentState.data[0].content).toBe('Updated comment');
337
-
338
- await commentState.addReaction('cmt_1', '🔥');
339
- expect(commentState.data[0].reactions).toHaveLength(1);
340
- expect(commentState.data[0].reactions?.[0].emoji).toBe('🔥');
341
-
342
- await commentState.removeReaction('cmt_1', '🔥');
343
- expect(commentState.data[0].reactions).toBeUndefined();
344
-
345
- await commentState.deleteComment('cmt_1');
346
- expect(commentState.data).toEqual([]);
347
- });
348
- });
349
-
350
- describe('AttachmentState', () => {
351
- it('fetches, creates, and deletes attachments', async () => {
352
- const mockAttachment = {
353
- id: 'att_1',
354
- filename: 'spec.pdf',
355
- mimeType: 'application/pdf',
356
- sizeBytes: 1024,
357
- url: 'https://storage.example.com/spec.pdf',
358
- uploaderId: 'u1',
359
- uploaderType: 'user' as const,
360
- createdAt: '2026-01-01',
361
- updatedAt: '2026-01-01'
362
- };
363
-
364
- const mockClient = {
365
- getAttachments: vi.fn().mockResolvedValue([mockAttachment]),
366
- createAttachment: vi.fn().mockResolvedValue(mockAttachment),
367
- deleteAttachment: vi.fn().mockResolvedValue(true)
368
- } as unknown as CriticalPathClient;
369
-
370
- const attachmentState = new AttachmentState(mockClient, { taskId: 'task_1' });
371
- await attachmentState.fetch();
372
-
373
- expect(attachmentState.data).toEqual([mockAttachment]);
374
-
375
- await attachmentState.deleteAttachment('att_1');
376
- expect(attachmentState.data).toEqual([]);
377
- });
378
-
379
- it('rolls back optimistic deletion if deleteAttachment fails', async () => {
380
- const mockAttachment = {
381
- id: 'att_2',
382
- filename: 'notes.txt',
383
- mimeType: 'text/plain',
384
- sizeBytes: 100,
385
- url: 'https://storage.example.com/notes.txt',
386
- uploaderId: 'u1',
387
- uploaderType: 'user' as const,
388
- createdAt: '2026-01-01',
389
- updatedAt: '2026-01-01'
390
- };
391
-
392
- const mockClient = {
393
- getAttachments: vi.fn().mockResolvedValue([mockAttachment]),
394
- deleteAttachment: vi.fn().mockRejectedValue(new Error('Network error'))
395
- } as unknown as CriticalPathClient;
396
-
397
- const attachmentState = new AttachmentState(mockClient, { taskId: 'task_1' });
398
- await attachmentState.fetch();
399
-
400
- expect(attachmentState.data).toHaveLength(1);
401
- await expect(attachmentState.deleteAttachment('att_2')).rejects.toThrow('Network error');
402
- expect(attachmentState.data).toEqual([mockAttachment]);
403
- });
404
- });
405
-
406
- describe('TaskActivityState', () => {
407
- it('fetches comments and attachments and unifies them into threads with attachments', async () => {
408
- const mockComment: Comment = {
409
- id: 'cmt_1',
410
- taskId: 'task_1',
411
- content: 'Comment with attachment',
412
- authorId: 'u1',
413
- authorType: 'user',
414
- createdAt: '2026-01-01',
415
- updatedAt: '2026-01-01'
416
- };
417
-
418
- const mockAttachment: Attachment = {
419
- id: 'att_1',
420
- taskId: 'task_1',
421
- commentId: 'cmt_1',
422
- filename: 'screenshot.png',
423
- mimeType: 'image/png',
424
- sizeBytes: 2048,
425
- url: 'https://storage.example.com/screenshot.png',
426
- uploaderId: 'u1',
427
- uploaderType: 'user',
428
- createdAt: '2026-01-01',
429
- updatedAt: '2026-01-01'
430
- };
431
-
432
- const mockClient = {
433
- getComments: vi.fn().mockResolvedValue([mockComment]),
434
- getAttachments: vi.fn().mockResolvedValue([mockAttachment]),
435
- addComment: vi.fn().mockResolvedValue(mockComment),
436
- createAttachment: vi.fn().mockResolvedValue(mockAttachment),
437
- deleteComment: vi.fn().mockResolvedValue(true),
438
- deleteAttachment: vi.fn().mockResolvedValue(true)
439
- } as unknown as CriticalPathClient;
440
-
441
- const activityState = createTaskActivityState(mockClient, 'task_1');
442
- await activityState.fetch();
443
-
444
- expect(activityState.comments).toEqual([mockComment]);
445
- expect(activityState.attachments).toEqual([mockAttachment]);
446
- expect(activityState.threads).toHaveLength(1);
447
- expect(activityState.threads[0].attachments).toEqual([mockAttachment]);
448
- expect(activityState.standaloneAttachments).toHaveLength(0);
449
-
450
- await activityState.deleteAttachment('att_1');
451
- expect(activityState.attachments).toEqual([]);
452
- expect(activityState.threads[0].attachments).toEqual([]);
453
- });
454
- });
455
-
456
- describe('DeliverableState', () => {
457
- it('fetches, creates, updates, and deletes deliverables and gets summary', async () => {
458
- const mockDeliverable: Deliverable = {
459
- id: 'deliv_1',
460
- projectId: 'proj_1',
461
- title: 'Commercial Cut 30s',
462
- status: 'planned',
463
- outputUrls: [],
464
- createdAt: '2026-01-01',
465
- updatedAt: '2026-01-01'
466
- };
467
-
468
- const mockSummary: DeliverableSummary = {
469
- deliverable: mockDeliverable,
470
- totalTasks: 2,
471
- completedTasks: 1,
472
- activeTasks: 1,
473
- progressPercentage: 50,
474
- estimatedHours: 10,
475
- loggedHours: 5
476
- };
477
-
478
- const mockClient = {
479
- getDeliverables: vi.fn().mockResolvedValue([mockDeliverable]),
480
- createDeliverable: vi.fn().mockResolvedValue(mockDeliverable),
481
- updateDeliverable: vi.fn().mockResolvedValue({ ...mockDeliverable, status: 'delivered' }),
482
- deleteDeliverable: vi.fn().mockResolvedValue(true),
483
- getDeliverableSummary: vi.fn().mockResolvedValue(mockSummary)
484
- } as unknown as CriticalPathClient;
485
-
486
- const state = createDeliverableState(mockClient, 'proj_1');
487
- await state.fetch();
488
- expect(state.data).toEqual([mockDeliverable]);
489
-
490
- const created = await state.createDeliverable({
491
- projectId: 'proj_1',
492
- title: 'Commercial Cut 30s'
493
- });
494
- expect(created).toEqual(mockDeliverable);
495
-
496
- const updated = await state.updateDeliverable('deliv_1', { status: 'delivered' });
497
- expect(updated.status).toBe('delivered');
498
-
499
- const summary = await state.getSummary('deliv_1');
500
- expect(summary.totalTasks).toBe(2);
501
- expect(summary.progressPercentage).toBe(50);
502
-
503
- await state.deleteDeliverable('deliv_1');
504
- expect(state.data).toEqual([]);
505
- });
506
- });
507
-
508
- describe('WebMcpState', () => {
509
- it('manages WebMCP registration, project scoping, and unregistration', async () => {
510
- const mockDoc: any = {};
511
- const mockWin: any = {};
512
-
513
- const mockClient = {
514
- getTasks: vi.fn().mockResolvedValue([]),
515
- createTask: vi.fn().mockImplementation((input) => Promise.resolve({ id: 't1', ...input }))
516
- } as unknown as CriticalPathClient;
517
-
518
- const mcpState = createWebMcpState(mockClient, {
519
- projectId: 'proj_svelte',
520
- document: mockDoc,
521
- window: mockWin
522
- });
523
-
524
- expect(mcpState.registered).toBe(true);
525
- expect(mcpState.activeProjectId).toBe('proj_svelte');
526
- expect(mcpState.tools.length).toBeGreaterThan(0);
527
- expect(mockDoc.modelContext).toBeDefined();
528
-
529
- // Execute tool via modelContext
530
- const created = await mockDoc.modelContext.executeTool('create_task', {
531
- title: 'Svelte 5 Task'
532
- });
533
- expect(created.projectId).toBe('proj_svelte');
534
- expect(created.title).toBe('Svelte 5 Task');
535
-
536
- // Update project ID dynamically
537
- mcpState.setProjectId('proj_svelte_2');
538
- expect(mcpState.activeProjectId).toBe('proj_svelte_2');
539
- expect(mcpState.registered).toBe(true);
540
-
541
- const created2 = await mockDoc.modelContext.executeTool('create_task', {
542
- title: 'Svelte 5 Task 2'
543
- });
544
- expect(created2.projectId).toBe('proj_svelte_2');
545
-
546
- // Unregister
547
- mcpState.unregister();
548
- expect(mcpState.registered).toBe(false);
549
- expect(mcpState.tools).toEqual([]);
550
- });
551
- });
552
-
553
- describe('KanbanState', () => {
554
- it('initializes and buckets tasks into workflow and semantic columns', async () => {
555
- const mockTasks: Task[] = [
556
- {
557
- id: 't_todo',
558
- projectId: 'p1',
559
- title: 'To Do Task',
560
- status: 'todo',
561
- priority: 'high',
562
- createdAt: '2026-01-01',
563
- updatedAt: '2026-01-01'
564
- },
565
- {
566
- id: 't_done',
567
- projectId: 'p1',
568
- title: 'Done Task',
569
- status: 'done',
570
- priority: 'low',
571
- createdAt: '2026-01-01',
572
- updatedAt: '2026-01-01'
573
- }
574
- ];
575
-
576
- const mockClient = {
577
- getTasks: vi.fn().mockResolvedValue(mockTasks),
578
- updateTask: vi.fn().mockImplementation((id, updates) => Promise.resolve({ ...mockTasks.find((t) => t.id === id), ...updates }))
579
- } as unknown as CriticalPathClient;
580
-
581
- // Workflow grouping
582
- const kanban = createKanbanState(mockClient, 'p1');
583
- await kanban.fetch();
584
-
585
- expect(kanban.columns.todo).toHaveLength(1);
586
- expect(kanban.columns.done).toHaveLength(1);
587
- expect(kanban.columns.in_progress).toHaveLength(0);
588
-
589
- // Move task
590
- const updated = await kanban.moveTask('t_todo', 'in_progress');
591
- expect(updated.status).toBe('in_progress');
592
-
593
- // Semantic grouping
594
- const semanticKanban = createKanbanState(mockClient, 'p1', { groupBy: 'semantic' });
595
- await semanticKanban.fetch();
596
- expect(semanticKanban.columns.not_started).toHaveLength(1);
597
- expect(semanticKanban.columns.completed).toHaveLength(1);
598
- });
599
- });
600
-
601
- describe('TaskTransitionsState', () => {
602
- it('fetches allowed transitions for a task', async () => {
603
- const mockClient = {
604
- getAllowedTaskTransitions: vi.fn().mockResolvedValue(['in_progress', 'canceled'])
605
- } as unknown as CriticalPathClient;
606
-
607
- const transitionsState = createTaskTransitionsState(mockClient, 'task_123');
608
- expect(transitionsState.allowedTransitions).toEqual([]);
609
-
610
- await transitionsState.fetch();
611
- expect(mockClient.getAllowedTaskTransitions).toHaveBeenCalledWith('task_123');
612
- expect(transitionsState.allowedTransitions).toEqual(['in_progress', 'canceled']);
613
- });
614
- });
615
-
616
- describe('DeliverableSummaryState', () => {
617
- it('fetches deliverable summary reactively', async () => {
618
- const mockDeliverable: Deliverable = {
619
- id: 'deliv_1',
620
- projectId: 'proj_1',
621
- title: 'MVP Launch',
622
- status: 'planned',
623
- createdAt: '2026-01-01',
624
- updatedAt: '2026-01-01'
625
- };
626
-
627
- const mockSummary: DeliverableSummary = {
628
- deliverable: mockDeliverable,
629
- totalTasks: 4,
630
- completedTasks: 3,
631
- activeTasks: 1,
632
- progressPercentage: 75,
633
- estimatedHours: 40,
634
- loggedHours: 30
635
- };
636
-
637
- const mockClient = {
638
- getDeliverableSummary: vi.fn().mockResolvedValue(mockSummary)
639
- } as unknown as CriticalPathClient;
640
-
641
- const summaryState = createDeliverableSummaryState(mockClient, 'deliv_1');
642
- expect(summaryState.summary).toBeNull();
643
- expect(summaryState.data).toBeNull();
644
-
645
- await summaryState.fetch();
646
- expect(mockClient.getDeliverableSummary).toHaveBeenCalledWith('deliv_1');
647
- expect(summaryState.summary).toEqual(mockSummary);
648
- expect(summaryState.data).toEqual(mockSummary);
649
- });
650
- });
651
-
652
- describe('CriticalPathState', () => {
653
- it('manages loading, error, and critical path analysis data', async () => {
654
- const mockAnalysis: CriticalPathAnalysis = {
655
- projectId: 'proj_cpm',
656
- calculatedAt: '2026-09-11T12:00:00Z',
657
- totalDurationHours: 42,
658
- criticalTaskIds: ['t1', 't2'],
659
- tasks: []
660
- };
661
-
662
- const mockClient = {
663
- calculateCriticalPath: vi.fn().mockResolvedValue(mockAnalysis)
664
- } as unknown as CriticalPathClient;
665
-
666
- const cpmState = createCriticalPathState(mockClient, 'proj_cpm');
667
- expect(cpmState.data).toBeNull();
668
- expect(cpmState.loading).toBe(false);
669
-
670
- await cpmState.fetch();
671
- expect(mockClient.calculateCriticalPath).toHaveBeenCalledWith('proj_cpm');
672
- expect(cpmState.data).toEqual(mockAnalysis);
673
- expect(cpmState.loading).toBe(false);
674
- });
675
- });
676
-
677
- describe('TimelineLadderState', () => {
678
- it('manages multi-scale ladder data, levels, and derived rungs', async () => {
679
- const mockLadder: TimelineLadder = {
680
- projectId: 'proj_ladder',
681
- generatedAt: '2026-09-11T12:00:00Z',
682
- level: 'all',
683
- macro: {
684
- projectId: 'proj_ladder',
685
- projectName: 'Ladder Proj',
686
- totalDurationHours: 50,
687
- criticalPathDurationHours: 40,
688
- overallProgressPercentage: 60,
689
- health: 'on_track',
690
- totalTasks: 5,
691
- completedTasks: 3,
692
- inProgressTasks: 2,
693
- blockedTasks: 0,
694
- totalEstimatedHours: 50,
695
- totalLoggedHours: 25,
696
- phases: []
697
- },
698
- standard: {
699
- tasks: [],
700
- criticalPathTaskIds: [],
701
- dependencies: [],
702
- totalDurationHours: 40
703
- },
704
- concrete: {}
705
- };
706
-
707
- const mockClient = {
708
- getTimelineLadder: vi.fn().mockResolvedValue(mockLadder)
709
- } as unknown as CriticalPathClient;
710
-
711
- const ladderState = createTimelineLadderState(mockClient, 'proj_ladder', { level: 'all' });
712
- expect(ladderState.data).toBeNull();
713
- expect(ladderState.macro).toBeNull();
714
- expect(ladderState.standard).toBeNull();
715
- expect(ladderState.concrete).toBeNull();
716
-
717
- await ladderState.fetch();
718
- expect(mockClient.getTimelineLadder).toHaveBeenCalledWith('proj_ladder', { level: 'all' });
719
- expect(ladderState.data).toEqual(mockLadder);
720
- expect(ladderState.macro?.projectName).toBe('Ladder Proj');
721
- expect(ladderState.standard?.tasks).toEqual([]);
722
- expect(ladderState.concrete).toEqual({});
723
-
724
- await ladderState.setLevel('macro');
725
- expect(ladderState.level).toBe('macro');
726
- expect(mockClient.getTimelineLadder).toHaveBeenCalledWith('proj_ladder', { level: 'macro' });
727
- });
728
- });
729
-
730
- describe('TaskMetricsState', () => {
731
- it('fetches metrics and progress history and derives subfields', async () => {
732
- const mockMetrics = {
733
- taskId: 'task_1',
734
- inferredActuals: {
735
- actualStartDate: '2026-09-01T00:00:00Z',
736
- actualEndDate: '2026-09-05T00:00:00Z',
737
- isStartDateInferred: false,
738
- isEndDateInferred: true,
739
- activeWorkingHours: 96
740
- },
741
- realityDelta: {
742
- estimatedHours: 20,
743
- loggedHours: 15,
744
- varianceHours: -5,
745
- effortVarianceHours: -5,
746
- accuracyRatio: 0.75,
747
- isOverdue: false,
748
- isOverEstimate: false
749
- },
750
- progress: {
751
- progressPercentage: 75,
752
- source: 'explicit' as const,
753
- isExplicit: true,
754
- breakdown: { explicitProgress: 75 }
755
- },
756
- evm: {
757
- plannedValue: 20,
758
- earnedValue: 15,
759
- actualCost: 15,
760
- costVariance: 0,
761
- scheduleVariance: -5,
762
- costPerformanceIndex: 1.0,
763
- schedulePerformanceIndex: 0.75
764
- }
765
- };
766
-
767
- const mockHistory = {
768
- taskId: 'task_1',
769
- points: [
770
- { timestamp: '2026-09-01T00:00:00Z', progress: 0, status: 'todo', semanticStatus: 'not_started' as const, action: 'task.created' },
771
- { timestamp: '2026-09-05T00:00:00Z', progress: 75, status: 'in_progress', semanticStatus: 'in_progress' as const, action: 'task.updated' }
772
- ],
773
- curveProfile: 'linear' as const
774
- };
775
-
776
- const mockClient = {
777
- getTaskMetrics: vi.fn().mockResolvedValue(mockMetrics),
778
- getTaskProgressHistory: vi.fn().mockResolvedValue(mockHistory)
779
- } as unknown as CriticalPathClient;
780
-
781
- const state = createTaskMetricsState(mockClient, 'task_1');
782
- expect(state.metrics).toBeNull();
783
- expect(state.history).toBeNull();
784
- expect(state.inferredActuals).toBeNull();
785
- expect(state.realityDelta).toBeNull();
786
- expect(state.progress).toBeNull();
787
- expect(state.evm).toBeNull();
788
- expect(state.curveProfile).toBeNull();
789
-
790
- await state.fetchAll();
791
- expect(mockClient.getTaskMetrics).toHaveBeenCalledWith('task_1');
792
- expect(mockClient.getTaskProgressHistory).toHaveBeenCalledWith('task_1');
793
- expect(state.metrics).toEqual(mockMetrics);
794
- expect(state.history).toEqual(mockHistory);
795
- expect(state.inferredActuals?.activeWorkingHours).toBe(96);
796
- expect(state.realityDelta?.effortVarianceHours).toBe(-5);
797
- expect(state.progress?.progressPercentage).toBe(75);
798
- expect(state.evm?.costPerformanceIndex).toBe(1.0);
799
- expect(state.curveProfile).toBe('linear');
800
- });
801
- });
802
-
803
- describe('WorkloadState & createWorkloadState', () => {
804
- it('initializes with default options and fetches workload distribution', async () => {
805
- const mockDistribution: WorkloadDistribution = {
806
- projectId: 'proj_wl',
807
- startDate: '2026-09-01',
808
- endDate: '2026-09-14',
809
- interval: 'week',
810
- groupBy: 'assignee',
811
- metric: 'blended',
812
- seriesKeys: ['u1', 'u2'],
813
- seriesLabels: { u1: 'Alice', u2: 'Bob' },
814
- buckets: [
815
- {
816
- date: '2026-09-01',
817
- timestamp: 1788220800000,
818
- totalHours: 40,
819
- values: { u1: 25, u2: 15 },
820
- capacity: { u1: 35, u2: 40 },
821
- totalCapacity: 75,
822
- utilizationRatio: 0.533
823
- }
824
- ],
825
- totalHours: 40,
826
- totalCapacity: 75,
827
- averageUtilization: 0.533
828
- };
829
-
830
- const mockClient = {
831
- getWorkloadDistribution: vi.fn().mockResolvedValue(mockDistribution)
832
- } as unknown as CriticalPathClient;
833
-
834
- const state = createWorkloadState(mockClient, 'proj_wl', {
835
- interval: 'week',
836
- groupBy: 'assignee',
837
- metric: 'blended'
838
- });
839
-
840
- expect(state.data).toBeNull();
841
- expect(state.buckets).toEqual([]);
842
- expect(state.seriesKeys).toEqual([]);
843
- expect(state.totalHours).toBe(0);
844
-
845
- await state.fetch();
846
-
847
- expect(mockClient.getWorkloadDistribution).toHaveBeenCalledWith('proj_wl', {
848
- interval: 'week',
849
- groupBy: 'assignee',
850
- metric: 'blended'
851
- });
852
- expect(state.data).toEqual(mockDistribution);
853
- expect(state.buckets).toHaveLength(1);
854
- expect(state.seriesKeys).toEqual(['u1', 'u2']);
855
- expect(state.totalHours).toBe(40);
856
- expect(state.totalCapacity).toBe(75);
857
- expect(state.averageUtilization).toBe(0.533);
858
- });
859
-
860
- it('updates interval and re-fetches', async () => {
861
- const mockClient = {
862
- getWorkloadDistribution: vi.fn().mockResolvedValue({
863
- buckets: [],
864
- seriesKeys: [],
865
- seriesLabels: {},
866
- totalHours: 0,
867
- totalCapacity: 0
868
- })
869
- } as unknown as CriticalPathClient;
870
-
871
- const state = new WorkloadState(mockClient, 'proj_wl');
872
- expect(state.interval).toBe('week');
873
-
874
- await state.setInterval('day');
875
- expect(state.interval).toBe('day');
876
- expect(mockClient.getWorkloadDistribution).toHaveBeenCalledWith(
877
- 'proj_wl',
878
- expect.objectContaining({ interval: 'day' })
879
- );
880
- });
881
- });
882
- });
883
-
884
-
885
-