@forgehive/task 0.2.3 → 0.2.4

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.
Files changed (72) hide show
  1. package/dist/index.d.ts +9 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +38 -9
  4. package/dist/index.js.map +1 -1
  5. package/dist/test/add-listener-with-boundaries.test.js +78 -7
  6. package/dist/test/add-listener-with-boundaries.test.js.map +1 -1
  7. package/dist/test/add-listener.test.js +36 -0
  8. package/dist/test/add-listener.test.js.map +1 -1
  9. package/dist/test/boundary-modes.test.js +45 -5
  10. package/dist/test/boundary-modes.test.js.map +1 -1
  11. package/dist/test/execution-record-boundaries.test.js +12 -2
  12. package/dist/test/execution-record-boundaries.test.js.map +1 -1
  13. package/dist/test/integration-enhanced-records.test.d.ts +2 -0
  14. package/dist/test/integration-enhanced-records.test.d.ts.map +1 -0
  15. package/dist/test/integration-enhanced-records.test.js +467 -0
  16. package/dist/test/integration-enhanced-records.test.js.map +1 -0
  17. package/dist/test/metrics-collection.test.d.ts +2 -0
  18. package/dist/test/metrics-collection.test.d.ts.map +1 -0
  19. package/dist/test/metrics-collection.test.js +409 -0
  20. package/dist/test/metrics-collection.test.js.map +1 -0
  21. package/dist/test/performance-edge-cases.test.d.ts +2 -0
  22. package/dist/test/performance-edge-cases.test.d.ts.map +1 -0
  23. package/dist/test/performance-edge-cases.test.js +502 -0
  24. package/dist/test/performance-edge-cases.test.js.map +1 -0
  25. package/dist/test/run-boundary.test.js +27 -3
  26. package/dist/test/run-boundary.test.js.map +1 -1
  27. package/dist/test/safe-replay-complex-boundary.test.js +110 -9
  28. package/dist/test/safe-replay-complex-boundary.test.js.map +1 -1
  29. package/dist/test/safe-replay.test.js +35 -5
  30. package/dist/test/safe-replay.test.js.map +1 -1
  31. package/dist/test/safe-run.test.js +46 -4
  32. package/dist/test/safe-run.test.js.map +1 -1
  33. package/dist/test/setmetrics-boundary.test.d.ts +2 -0
  34. package/dist/test/setmetrics-boundary.test.d.ts.map +1 -0
  35. package/dist/test/setmetrics-boundary.test.js +195 -0
  36. package/dist/test/setmetrics-boundary.test.js.map +1 -0
  37. package/dist/test/task-with-boundaries.test.js +63 -2
  38. package/dist/test/task-with-boundaries.test.js.map +1 -1
  39. package/dist/test/timing-capture.test.d.ts +2 -0
  40. package/dist/test/timing-capture.test.d.ts.map +1 -0
  41. package/dist/test/timing-capture.test.js +304 -0
  42. package/dist/test/timing-capture.test.js.map +1 -0
  43. package/dist/test/timing-utilities.test.d.ts +2 -0
  44. package/dist/test/timing-utilities.test.d.ts.map +1 -0
  45. package/dist/test/timing-utilities.test.js +127 -0
  46. package/dist/test/timing-utilities.test.js.map +1 -0
  47. package/dist/types.d.ts +93 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +78 -0
  50. package/dist/types.js.map +1 -0
  51. package/dist/utils/boundary.d.ts +3 -0
  52. package/dist/utils/boundary.d.ts.map +1 -1
  53. package/dist/utils/boundary.js +11 -2
  54. package/dist/utils/boundary.js.map +1 -1
  55. package/package.json +3 -2
  56. package/src/index.ts +63 -8
  57. package/src/test/add-listener-with-boundaries.test.ts +78 -7
  58. package/src/test/add-listener.test.ts +36 -0
  59. package/src/test/boundary-modes.test.ts +45 -5
  60. package/src/test/execution-record-boundaries.test.ts +12 -2
  61. package/src/test/metrics-collection.test.ts +476 -0
  62. package/src/test/performance-edge-cases.test.ts +596 -0
  63. package/src/test/run-boundary.test.ts +27 -3
  64. package/src/test/safe-replay-complex-boundary.test.ts +115 -10
  65. package/src/test/safe-replay.test.ts +35 -5
  66. package/src/test/safe-run.test.ts +46 -4
  67. package/src/test/setmetrics-boundary.test.ts +223 -0
  68. package/src/test/task-with-boundaries.test.ts +71 -5
  69. package/src/test/timing-capture.test.ts +348 -0
  70. package/src/test/timing-utilities.test.ts +145 -0
  71. package/src/types.ts +139 -0
  72. package/src/utils/boundary.ts +15 -2
@@ -0,0 +1,476 @@
1
+ import { createTask, validateMetric, createMetric } from '../index'
2
+ import { Schema } from '@forgehive/schema'
3
+
4
+ describe('Metrics Collection Tests', () => {
5
+ describe('setMetrics boundary functionality and validation', () => {
6
+ it('should validate metrics with correct structure', () => {
7
+ const validMetric = { type: 'business', name: 'user_count', value: 42 }
8
+ expect(validateMetric(validMetric)).toBe(true)
9
+
10
+ const performanceMetric = { type: 'performance', name: 'response_time', value: 150.5 }
11
+ expect(validateMetric(performanceMetric)).toBe(true)
12
+
13
+ const errorMetric = { type: 'error', name: 'failed_requests', value: 0 }
14
+ expect(validateMetric(errorMetric)).toBe(true)
15
+ })
16
+
17
+ it('should reject invalid metric structures', () => {
18
+ expect(validateMetric(null)).toBe(false)
19
+ expect(validateMetric(undefined)).toBe(false)
20
+ expect(validateMetric({})).toBe(false)
21
+ expect(validateMetric({ type: 'business' })).toBe(false) // missing name and value
22
+ expect(validateMetric({ name: 'test', value: 1 })).toBe(false) // missing type
23
+ expect(validateMetric({ type: 'business', name: 'test' })).toBe(false) // missing value
24
+ })
25
+
26
+ it('should reject metrics with invalid types', () => {
27
+ expect(validateMetric({ type: '', name: 'test', value: 1 })).toBe(false) // empty type
28
+ expect(validateMetric({ type: 123, name: 'test', value: 1 })).toBe(false) // numeric type
29
+ expect(validateMetric({ type: 'business', name: '', value: 1 })).toBe(false) // empty name
30
+ expect(validateMetric({ type: 'business', name: 123, value: 1 })).toBe(false) // numeric name
31
+ expect(validateMetric({ type: 'business', name: 'test', value: 'not-a-number' })).toBe(false) // string value
32
+ expect(validateMetric({ type: 'business', name: 'test', value: NaN })).toBe(false) // NaN value
33
+ expect(validateMetric({ type: 'business', name: 'test', value: Infinity })).toBe(false) // Infinity value
34
+ })
35
+
36
+ it('should create valid metrics using createMetric helper', () => {
37
+ const metric = createMetric('performance', 'api_response_time', 250)
38
+ expect(metric).toEqual({
39
+ type: 'performance',
40
+ name: 'api_response_time',
41
+ value: 250
42
+ })
43
+ expect(validateMetric(metric)).toBe(true)
44
+ })
45
+
46
+ it('should throw error when creating metric with invalid data', () => {
47
+ expect(() => createMetric('', 'test', 1)).toThrow('Invalid metric')
48
+ expect(() => createMetric('business', '', 1)).toThrow('Invalid metric')
49
+ expect(() => createMetric('business', 'test', NaN)).toThrow('Invalid metric')
50
+ expect(() => createMetric('business', 'test', Infinity)).toThrow('Invalid metric')
51
+ })
52
+ })
53
+
54
+ describe('Metrics accumulation and storage in execution records', () => {
55
+ it('should collect single metric in execution record', async () => {
56
+ const task = createTask({
57
+ name: 'single-metric-test',
58
+ schema: new Schema({ input: Schema.string() }),
59
+ boundaries: {},
60
+ fn: async ({ input }, { setMetrics }) => {
61
+ await setMetrics({
62
+ type: 'business',
63
+ name: 'items_processed',
64
+ value: 1
65
+ })
66
+ return { result: input.toUpperCase() }
67
+ }
68
+ })
69
+
70
+ const [result, error, record] = await task.safeRun({ input: 'test' })
71
+
72
+ expect(error).toBeNull()
73
+ expect(result).toEqual({ result: 'TEST' })
74
+ expect(record.metrics).toHaveLength(1)
75
+ expect(record.metrics?.[0]).toEqual({
76
+ type: 'business',
77
+ name: 'items_processed',
78
+ value: 1
79
+ })
80
+ })
81
+
82
+ it('should accumulate multiple metrics in execution record', async () => {
83
+ const task = createTask({
84
+ name: 'multiple-metrics-test',
85
+ schema: new Schema({ count: Schema.number() }),
86
+ boundaries: {},
87
+ fn: async ({ count }, { setMetrics }) => {
88
+ await setMetrics({ type: 'business', name: 'input_count', value: count })
89
+ await setMetrics({ type: 'performance', name: 'processing_time', value: 150 })
90
+ await setMetrics({ type: 'error', name: 'error_count', value: 0 })
91
+
92
+ return { processed: count * 2 }
93
+ }
94
+ })
95
+
96
+ const [result, error, record] = await task.safeRun({ count: 5 })
97
+
98
+ expect(error).toBeNull()
99
+ expect(result).toEqual({ processed: 10 })
100
+ expect(record.metrics).toHaveLength(3)
101
+
102
+ expect(record.metrics).toEqual(expect.arrayContaining([
103
+ { type: 'business', name: 'input_count', value: 5 },
104
+ { type: 'performance', name: 'processing_time', value: 150 },
105
+ { type: 'error', name: 'error_count', value: 0 }
106
+ ]))
107
+ })
108
+
109
+ it('should allow duplicate metric names with different values', async () => {
110
+ const task = createTask({
111
+ name: 'duplicate-metrics-test',
112
+ schema: new Schema({ iterations: Schema.number() }),
113
+ boundaries: {},
114
+ fn: async ({ iterations }, { setMetrics }) => {
115
+ for (let i = 0; i < iterations; i++) {
116
+ await setMetrics({
117
+ type: 'performance',
118
+ name: 'iteration_time',
119
+ value: (i + 1) * 10
120
+ })
121
+ }
122
+ return { completed: iterations }
123
+ }
124
+ })
125
+
126
+ const [result, error, record] = await task.safeRun({ iterations: 3 })
127
+
128
+ expect(error).toBeNull()
129
+ expect(result).toEqual({ completed: 3 })
130
+ expect(record.metrics).toHaveLength(3)
131
+
132
+ expect(record.metrics).toEqual([
133
+ { type: 'performance', name: 'iteration_time', value: 10 },
134
+ { type: 'performance', name: 'iteration_time', value: 20 },
135
+ { type: 'performance', name: 'iteration_time', value: 30 }
136
+ ])
137
+ })
138
+
139
+ it('should collect metrics from boundaries and main function', async () => {
140
+ const task = createTask({
141
+ name: 'boundary-metrics-test',
142
+ schema: new Schema({ input: Schema.string() }),
143
+ boundaries: {
144
+ processData: async (data: string) => {
145
+ return data.length
146
+ }
147
+ },
148
+ fn: async ({ input }, { processData, setMetrics }) => {
149
+ await setMetrics({ type: 'business', name: 'requests', value: 1 })
150
+
151
+ const length = await processData(input)
152
+ await setMetrics({ type: 'business', name: 'input_length', value: length })
153
+
154
+ return { length }
155
+ }
156
+ })
157
+
158
+ const [result, error, record] = await task.safeRun({ input: 'hello world' })
159
+
160
+ expect(error).toBeNull()
161
+ expect(result).toEqual({ length: 11 })
162
+ expect(record.metrics).toHaveLength(2)
163
+
164
+ expect(record.metrics).toEqual(expect.arrayContaining([
165
+ { type: 'business', name: 'requests', value: 1 },
166
+ { type: 'business', name: 'input_length', value: 11 }
167
+ ]))
168
+ })
169
+ })
170
+
171
+ describe('Metrics behavior in error scenarios', () => {
172
+ it('should preserve metrics collected before error occurs', async () => {
173
+ const task = createTask({
174
+ name: 'error-metrics-test',
175
+ schema: new Schema({ shouldFail: Schema.boolean() }),
176
+ boundaries: {},
177
+ fn: async ({ shouldFail }, { setMetrics }) => {
178
+ await setMetrics({ type: 'business', name: 'attempt_count', value: 1 })
179
+ await setMetrics({ type: 'performance', name: 'preparation_time', value: 50 })
180
+
181
+ if (shouldFail) {
182
+ await setMetrics({ type: 'error', name: 'failure_count', value: 1 })
183
+ throw new Error('Intentional failure')
184
+ }
185
+
186
+ return { success: true }
187
+ }
188
+ })
189
+
190
+ const [result, error, record] = await task.safeRun({ shouldFail: true })
191
+
192
+ expect(result).toBeNull()
193
+ expect(error).not.toBeNull()
194
+ expect(record.type).toBe('error')
195
+ expect(record.metrics).toHaveLength(3)
196
+
197
+ expect(record.metrics).toEqual(expect.arrayContaining([
198
+ { type: 'business', name: 'attempt_count', value: 1 },
199
+ { type: 'performance', name: 'preparation_time', value: 50 },
200
+ { type: 'error', name: 'failure_count', value: 1 }
201
+ ]))
202
+ })
203
+
204
+ it('should handle boundary errors while preserving metrics', async () => {
205
+ const task = createTask({
206
+ name: 'boundary-error-metrics-test',
207
+ schema: new Schema({ input: Schema.string() }),
208
+ boundaries: {
209
+ failingBoundary: async (data: string) => {
210
+ throw new Error(`Boundary failed for: ${data}`)
211
+ }
212
+ },
213
+ fn: async ({ input }, { failingBoundary, setMetrics }) => {
214
+ await setMetrics({ type: 'business', name: 'attempts', value: 1 })
215
+
216
+ try {
217
+ await failingBoundary(input)
218
+ } catch (error) {
219
+ await setMetrics({ type: 'error', name: 'boundary_failures', value: 1 })
220
+ throw error
221
+ }
222
+
223
+ return { success: true }
224
+ }
225
+ })
226
+
227
+ const [result, error, record] = await task.safeRun({ input: 'test' })
228
+
229
+ expect(result).toBeNull()
230
+ expect(error).not.toBeNull()
231
+ expect(record.type).toBe('error')
232
+ expect(record.metrics).toHaveLength(2)
233
+
234
+ expect(record.metrics).toEqual(expect.arrayContaining([
235
+ { type: 'business', name: 'attempts', value: 1 },
236
+ { type: 'error', name: 'boundary_failures', value: 1 }
237
+ ]))
238
+ })
239
+
240
+ it('should reject invalid metrics and continue execution', async () => {
241
+ const task = createTask({
242
+ name: 'invalid-metrics-test',
243
+ schema: new Schema({ input: Schema.string() }),
244
+ boundaries: {},
245
+ fn: async ({ input }, { setMetrics }) => {
246
+ // Valid metric should be stored
247
+ await setMetrics({ type: 'business', name: 'valid_metric', value: 1 })
248
+
249
+ // Invalid metrics should be rejected but not crash the task
250
+ try {
251
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
252
+ await setMetrics({ type: '', name: 'invalid', value: 1 } as any)
253
+ } catch (error) {
254
+ // Expected to fail validation
255
+ }
256
+
257
+ try {
258
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
259
+ await setMetrics({ type: 'business', name: 'invalid', value: NaN } as any)
260
+ } catch (error) {
261
+ // Expected to fail validation
262
+ }
263
+
264
+ // Another valid metric should still work
265
+ await setMetrics({ type: 'performance', name: 'final_metric', value: 100 })
266
+
267
+ return { result: input }
268
+ }
269
+ })
270
+
271
+ const [result, error, record] = await task.safeRun({ input: 'test' })
272
+
273
+ expect(error).toBeNull()
274
+ expect(result).toEqual({ result: 'test' })
275
+ expect(record.metrics).toHaveLength(2) // Only valid metrics should be stored
276
+
277
+ expect(record.metrics).toEqual(expect.arrayContaining([
278
+ { type: 'business', name: 'valid_metric', value: 1 },
279
+ { type: 'performance', name: 'final_metric', value: 100 }
280
+ ]))
281
+ })
282
+ })
283
+
284
+ describe('Metrics behavior during replay', () => {
285
+ it('should preserve original metrics during replay', async () => {
286
+ const task = createTask({
287
+ name: 'replay-metrics-test',
288
+ schema: new Schema({ input: Schema.string() }),
289
+ boundaries: {
290
+ dataFetch: async (query: string) => {
291
+ return `data-for-${query}`
292
+ }
293
+ },
294
+ fn: async ({ input }, { dataFetch, setMetrics }) => {
295
+ await setMetrics({ type: 'business', name: 'queries', value: 1 })
296
+ const data = await dataFetch(input)
297
+ await setMetrics({ type: 'performance', name: 'data_size', value: data.length })
298
+ return { data }
299
+ }
300
+ })
301
+
302
+ // First run to get original execution
303
+ const [originalResult, originalError, originalRecord] = await task.safeRun({ input: 'test' })
304
+
305
+ expect(originalError).toBeNull()
306
+ expect(originalResult).toEqual({ data: 'data-for-test' })
307
+ expect(originalRecord.metrics).toHaveLength(2)
308
+
309
+ // Replay the execution
310
+ const [replayResult, replayError, replayRecord] = await task.safeReplay(originalRecord, { boundaries: {} })
311
+
312
+ expect(replayError).toBeNull()
313
+ expect(replayResult).toEqual({ data: 'data-for-test' })
314
+
315
+ // Replay should preserve original metrics and may add new ones
316
+ expect(replayRecord.metrics?.length).toBeGreaterThanOrEqual(2)
317
+
318
+ // Original metrics should be included
319
+ expect(replayRecord.metrics).toEqual(expect.arrayContaining([
320
+ { type: 'business', name: 'queries', value: 1 },
321
+ { type: 'performance', name: 'data_size', value: 13 }
322
+ ]))
323
+ })
324
+
325
+ it('should allow new metrics during replay execution', async () => {
326
+ const task = createTask({
327
+ name: 'replay-new-metrics-test',
328
+ schema: new Schema({ mode: Schema.string() }),
329
+ boundaries: {
330
+ operation: async (mode: string) => {
331
+ return `result-${mode}`
332
+ }
333
+ },
334
+ fn: async ({ mode }, { operation, setMetrics }) => {
335
+ if (mode === 'original') {
336
+ await setMetrics({ type: 'business', name: 'original_run', value: 1 })
337
+ } else if (mode === 'replay') {
338
+ await setMetrics({ type: 'business', name: 'replay_run', value: 1 })
339
+ }
340
+
341
+ const result = await operation(mode)
342
+ await setMetrics({ type: 'performance', name: 'execution_count', value: 1 })
343
+
344
+ return { result }
345
+ }
346
+ })
347
+
348
+ // Original run
349
+ const [, originalError, originalRecord] = await task.safeRun({ mode: 'original' })
350
+ expect(originalError).toBeNull()
351
+ expect(originalRecord.metrics).toHaveLength(2)
352
+
353
+ // Modify the record for replay to change the mode
354
+ const modifiedRecord = {
355
+ ...originalRecord,
356
+ input: { mode: 'replay' }
357
+ }
358
+
359
+ // Replay with different mode
360
+ const [, replayError, replayRecord] = await task.safeReplay(modifiedRecord, { boundaries: {} })
361
+
362
+ expect(replayError).toBeNull()
363
+ expect(replayRecord.metrics?.length).toBeGreaterThanOrEqual(2)
364
+
365
+ // Should have both original metrics and new replay metrics
366
+ const metricNames = replayRecord.metrics?.map(m => m.name) || []
367
+ expect(metricNames).toContain('execution_count')
368
+ })
369
+ })
370
+
371
+ describe('Performance metrics and complex scenarios', () => {
372
+ it('should handle high-frequency metric collection', async () => {
373
+ const task = createTask({
374
+ name: 'high-frequency-metrics-test',
375
+ schema: new Schema({ count: Schema.number() }),
376
+ boundaries: {},
377
+ fn: async ({ count }, { setMetrics }) => {
378
+ for (let i = 0; i < count; i++) {
379
+ await setMetrics({
380
+ type: 'performance',
381
+ name: 'iteration',
382
+ value: i
383
+ })
384
+ }
385
+ return { completed: count }
386
+ }
387
+ })
388
+
389
+ const [result, error, record] = await task.safeRun({ count: 100 })
390
+
391
+ expect(error).toBeNull()
392
+ expect(result).toEqual({ completed: 100 })
393
+ expect(record.metrics).toHaveLength(100)
394
+
395
+ // Verify all metrics were collected correctly
396
+ record.metrics?.forEach((metric, index) => {
397
+ expect(metric).toEqual({
398
+ type: 'performance',
399
+ name: 'iteration',
400
+ value: index
401
+ })
402
+ })
403
+ })
404
+
405
+ it('should support different metric types in complex workflow', async () => {
406
+ const task = createTask({
407
+ name: 'complex-workflow-metrics-test',
408
+ schema: new Schema({
409
+ userId: Schema.string(),
410
+ operations: Schema.array(Schema.string())
411
+ }),
412
+ boundaries: {
413
+ validateUser: async (userId: string) => {
414
+ return userId.length > 0
415
+ },
416
+ processOperation: async (operation: string) => {
417
+ return `processed-${operation}`
418
+ }
419
+ },
420
+ fn: async ({ userId, operations }, { validateUser, processOperation, setMetrics }) => {
421
+ // Business metrics
422
+ await setMetrics({ type: 'business', name: 'user_requests', value: 1 })
423
+ await setMetrics({ type: 'business', name: 'operation_count', value: operations.length })
424
+
425
+ const startTime = Date.now()
426
+
427
+ // Validate user
428
+ const isValid = await validateUser(userId)
429
+ if (!isValid) {
430
+ await setMetrics({ type: 'error', name: 'validation_failures', value: 1 })
431
+ throw new Error('Invalid user')
432
+ }
433
+
434
+ // Process operations
435
+ const results = []
436
+ for (const operation of operations) {
437
+ const result = await processOperation(operation)
438
+ results.push(result)
439
+ await setMetrics({ type: 'business', name: 'operations_processed', value: 1 })
440
+ }
441
+
442
+ // Performance metrics
443
+ const duration = Date.now() - startTime
444
+ await setMetrics({ type: 'performance', name: 'total_processing_time', value: duration })
445
+ await setMetrics({ type: 'performance', name: 'avg_operation_time', value: duration / operations.length })
446
+
447
+ // Success metrics
448
+ await setMetrics({ type: 'business', name: 'successful_requests', value: 1 })
449
+ await setMetrics({ type: 'error', name: 'error_count', value: 0 })
450
+
451
+ return { userId, results, processingTime: duration }
452
+ }
453
+ })
454
+
455
+ const [result, error, record] = await task.safeRun({
456
+ userId: 'user123',
457
+ operations: ['op1', 'op2', 'op3']
458
+ })
459
+
460
+ expect(error).toBeNull()
461
+ expect(result?.userId).toBe('user123')
462
+ expect(result?.results).toEqual(['processed-op1', 'processed-op2', 'processed-op3'])
463
+
464
+ // Should have collected multiple types of metrics
465
+ expect(record.metrics?.length).toBeGreaterThanOrEqual(8)
466
+
467
+ const businessMetrics = record.metrics?.filter(m => m.type === 'business') || []
468
+ const performanceMetrics = record.metrics?.filter(m => m.type === 'performance') || []
469
+ const errorMetrics = record.metrics?.filter(m => m.type === 'error') || []
470
+
471
+ expect(businessMetrics.length).toBeGreaterThanOrEqual(5)
472
+ expect(performanceMetrics.length).toBeGreaterThanOrEqual(2)
473
+ expect(errorMetrics.length).toBeGreaterThanOrEqual(1)
474
+ })
475
+ })
476
+ })