digital-tasks 2.0.2 → 2.1.1

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.
@@ -0,0 +1,407 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { createTaskQueue } from '../src/index.js';
3
+ describe('TaskQueue', () => {
4
+ let queue;
5
+ const createTestFunc = (name) => ({
6
+ type: 'generative',
7
+ name,
8
+ args: {},
9
+ output: 'string',
10
+ });
11
+ beforeEach(() => {
12
+ queue = createTaskQueue({ name: 'test-queue' });
13
+ });
14
+ describe('add() and get()', () => {
15
+ it('should add and retrieve a task', async () => {
16
+ const task = {
17
+ id: 'test_task_1',
18
+ function: createTestFunc('test'),
19
+ status: 'queued',
20
+ priority: 'normal',
21
+ createdAt: new Date(),
22
+ events: [],
23
+ };
24
+ await queue.add(task);
25
+ const retrieved = await queue.get('test_task_1');
26
+ expect(retrieved).toBeDefined();
27
+ expect(retrieved.id).toBe('test_task_1');
28
+ expect(retrieved.events?.length).toBeGreaterThan(0); // Should have 'created' event
29
+ });
30
+ it('should return undefined for non-existent task', async () => {
31
+ const result = await queue.get('non_existent');
32
+ expect(result).toBeUndefined();
33
+ });
34
+ });
35
+ describe('update()', () => {
36
+ it('should update task status', async () => {
37
+ const task = {
38
+ id: 'update_test',
39
+ function: createTestFunc('update'),
40
+ status: 'queued',
41
+ priority: 'normal',
42
+ createdAt: new Date(),
43
+ events: [],
44
+ };
45
+ await queue.add(task);
46
+ const updated = await queue.update('update_test', { status: 'in_progress' });
47
+ expect(updated).toBeDefined();
48
+ expect(updated.status).toBe('in_progress');
49
+ });
50
+ it('should update task priority', async () => {
51
+ const task = {
52
+ id: 'priority_test',
53
+ function: createTestFunc('priority'),
54
+ status: 'queued',
55
+ priority: 'normal',
56
+ createdAt: new Date(),
57
+ events: [],
58
+ };
59
+ await queue.add(task);
60
+ const updated = await queue.update('priority_test', { priority: 'urgent' });
61
+ expect(updated.priority).toBe('urgent');
62
+ });
63
+ it('should update task progress', async () => {
64
+ const task = {
65
+ id: 'progress_test',
66
+ function: createTestFunc('progress'),
67
+ status: 'in_progress',
68
+ priority: 'normal',
69
+ createdAt: new Date(),
70
+ events: [],
71
+ };
72
+ await queue.add(task);
73
+ const updated = await queue.update('progress_test', {
74
+ progress: { percent: 50, step: 'Processing' },
75
+ });
76
+ expect(updated.progress?.percent).toBe(50);
77
+ expect(updated.progress?.step).toBe('Processing');
78
+ expect(updated.progress?.updatedAt).toBeInstanceOf(Date);
79
+ });
80
+ it('should add events to task history', async () => {
81
+ const task = {
82
+ id: 'event_test',
83
+ function: createTestFunc('event'),
84
+ status: 'queued',
85
+ priority: 'normal',
86
+ createdAt: new Date(),
87
+ events: [],
88
+ };
89
+ await queue.add(task);
90
+ await queue.update('event_test', {
91
+ event: {
92
+ type: 'comment',
93
+ message: 'Test comment',
94
+ },
95
+ });
96
+ const updated = await queue.get('event_test');
97
+ const commentEvent = updated.events?.find(e => e.type === 'comment');
98
+ expect(commentEvent).toBeDefined();
99
+ expect(commentEvent.message).toBe('Test comment');
100
+ });
101
+ it('should return undefined for non-existent task', async () => {
102
+ const result = await queue.update('non_existent', { status: 'completed' });
103
+ expect(result).toBeUndefined();
104
+ });
105
+ });
106
+ describe('remove()', () => {
107
+ it('should remove a task', async () => {
108
+ const task = {
109
+ id: 'remove_test',
110
+ function: createTestFunc('remove'),
111
+ status: 'queued',
112
+ priority: 'normal',
113
+ createdAt: new Date(),
114
+ events: [],
115
+ };
116
+ await queue.add(task);
117
+ const removed = await queue.remove('remove_test');
118
+ expect(removed).toBe(true);
119
+ const retrieved = await queue.get('remove_test');
120
+ expect(retrieved).toBeUndefined();
121
+ });
122
+ it('should return false for non-existent task', async () => {
123
+ const result = await queue.remove('non_existent');
124
+ expect(result).toBe(false);
125
+ });
126
+ });
127
+ describe('query()', () => {
128
+ beforeEach(async () => {
129
+ // Add test tasks
130
+ const tasks = [
131
+ {
132
+ id: 'query_1',
133
+ function: createTestFunc('task1'),
134
+ status: 'queued',
135
+ priority: 'high',
136
+ tags: ['frontend'],
137
+ projectId: 'proj_1',
138
+ createdAt: new Date('2024-01-01'),
139
+ events: [],
140
+ },
141
+ {
142
+ id: 'query_2',
143
+ function: createTestFunc('task2'),
144
+ status: 'in_progress',
145
+ priority: 'normal',
146
+ tags: ['backend'],
147
+ projectId: 'proj_1',
148
+ createdAt: new Date('2024-01-02'),
149
+ events: [],
150
+ },
151
+ {
152
+ id: 'query_3',
153
+ function: createTestFunc('task3'),
154
+ status: 'completed',
155
+ priority: 'low',
156
+ tags: ['frontend', 'ui'],
157
+ projectId: 'proj_2',
158
+ createdAt: new Date('2024-01-03'),
159
+ events: [],
160
+ },
161
+ ];
162
+ for (const task of tasks) {
163
+ await queue.add(task);
164
+ }
165
+ });
166
+ it('should filter by status', async () => {
167
+ const results = await queue.query({ status: 'queued' });
168
+ expect(results).toHaveLength(1);
169
+ expect(results[0].id).toBe('query_1');
170
+ });
171
+ it('should filter by multiple statuses', async () => {
172
+ const results = await queue.query({ status: ['queued', 'in_progress'] });
173
+ expect(results).toHaveLength(2);
174
+ });
175
+ it('should filter by priority', async () => {
176
+ const results = await queue.query({ priority: 'high' });
177
+ expect(results).toHaveLength(1);
178
+ expect(results[0].id).toBe('query_1');
179
+ });
180
+ it('should filter by tags', async () => {
181
+ const results = await queue.query({ tags: ['frontend'] });
182
+ expect(results).toHaveLength(2);
183
+ });
184
+ it('should filter by projectId', async () => {
185
+ const results = await queue.query({ projectId: 'proj_1' });
186
+ expect(results).toHaveLength(2);
187
+ });
188
+ it('should sort by priority descending', async () => {
189
+ const results = await queue.query({ sortBy: 'priority', sortOrder: 'desc' });
190
+ expect(results[0].priority).toBe('high');
191
+ expect(results[results.length - 1].priority).toBe('low');
192
+ });
193
+ it('should sort by createdAt ascending', async () => {
194
+ const results = await queue.query({ sortBy: 'createdAt', sortOrder: 'asc' });
195
+ expect(results[0].id).toBe('query_1');
196
+ expect(results[2].id).toBe('query_3');
197
+ });
198
+ it('should support pagination', async () => {
199
+ const page1 = await queue.query({ limit: 2, offset: 0 });
200
+ expect(page1).toHaveLength(2);
201
+ const page2 = await queue.query({ limit: 2, offset: 2 });
202
+ expect(page2).toHaveLength(1);
203
+ });
204
+ it('should search by name', async () => {
205
+ const results = await queue.query({ search: 'task2' });
206
+ expect(results).toHaveLength(1);
207
+ expect(results[0].id).toBe('query_2');
208
+ });
209
+ });
210
+ describe('getNextForWorker()', () => {
211
+ it('should return highest priority available task', async () => {
212
+ const tasks = [
213
+ {
214
+ id: 'next_1',
215
+ function: createTestFunc('low'),
216
+ status: 'queued',
217
+ priority: 'low',
218
+ createdAt: new Date(),
219
+ events: [],
220
+ },
221
+ {
222
+ id: 'next_2',
223
+ function: createTestFunc('high'),
224
+ status: 'queued',
225
+ priority: 'high',
226
+ createdAt: new Date(),
227
+ events: [],
228
+ },
229
+ ];
230
+ for (const task of tasks) {
231
+ await queue.add(task);
232
+ }
233
+ const worker = { type: 'agent', id: 'agent_1' };
234
+ const next = await queue.getNextForWorker(worker);
235
+ expect(next).toBeDefined();
236
+ expect(next.id).toBe('next_2'); // Higher priority
237
+ });
238
+ it('should respect allowedWorkers', async () => {
239
+ const task = {
240
+ id: 'worker_type_test',
241
+ function: { type: 'human', name: 'human-task', args: {}, output: 'object', instructions: 'Do something' },
242
+ status: 'queued',
243
+ priority: 'high',
244
+ allowedWorkers: ['human'],
245
+ createdAt: new Date(),
246
+ events: [],
247
+ };
248
+ await queue.add(task);
249
+ const agentWorker = { type: 'agent', id: 'agent_1' };
250
+ const agentNext = await queue.getNextForWorker(agentWorker);
251
+ expect(agentNext).toBeUndefined();
252
+ const humanWorker = { type: 'human', id: 'human_1' };
253
+ const humanNext = await queue.getNextForWorker(humanWorker);
254
+ expect(humanNext).toBeDefined();
255
+ expect(humanNext.id).toBe('worker_type_test');
256
+ });
257
+ it('should skip scheduled tasks not yet due', async () => {
258
+ const futureDate = new Date(Date.now() + 60000); // 1 minute in future
259
+ const task = {
260
+ id: 'scheduled_test',
261
+ function: createTestFunc('scheduled'),
262
+ status: 'pending',
263
+ priority: 'high',
264
+ scheduledFor: futureDate,
265
+ createdAt: new Date(),
266
+ events: [],
267
+ };
268
+ await queue.add(task);
269
+ const worker = { type: 'agent', id: 'agent_1' };
270
+ const next = await queue.getNextForWorker(worker);
271
+ expect(next).toBeUndefined();
272
+ });
273
+ it('should skip blocked tasks', async () => {
274
+ const task = {
275
+ id: 'blocked_test',
276
+ function: createTestFunc('blocked'),
277
+ status: 'queued',
278
+ priority: 'high',
279
+ dependencies: [{ type: 'blocked_by', taskId: 'other_task', satisfied: false }],
280
+ createdAt: new Date(),
281
+ events: [],
282
+ };
283
+ await queue.add(task);
284
+ const worker = { type: 'agent', id: 'agent_1' };
285
+ const next = await queue.getNextForWorker(worker);
286
+ expect(next).toBeUndefined();
287
+ });
288
+ });
289
+ describe('claim()', () => {
290
+ it('should claim a task and assign it to worker', async () => {
291
+ const task = {
292
+ id: 'claim_test',
293
+ function: createTestFunc('claim'),
294
+ status: 'queued',
295
+ priority: 'normal',
296
+ createdAt: new Date(),
297
+ events: [],
298
+ };
299
+ await queue.add(task);
300
+ const worker = { type: 'agent', id: 'agent_1', name: 'Agent 1' };
301
+ const claimed = await queue.claim('claim_test', worker);
302
+ expect(claimed).toBe(true);
303
+ const updated = await queue.get('claim_test');
304
+ expect(updated.status).toBe('assigned');
305
+ expect(updated.assignment?.worker).toEqual(worker);
306
+ });
307
+ it('should not claim already assigned task', async () => {
308
+ const task = {
309
+ id: 'claim_assigned',
310
+ function: createTestFunc('claim'),
311
+ status: 'assigned',
312
+ priority: 'normal',
313
+ assignment: {
314
+ worker: { type: 'agent', id: 'other_agent' },
315
+ assignedAt: new Date(),
316
+ },
317
+ createdAt: new Date(),
318
+ events: [],
319
+ };
320
+ await queue.add(task);
321
+ const worker = { type: 'agent', id: 'agent_1' };
322
+ const claimed = await queue.claim('claim_assigned', worker);
323
+ expect(claimed).toBe(false);
324
+ });
325
+ it('should return false for non-existent task', async () => {
326
+ const worker = { type: 'agent', id: 'agent_1' };
327
+ const claimed = await queue.claim('non_existent', worker);
328
+ expect(claimed).toBe(false);
329
+ });
330
+ });
331
+ describe('complete()', () => {
332
+ it('should mark task as completed', async () => {
333
+ const task = {
334
+ id: 'complete_test',
335
+ function: createTestFunc('complete'),
336
+ status: 'in_progress',
337
+ priority: 'normal',
338
+ createdAt: new Date(),
339
+ events: [],
340
+ };
341
+ await queue.add(task);
342
+ await queue.complete('complete_test', 'Result data');
343
+ const updated = await queue.get('complete_test');
344
+ expect(updated.status).toBe('completed');
345
+ });
346
+ it('should satisfy dependencies in other tasks', async () => {
347
+ const task1 = {
348
+ id: 'dep_complete_1',
349
+ function: createTestFunc('task1'),
350
+ status: 'in_progress',
351
+ priority: 'normal',
352
+ createdAt: new Date(),
353
+ events: [],
354
+ };
355
+ const task2 = {
356
+ id: 'dep_complete_2',
357
+ function: createTestFunc('task2'),
358
+ status: 'blocked',
359
+ priority: 'normal',
360
+ dependencies: [{ type: 'blocked_by', taskId: 'dep_complete_1', satisfied: false }],
361
+ createdAt: new Date(),
362
+ events: [],
363
+ };
364
+ await queue.add(task1);
365
+ await queue.add(task2);
366
+ await queue.complete('dep_complete_1', 'done');
367
+ const updated = await queue.get('dep_complete_2');
368
+ expect(updated.dependencies[0].satisfied).toBe(true);
369
+ expect(updated.status).toBe('queued'); // Unblocked
370
+ });
371
+ });
372
+ describe('fail()', () => {
373
+ it('should mark task as failed', async () => {
374
+ const task = {
375
+ id: 'fail_test',
376
+ function: createTestFunc('fail'),
377
+ status: 'in_progress',
378
+ priority: 'normal',
379
+ createdAt: new Date(),
380
+ events: [],
381
+ };
382
+ await queue.add(task);
383
+ await queue.fail('fail_test', 'Error message');
384
+ const updated = await queue.get('fail_test');
385
+ expect(updated.status).toBe('failed');
386
+ });
387
+ });
388
+ describe('stats()', () => {
389
+ it('should return queue statistics', async () => {
390
+ const tasks = [
391
+ { id: 's1', function: createTestFunc('t1'), status: 'queued', priority: 'high', createdAt: new Date(), events: [] },
392
+ { id: 's2', function: createTestFunc('t2'), status: 'queued', priority: 'normal', createdAt: new Date(), events: [] },
393
+ { id: 's3', function: createTestFunc('t3'), status: 'completed', priority: 'low', createdAt: new Date(), events: [] },
394
+ ];
395
+ for (const task of tasks) {
396
+ await queue.add(task);
397
+ }
398
+ const stats = await queue.stats();
399
+ expect(stats.total).toBe(3);
400
+ expect(stats.byStatus.queued).toBe(2);
401
+ expect(stats.byStatus.completed).toBe(1);
402
+ expect(stats.byPriority.high).toBe(1);
403
+ expect(stats.byPriority.normal).toBe(1);
404
+ expect(stats.byPriority.low).toBe(1);
405
+ });
406
+ });
407
+ });