@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/package.json +10 -6
- package/CHANGELOG.md +0 -730
- package/dist/cancellation.test.d.ts +0 -2
- package/dist/cancellation.test.d.ts.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/src/activity-state.svelte.ts +0 -203
- package/src/attachment-state.svelte.ts +0 -71
- package/src/cancellation.test.ts +0 -43
- package/src/comment-state.svelte.ts +0 -140
- package/src/critical-path-state.svelte.ts +0 -43
- package/src/deliverable-state.svelte.ts +0 -87
- package/src/deliverable-summary-state.svelte.ts +0 -51
- package/src/index.test.ts +0 -885
- package/src/index.ts +0 -23
- package/src/kanban-state.svelte.ts +0 -113
- package/src/latest-request.ts +0 -24
- package/src/project-state.svelte.ts +0 -64
- package/src/task-metrics-state.svelte.ts +0 -103
- package/src/task-state.svelte.ts +0 -205
- package/src/task-transitions-state.svelte.ts +0 -46
- package/src/timeline-ladder-state.svelte.ts +0 -68
- package/src/webmcp-state.svelte.ts +0 -78
- package/src/workflow-state.svelte.ts +0 -72
- package/src/workload-state.svelte.ts +0 -93
- package/tsconfig.json +0 -8
- package/vite.config.ts +0 -22
- package/vitest.config.ts +0 -6
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
|
-
|