@forgehive/task 0.2.2 → 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 (77) hide show
  1. package/dist/index.d.ts +12 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +66 -13
  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/listen-execution-records.test.d.ts +2 -0
  18. package/dist/test/listen-execution-records.test.d.ts.map +1 -0
  19. package/dist/test/listen-execution-records.test.js +223 -0
  20. package/dist/test/listen-execution-records.test.js.map +1 -0
  21. package/dist/test/metrics-collection.test.d.ts +2 -0
  22. package/dist/test/metrics-collection.test.d.ts.map +1 -0
  23. package/dist/test/metrics-collection.test.js +409 -0
  24. package/dist/test/metrics-collection.test.js.map +1 -0
  25. package/dist/test/performance-edge-cases.test.d.ts +2 -0
  26. package/dist/test/performance-edge-cases.test.d.ts.map +1 -0
  27. package/dist/test/performance-edge-cases.test.js +502 -0
  28. package/dist/test/performance-edge-cases.test.js.map +1 -0
  29. package/dist/test/run-boundary.test.js +27 -3
  30. package/dist/test/run-boundary.test.js.map +1 -1
  31. package/dist/test/safe-replay-complex-boundary.test.js +110 -9
  32. package/dist/test/safe-replay-complex-boundary.test.js.map +1 -1
  33. package/dist/test/safe-replay.test.js +35 -5
  34. package/dist/test/safe-replay.test.js.map +1 -1
  35. package/dist/test/safe-run.test.js +46 -4
  36. package/dist/test/safe-run.test.js.map +1 -1
  37. package/dist/test/setmetrics-boundary.test.d.ts +2 -0
  38. package/dist/test/setmetrics-boundary.test.d.ts.map +1 -0
  39. package/dist/test/setmetrics-boundary.test.js +195 -0
  40. package/dist/test/setmetrics-boundary.test.js.map +1 -0
  41. package/dist/test/task-with-boundaries.test.js +63 -2
  42. package/dist/test/task-with-boundaries.test.js.map +1 -1
  43. package/dist/test/timing-capture.test.d.ts +2 -0
  44. package/dist/test/timing-capture.test.d.ts.map +1 -0
  45. package/dist/test/timing-capture.test.js +304 -0
  46. package/dist/test/timing-capture.test.js.map +1 -0
  47. package/dist/test/timing-utilities.test.d.ts +2 -0
  48. package/dist/test/timing-utilities.test.d.ts.map +1 -0
  49. package/dist/test/timing-utilities.test.js +127 -0
  50. package/dist/test/timing-utilities.test.js.map +1 -0
  51. package/dist/types.d.ts +93 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/dist/types.js +78 -0
  54. package/dist/types.js.map +1 -0
  55. package/dist/utils/boundary.d.ts +3 -0
  56. package/dist/utils/boundary.d.ts.map +1 -1
  57. package/dist/utils/boundary.js +11 -2
  58. package/dist/utils/boundary.js.map +1 -1
  59. package/package.json +3 -2
  60. package/src/index.ts +97 -14
  61. package/src/test/add-listener-with-boundaries.test.ts +78 -7
  62. package/src/test/add-listener.test.ts +36 -0
  63. package/src/test/boundary-modes.test.ts +45 -5
  64. package/src/test/execution-record-boundaries.test.ts +12 -2
  65. package/src/test/listen-execution-records.test.ts +295 -0
  66. package/src/test/metrics-collection.test.ts +476 -0
  67. package/src/test/performance-edge-cases.test.ts +596 -0
  68. package/src/test/run-boundary.test.ts +27 -3
  69. package/src/test/safe-replay-complex-boundary.test.ts +115 -10
  70. package/src/test/safe-replay.test.ts +35 -5
  71. package/src/test/safe-run.test.ts +46 -4
  72. package/src/test/setmetrics-boundary.test.ts +223 -0
  73. package/src/test/task-with-boundaries.test.ts +71 -5
  74. package/src/test/timing-capture.test.ts +348 -0
  75. package/src/test/timing-utilities.test.ts +145 -0
  76. package/src/types.ts +139 -0
  77. package/src/utils/boundary.ts +15 -2
@@ -54,7 +54,15 @@ describe('Boundaries tasks tests', () => {
54
54
  },
55
55
  boundariesData: {
56
56
  fetchExternalData: [
57
- { input: [], output: { foo: false } }
57
+ {
58
+ input: [],
59
+ output: { foo: false },
60
+ timing: {
61
+ startTime: 1000,
62
+ endTime: 1100,
63
+ duration: 100
64
+ }
65
+ }
58
66
  ]
59
67
  },
60
68
  mode: 'proxy-pass'
@@ -122,7 +130,15 @@ describe('Boundaries tasks tests', () => {
122
130
  },
123
131
  boundariesData: {
124
132
  fetchExternalData: [
125
- { input: [4], output: 2 }
133
+ {
134
+ input: [4],
135
+ output: 2,
136
+ timing: {
137
+ startTime: 1000,
138
+ endTime: 1100,
139
+ duration: 100
140
+ }
141
+ }
126
142
  ]
127
143
  },
128
144
  mode: 'proxy-pass'
@@ -140,6 +156,11 @@ describe('Boundaries tasks tests', () => {
140
156
  type BoundaryData = {
141
157
  input: unknown[];
142
158
  output?: unknown;
159
+ timing?: {
160
+ startTime: number;
161
+ endTime: number;
162
+ duration: number;
163
+ };
143
164
  };
144
165
 
145
166
  // Define a type for the record boundaries
@@ -203,6 +224,11 @@ describe('Boundaries tasks tests', () => {
203
224
  expect(boundaries0.fetchExternalData).toHaveLength(1)
204
225
  expect(boundaries0.fetchExternalData[0].input).toEqual([2])
205
226
  expect(boundaries0.fetchExternalData[0].output).toBe(4)
227
+ expect(boundaries0.fetchExternalData[0].timing).toEqual(expect.objectContaining({
228
+ startTime: expect.any(Number),
229
+ endTime: expect.any(Number),
230
+ duration: expect.any(Number)
231
+ }))
206
232
 
207
233
  // Check record for second task (value: 3)
208
234
  expect(sortedRecords[1].input).toEqual({ value: 3 })
@@ -213,6 +239,11 @@ describe('Boundaries tasks tests', () => {
213
239
  expect(boundaries1.fetchExternalData).toHaveLength(1)
214
240
  expect(boundaries1.fetchExternalData[0].input).toEqual([3])
215
241
  expect(boundaries1.fetchExternalData[0].output).toBe(6)
242
+ expect(boundaries1.fetchExternalData[0].timing).toEqual(expect.objectContaining({
243
+ startTime: expect.any(Number),
244
+ endTime: expect.any(Number),
245
+ duration: expect.any(Number)
246
+ }))
216
247
 
217
248
  // Check record for third task (value: 4)
218
249
  expect(sortedRecords[2].input).toEqual({ value: 4 })
@@ -223,6 +254,11 @@ describe('Boundaries tasks tests', () => {
223
254
  expect(boundaries2.fetchExternalData).toHaveLength(1)
224
255
  expect(boundaries2.fetchExternalData[0].input).toEqual([4])
225
256
  expect(boundaries2.fetchExternalData[0].output).toBe(8)
257
+ expect(boundaries2.fetchExternalData[0].timing).toEqual(expect.objectContaining({
258
+ startTime: expect.any(Number),
259
+ endTime: expect.any(Number),
260
+ duration: expect.any(Number)
261
+ }))
226
262
  })
227
263
 
228
264
  it('Boundary data accumulates run data correctly', async () => {
@@ -261,9 +297,14 @@ describe('Boundaries tasks tests', () => {
261
297
  expect(boundariesData1.fetchExternalData).toHaveLength(1)
262
298
 
263
299
  // Verify the tape entry for first run
264
- const firstRunTape = boundariesData1.fetchExternalData as Array<{input: unknown[], output: unknown}>
300
+ const firstRunTape = boundariesData1.fetchExternalData as Array<{input: unknown[], output: unknown, timing?: { startTime: number; endTime: number; duration: number }}>
265
301
  expect(firstRunTape[0].input).toEqual([2])
266
302
  expect(firstRunTape[0].output).toBe(4)
303
+ expect(firstRunTape[0].timing).toEqual(expect.objectContaining({
304
+ startTime: expect.any(Number),
305
+ endTime: expect.any(Number),
306
+ duration: expect.any(Number)
307
+ }))
267
308
 
268
309
  // Run task with value 3
269
310
  await multiplyTask.run({ value: 3 })
@@ -275,16 +316,26 @@ describe('Boundaries tasks tests', () => {
275
316
  expect(boundariesData2.fetchExternalData).toHaveLength(2)
276
317
 
277
318
  // Sort the tape by input value for consistent testing
278
- const secondRunTape = boundariesData2.fetchExternalData as Array<{input: unknown[], output: unknown}>
319
+ const secondRunTape = boundariesData2.fetchExternalData as Array<{input: unknown[], output: unknown, timing?: { startTime: number; endTime: number; duration: number }}>
279
320
  const sortedTape = [...secondRunTape].sort((a, b) => (a.input[0] as number) - (b.input[0] as number))
280
321
 
281
322
  // First entry should still be the same
282
323
  expect(sortedTape[0].input).toEqual([2])
283
324
  expect(sortedTape[0].output).toBe(4)
325
+ expect(sortedTape[0].timing).toEqual(expect.objectContaining({
326
+ startTime: expect.any(Number),
327
+ endTime: expect.any(Number),
328
+ duration: expect.any(Number)
329
+ }))
284
330
 
285
331
  // Second entry should be from the second run
286
332
  expect(sortedTape[1].input).toEqual([3])
287
333
  expect(sortedTape[1].output).toBe(6)
334
+ expect(sortedTape[1].timing).toEqual(expect.objectContaining({
335
+ startTime: expect.any(Number),
336
+ endTime: expect.any(Number),
337
+ duration: expect.any(Number)
338
+ }))
288
339
 
289
340
  // Run task with value 4
290
341
  await multiplyTask.run({ value: 4 })
@@ -296,18 +347,33 @@ describe('Boundaries tasks tests', () => {
296
347
  expect(boundariesData3.fetchExternalData).toHaveLength(3)
297
348
 
298
349
  // Sort the tape again
299
- const thirdRunTape = boundariesData3.fetchExternalData as Array<{input: unknown[], output: unknown}>
350
+ const thirdRunTape = boundariesData3.fetchExternalData as Array<{input: unknown[], output: unknown, timing?: { startTime: number; endTime: number; duration: number }}>
300
351
  const finalSortedTape = [...thirdRunTape].sort((a, b) => (a.input[0] as number) - (b.input[0] as number))
301
352
 
302
353
  // Verify all three entries
303
354
  expect(finalSortedTape[0].input).toEqual([2])
304
355
  expect(finalSortedTape[0].output).toBe(4)
356
+ expect(finalSortedTape[0].timing).toEqual(expect.objectContaining({
357
+ startTime: expect.any(Number),
358
+ endTime: expect.any(Number),
359
+ duration: expect.any(Number)
360
+ }))
305
361
 
306
362
  expect(finalSortedTape[1].input).toEqual([3])
307
363
  expect(finalSortedTape[1].output).toBe(6)
364
+ expect(finalSortedTape[1].timing).toEqual(expect.objectContaining({
365
+ startTime: expect.any(Number),
366
+ endTime: expect.any(Number),
367
+ duration: expect.any(Number)
368
+ }))
308
369
 
309
370
  expect(finalSortedTape[2].input).toEqual([4])
310
371
  expect(finalSortedTape[2].output).toBe(8)
372
+ expect(finalSortedTape[2].timing).toEqual(expect.objectContaining({
373
+ startTime: expect.any(Number),
374
+ endTime: expect.any(Number),
375
+ duration: expect.any(Number)
376
+ }))
311
377
 
312
378
  // Verify tape can be used for replay in proxy-pass mode
313
379
  const replayTask = createTask({
@@ -0,0 +1,348 @@
1
+ import { createTask, TimingTracker } from '../index'
2
+ import { Schema } from '@forgehive/schema'
3
+
4
+ describe('Timing Capture Tests', () => {
5
+ describe('TimingTracker accuracy and reliability', () => {
6
+ it('should capture timing with millisecond precision', async () => {
7
+ const tracker = TimingTracker.create()
8
+
9
+ tracker.start()
10
+ await new Promise(resolve => setTimeout(resolve, 100)) // Wait 100ms
11
+ const timing = tracker.end()
12
+
13
+ expect(timing).not.toBeNull()
14
+ if (timing) {
15
+ expect(timing.duration).toBeGreaterThanOrEqual(90) // Allow for some variance
16
+ expect(timing.duration).toBeLessThan(150) // But not too much
17
+ expect(timing.endTime - timing.startTime).toBe(timing.duration)
18
+ }
19
+ })
20
+
21
+ it('should handle rapid successive timing operations', () => {
22
+ const timings: Array<{ startTime: number; endTime: number; duration?: number }> = []
23
+
24
+ for (let i = 0; i < 10; i++) {
25
+ const tracker = TimingTracker.create()
26
+ tracker.start()
27
+ // Immediate end
28
+ const timing = tracker.end()
29
+ if (timing) {
30
+ timings.push(timing)
31
+ }
32
+ }
33
+
34
+ timings.forEach(timing => {
35
+ expect(timing).not.toBeNull()
36
+ expect(timing.duration).toBeGreaterThanOrEqual(0)
37
+ expect(timing.duration).toBeLessThan(10) // Should be very fast
38
+ })
39
+ })
40
+
41
+ it('should provide monotonic timestamps', () => {
42
+ const tracker1 = TimingTracker.create()
43
+ const tracker2 = TimingTracker.create()
44
+
45
+ tracker1.start()
46
+ tracker2.start()
47
+
48
+ const timing1 = tracker1.end()
49
+ const timing2 = tracker2.end()
50
+
51
+ expect(timing1).not.toBeNull()
52
+ expect(timing2).not.toBeNull()
53
+ if (timing1 && timing2) {
54
+ expect(timing2.startTime).toBeGreaterThanOrEqual(timing1.startTime)
55
+ }
56
+ })
57
+ })
58
+
59
+ describe('Boundary timing capture in various scenarios', () => {
60
+ it('should capture timing for successful boundary calls', async () => {
61
+ const task = createTask({
62
+ name: 'timing-test-success',
63
+ schema: new Schema({ input: Schema.string() }),
64
+ boundaries: {
65
+ slowOperation: async (data: string) => {
66
+ await new Promise(resolve => setTimeout(resolve, 50))
67
+ return `processed: ${data}`
68
+ }
69
+ },
70
+ fn: async ({ input }, { slowOperation }) => {
71
+ const result = await slowOperation(input)
72
+ return { result }
73
+ }
74
+ })
75
+
76
+ const [result, error, record] = await task.safeRun({ input: 'test' })
77
+
78
+ expect(error).toBeNull()
79
+ expect(result).toEqual({ result: 'processed: test' })
80
+ expect(record.boundaries.slowOperation).toHaveLength(1)
81
+
82
+ const boundaryCall = record.boundaries.slowOperation[0]
83
+ expect(boundaryCall.timing).toEqual(expect.objectContaining({
84
+ startTime: expect.any(Number),
85
+ endTime: expect.any(Number),
86
+ duration: expect.any(Number)
87
+ }))
88
+ expect(boundaryCall.timing.duration).toBeGreaterThanOrEqual(40)
89
+ expect(boundaryCall.timing.duration).toBeLessThan(100)
90
+ })
91
+
92
+ it('should capture timing for failed boundary calls', async () => {
93
+ const task = createTask({
94
+ name: 'timing-test-error',
95
+ schema: new Schema({ input: Schema.string() }),
96
+ boundaries: {
97
+ failingOperation: async (data: string) => {
98
+ await new Promise(resolve => setTimeout(resolve, 30))
99
+ throw new Error(`Failed to process: ${data}`)
100
+ }
101
+ },
102
+ fn: async ({ input }, { failingOperation }) => {
103
+ const result = await failingOperation(input)
104
+ return { result }
105
+ }
106
+ })
107
+
108
+ const [result, error, record] = await task.safeRun({ input: 'test' })
109
+
110
+ expect(result).toBeNull()
111
+ expect(error).not.toBeNull()
112
+ expect(record.boundaries.failingOperation).toHaveLength(1)
113
+
114
+ const boundaryCall = record.boundaries.failingOperation[0]
115
+ expect(boundaryCall.timing).toEqual(expect.objectContaining({
116
+ startTime: expect.any(Number),
117
+ endTime: expect.any(Number),
118
+ duration: expect.any(Number)
119
+ }))
120
+ expect(boundaryCall.timing.duration).toBeGreaterThanOrEqual(20)
121
+ expect(boundaryCall.timing.duration).toBeLessThan(60)
122
+ expect('error' in boundaryCall).toBe(true)
123
+ })
124
+
125
+ it('should capture timing for multiple boundary calls', async () => {
126
+ const task = createTask({
127
+ name: 'timing-test-multiple',
128
+ schema: new Schema({ count: Schema.number() }),
129
+ boundaries: {
130
+ operation: async (index: number) => {
131
+ await new Promise(resolve => setTimeout(resolve, 20 + index * 10))
132
+ return `result-${index}`
133
+ }
134
+ },
135
+ fn: async ({ count }, { operation }) => {
136
+ const results = []
137
+ for (let i = 0; i < count; i++) {
138
+ results.push(await operation(i))
139
+ }
140
+ return { results }
141
+ }
142
+ })
143
+
144
+ const [result, error, record] = await task.safeRun({ count: 3 })
145
+
146
+ expect(error).toBeNull()
147
+ expect(result?.results).toEqual(['result-0', 'result-1', 'result-2'])
148
+ expect(record.boundaries.operation).toHaveLength(3)
149
+
150
+ record.boundaries.operation.forEach((call, index) => {
151
+ expect(call.timing).toEqual(expect.objectContaining({
152
+ startTime: expect.any(Number),
153
+ endTime: expect.any(Number),
154
+ duration: expect.any(Number)
155
+ }))
156
+ // Each call should take longer than the previous
157
+ expect(call.timing.duration).toBeGreaterThanOrEqual(15 + index * 10)
158
+ expect(call.timing.duration).toBeLessThan(50 + index * 10)
159
+ })
160
+ })
161
+
162
+ it('should capture timing for parallel boundary calls', async () => {
163
+ const task = createTask({
164
+ name: 'timing-test-parallel',
165
+ schema: new Schema({ delay: Schema.number() }),
166
+ boundaries: {
167
+ operationA: async (delay: number) => {
168
+ await new Promise(resolve => setTimeout(resolve, delay))
169
+ return 'A'
170
+ },
171
+ operationB: async (delay: number) => {
172
+ await new Promise(resolve => setTimeout(resolve, delay))
173
+ return 'B'
174
+ }
175
+ },
176
+ fn: async ({ delay }, { operationA, operationB }) => {
177
+ const [resultA, resultB] = await Promise.all([
178
+ operationA(delay),
179
+ operationB(delay)
180
+ ])
181
+ return { resultA, resultB }
182
+ }
183
+ })
184
+
185
+ const [result, error, record] = await task.safeRun({ delay: 50 })
186
+
187
+ expect(error).toBeNull()
188
+ expect(result).toEqual({ resultA: 'A', resultB: 'B' })
189
+ expect(record.boundaries.operationA).toHaveLength(1)
190
+ expect(record.boundaries.operationB).toHaveLength(1)
191
+
192
+ const callA = record.boundaries.operationA[0]
193
+ const callB = record.boundaries.operationB[0]
194
+
195
+ expect(callA.timing.duration).toBeGreaterThanOrEqual(40)
196
+ expect(callB.timing.duration).toBeGreaterThanOrEqual(40)
197
+
198
+ // Both calls should overlap in time since they run in parallel
199
+ const startDiff = Math.abs(callA.timing.startTime - callB.timing.startTime)
200
+ expect(startDiff).toBeLessThan(20) // Started within 20ms of each other
201
+ })
202
+ })
203
+
204
+ describe('Main function timing capture', () => {
205
+ it('should capture timing for main task function execution', async () => {
206
+ const task = createTask({
207
+ name: 'timing-test-main',
208
+ schema: new Schema({ delay: Schema.number() }),
209
+ boundaries: {},
210
+ fn: async ({ delay }) => {
211
+ await new Promise(resolve => setTimeout(resolve, delay))
212
+ return { completed: true }
213
+ }
214
+ })
215
+
216
+ const [result, error, record] = await task.safeRun({ delay: 100 })
217
+
218
+ expect(error).toBeNull()
219
+ expect(result).toEqual({ completed: true })
220
+ expect(record.timing).toEqual(expect.objectContaining({
221
+ startTime: expect.any(Number),
222
+ endTime: expect.any(Number),
223
+ duration: expect.any(Number)
224
+ }))
225
+ expect(record.timing?.duration).toBeGreaterThanOrEqual(90)
226
+ expect(record.timing?.duration).toBeLessThan(150)
227
+ })
228
+
229
+ it('should capture timing even when main function throws error', async () => {
230
+ const task = createTask({
231
+ name: 'timing-test-main-error',
232
+ schema: new Schema({ delay: Schema.number() }),
233
+ boundaries: {},
234
+ fn: async ({ delay }) => {
235
+ await new Promise(resolve => setTimeout(resolve, delay))
236
+ throw new Error('Intentional error')
237
+ }
238
+ })
239
+
240
+ const [result, error, record] = await task.safeRun({ delay: 80 })
241
+
242
+ expect(result).toBeNull()
243
+ expect(error).not.toBeNull()
244
+ expect(record.timing).toEqual(expect.objectContaining({
245
+ startTime: expect.any(Number),
246
+ endTime: expect.any(Number),
247
+ duration: expect.any(Number)
248
+ }))
249
+ expect(record.timing?.duration).toBeGreaterThanOrEqual(70)
250
+ expect(record.timing?.duration).toBeLessThan(120)
251
+ })
252
+
253
+ it('should include both main function and boundary timing', async () => {
254
+ const task = createTask({
255
+ name: 'timing-test-comprehensive',
256
+ schema: new Schema({ delay: Schema.number() }),
257
+ boundaries: {
258
+ slowBoundary: async (delay: number) => {
259
+ await new Promise(resolve => setTimeout(resolve, delay))
260
+ return 'boundary-result'
261
+ }
262
+ },
263
+ fn: async ({ delay }, { slowBoundary }) => {
264
+ await new Promise(resolve => setTimeout(resolve, delay))
265
+ const boundaryResult = await slowBoundary(delay)
266
+ await new Promise(resolve => setTimeout(resolve, delay))
267
+ return { main: 'completed', boundary: boundaryResult }
268
+ }
269
+ })
270
+
271
+ const [result, error, record] = await task.safeRun({ delay: 50 })
272
+
273
+ expect(error).toBeNull()
274
+ expect(result).toEqual({ main: 'completed', boundary: 'boundary-result' })
275
+
276
+ // Main function timing should include all delays
277
+ expect(record.timing?.duration).toBeGreaterThanOrEqual(140) // 3 * 50ms delays
278
+ expect(record.timing?.duration).toBeLessThan(200)
279
+
280
+ // Boundary timing should only include its delay
281
+ expect(record.boundaries.slowBoundary[0].timing.duration).toBeGreaterThanOrEqual(40)
282
+ expect(record.boundaries.slowBoundary[0].timing.duration).toBeLessThan(80)
283
+
284
+ // Boundary timing should be within the main function timing
285
+ const mainTiming = record.timing!
286
+ const boundaryTiming = record.boundaries.slowBoundary[0].timing
287
+
288
+ expect(boundaryTiming.startTime).toBeGreaterThanOrEqual(mainTiming.startTime)
289
+ expect(boundaryTiming.endTime).toBeLessThanOrEqual(mainTiming.endTime)
290
+ })
291
+ })
292
+
293
+ describe('Edge cases and error scenarios', () => {
294
+ it('should handle timing when schema validation fails', async () => {
295
+ const task = createTask({
296
+ name: 'timing-test-validation-error',
297
+ schema: new Schema({ requiredField: Schema.string() }),
298
+ boundaries: {},
299
+ fn: async ({ requiredField }) => {
300
+ return { field: requiredField }
301
+ }
302
+ })
303
+
304
+ // siabled to test an incorrect execution
305
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
306
+ const [result, error, record] = await task.safeRun({ wrongField: 'test' } as any)
307
+
308
+ expect(result).toBeNull()
309
+ expect(error).not.toBeNull()
310
+ expect(record.type).toBe('error')
311
+
312
+ // Should not have main function timing since function wasn't executed
313
+ expect(record.timing).toBeUndefined()
314
+ })
315
+
316
+ it('should handle timing with very fast operations', async () => {
317
+ const task = createTask({
318
+ name: 'timing-test-fast',
319
+ schema: new Schema({ input: Schema.string() }),
320
+ boundaries: {
321
+ fastOperation: async (data: string) => {
322
+ return data.toUpperCase() // Very fast operation
323
+ }
324
+ },
325
+ fn: async ({ input }, { fastOperation }) => {
326
+ const result = await fastOperation(input)
327
+ return { result }
328
+ }
329
+ })
330
+
331
+ const [result, error, record] = await task.safeRun({ input: 'test' })
332
+
333
+ expect(error).toBeNull()
334
+ expect(result).toEqual({ result: 'TEST' })
335
+
336
+ const boundaryCall = record.boundaries.fastOperation[0]
337
+ expect(boundaryCall.timing).toEqual(expect.objectContaining({
338
+ startTime: expect.any(Number),
339
+ endTime: expect.any(Number),
340
+ duration: expect.any(Number)
341
+ }))
342
+
343
+ // Even very fast operations should have non-negative duration
344
+ expect(boundaryCall.timing.duration).toBeGreaterThanOrEqual(0)
345
+ expect(boundaryCall.timing.duration).toBeLessThan(50)
346
+ })
347
+ })
348
+ })
@@ -0,0 +1,145 @@
1
+ import { TimingTracker, type TimingInfo, type Metric, type BoundaryTimingRecord, type BaseExecutionRecord } from '../types'
2
+
3
+ describe('Timing Utilities', () => {
4
+ describe('TimingTracker', () => {
5
+ it('should track timing correctly with proper TypeScript types', () => {
6
+ const tracker: TimingTracker = new TimingTracker()
7
+
8
+ tracker.start()
9
+ const timing: TimingInfo | null = tracker.end()
10
+
11
+ expect(timing).not.toBeNull()
12
+ if (timing) {
13
+ expect(typeof timing.startTime).toBe('number')
14
+ expect(typeof timing.endTime).toBe('number')
15
+ expect(typeof timing.duration).toBe('number')
16
+ expect(timing.startTime).toBeLessThanOrEqual(timing.endTime)
17
+ expect(timing.duration).toBeGreaterThanOrEqual(0)
18
+ expect(timing.duration).toBe(timing.endTime - timing.startTime)
19
+ }
20
+ })
21
+
22
+ it('should return null if end() called without start()', () => {
23
+ const tracker: TimingTracker = new TimingTracker()
24
+ const timing: TimingInfo | null = tracker.end()
25
+
26
+ expect(timing).toBeNull()
27
+ })
28
+
29
+ it('should create new instances via static method with correct type', () => {
30
+ const tracker: TimingTracker = TimingTracker.create()
31
+ expect(tracker).toBeInstanceOf(TimingTracker)
32
+ })
33
+
34
+ it('should handle multiple start/end cycles', () => {
35
+ const tracker: TimingTracker = new TimingTracker()
36
+
37
+ // First cycle
38
+ tracker.start()
39
+ const timing1: TimingInfo | null = tracker.end()
40
+ expect(timing1).not.toBeNull()
41
+
42
+ // Second cycle
43
+ tracker.start()
44
+ const timing2: TimingInfo | null = tracker.end()
45
+ expect(timing2).not.toBeNull()
46
+
47
+ if (timing1 && timing2) {
48
+ expect(timing2.startTime).toBeGreaterThanOrEqual(timing1.endTime)
49
+ }
50
+ })
51
+ })
52
+
53
+ describe('TypeScript Type Definitions', () => {
54
+ it('should enforce TimingInfo structure with required properties', () => {
55
+ const timing: TimingInfo = {
56
+ startTime: 1000,
57
+ endTime: 2000,
58
+ duration: 1000
59
+ }
60
+
61
+ expect(timing.startTime).toBe(1000)
62
+ expect(timing.endTime).toBe(2000)
63
+ expect(timing.duration).toBe(1000)
64
+
65
+ // TypeScript should enforce these are numbers
66
+ expect(typeof timing.startTime).toBe('number')
67
+ expect(typeof timing.endTime).toBe('number')
68
+ expect(typeof timing.duration).toBe('number')
69
+ })
70
+
71
+ it('should enforce Metric structure with typed properties', () => {
72
+ const metric: Metric = {
73
+ type: 'performance',
74
+ name: 'response_time',
75
+ value: 150
76
+ }
77
+
78
+ expect(metric.type).toBe('performance')
79
+ expect(metric.name).toBe('response_time')
80
+ expect(metric.value).toBe(150)
81
+
82
+ // TypeScript should enforce correct types
83
+ expect(typeof metric.type).toBe('string')
84
+ expect(typeof metric.name).toBe('string')
85
+ expect(typeof metric.value).toBe('number')
86
+ })
87
+
88
+ it('should support generic BoundaryTimingRecord with proper typing', () => {
89
+ const stringNumberRecord: BoundaryTimingRecord<[string], number> = {
90
+ input: ['test'],
91
+ output: 42,
92
+ timing: {
93
+ startTime: 1000,
94
+ endTime: 2000,
95
+ duration: 1000
96
+ }
97
+ }
98
+
99
+ expect(stringNumberRecord.input).toEqual(['test'])
100
+ expect(stringNumberRecord.output).toBe(42)
101
+ expect(stringNumberRecord.timing.duration).toBe(1000)
102
+
103
+ // Test with error record
104
+ const errorRecord: BoundaryTimingRecord<[string], never> = {
105
+ input: ['test'],
106
+ error: 'Something went wrong',
107
+ timing: {
108
+ startTime: 1000,
109
+ endTime: 2000,
110
+ duration: 1000
111
+ }
112
+ }
113
+
114
+ expect(errorRecord.input).toEqual(['test'])
115
+ expect(errorRecord.error).toBe('Something went wrong')
116
+ expect(errorRecord.output).toBeUndefined()
117
+ })
118
+
119
+ it('should support BaseExecutionRecord with comprehensive typing', () => {
120
+ const executionRecord: BaseExecutionRecord<{ userId: string }, { result: number }> = {
121
+ input: { userId: 'user123' },
122
+ output: { result: 42 },
123
+ taskName: 'testTask',
124
+ metadata: { environment: 'test' },
125
+ metrics: [
126
+ { type: 'performance', name: 'execution_time', value: 150 }
127
+ ],
128
+ timing: {
129
+ startTime: 1000,
130
+ endTime: 2000,
131
+ duration: 1000
132
+ },
133
+ type: 'success'
134
+ }
135
+
136
+ expect(executionRecord.input.userId).toBe('user123')
137
+ expect(executionRecord.output?.result).toBe(42)
138
+ expect(executionRecord.taskName).toBe('testTask')
139
+ expect(executionRecord.metadata?.environment).toBe('test')
140
+ expect(executionRecord.metrics?.[0].value).toBe(150)
141
+ expect(executionRecord.timing?.duration).toBe(1000)
142
+ expect(executionRecord.type).toBe('success')
143
+ })
144
+ })
145
+ })