@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.
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +66 -13
- package/dist/index.js.map +1 -1
- package/dist/test/add-listener-with-boundaries.test.js +78 -7
- package/dist/test/add-listener-with-boundaries.test.js.map +1 -1
- package/dist/test/add-listener.test.js +36 -0
- package/dist/test/add-listener.test.js.map +1 -1
- package/dist/test/boundary-modes.test.js +45 -5
- package/dist/test/boundary-modes.test.js.map +1 -1
- package/dist/test/execution-record-boundaries.test.js +12 -2
- package/dist/test/execution-record-boundaries.test.js.map +1 -1
- package/dist/test/integration-enhanced-records.test.d.ts +2 -0
- package/dist/test/integration-enhanced-records.test.d.ts.map +1 -0
- package/dist/test/integration-enhanced-records.test.js +467 -0
- package/dist/test/integration-enhanced-records.test.js.map +1 -0
- package/dist/test/listen-execution-records.test.d.ts +2 -0
- package/dist/test/listen-execution-records.test.d.ts.map +1 -0
- package/dist/test/listen-execution-records.test.js +223 -0
- package/dist/test/listen-execution-records.test.js.map +1 -0
- package/dist/test/metrics-collection.test.d.ts +2 -0
- package/dist/test/metrics-collection.test.d.ts.map +1 -0
- package/dist/test/metrics-collection.test.js +409 -0
- package/dist/test/metrics-collection.test.js.map +1 -0
- package/dist/test/performance-edge-cases.test.d.ts +2 -0
- package/dist/test/performance-edge-cases.test.d.ts.map +1 -0
- package/dist/test/performance-edge-cases.test.js +502 -0
- package/dist/test/performance-edge-cases.test.js.map +1 -0
- package/dist/test/run-boundary.test.js +27 -3
- package/dist/test/run-boundary.test.js.map +1 -1
- package/dist/test/safe-replay-complex-boundary.test.js +110 -9
- package/dist/test/safe-replay-complex-boundary.test.js.map +1 -1
- package/dist/test/safe-replay.test.js +35 -5
- package/dist/test/safe-replay.test.js.map +1 -1
- package/dist/test/safe-run.test.js +46 -4
- package/dist/test/safe-run.test.js.map +1 -1
- package/dist/test/setmetrics-boundary.test.d.ts +2 -0
- package/dist/test/setmetrics-boundary.test.d.ts.map +1 -0
- package/dist/test/setmetrics-boundary.test.js +195 -0
- package/dist/test/setmetrics-boundary.test.js.map +1 -0
- package/dist/test/task-with-boundaries.test.js +63 -2
- package/dist/test/task-with-boundaries.test.js.map +1 -1
- package/dist/test/timing-capture.test.d.ts +2 -0
- package/dist/test/timing-capture.test.d.ts.map +1 -0
- package/dist/test/timing-capture.test.js +304 -0
- package/dist/test/timing-capture.test.js.map +1 -0
- package/dist/test/timing-utilities.test.d.ts +2 -0
- package/dist/test/timing-utilities.test.d.ts.map +1 -0
- package/dist/test/timing-utilities.test.js +127 -0
- package/dist/test/timing-utilities.test.js.map +1 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +78 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/boundary.d.ts +3 -0
- package/dist/utils/boundary.d.ts.map +1 -1
- package/dist/utils/boundary.js +11 -2
- package/dist/utils/boundary.js.map +1 -1
- package/package.json +3 -2
- package/src/index.ts +97 -14
- package/src/test/add-listener-with-boundaries.test.ts +78 -7
- package/src/test/add-listener.test.ts +36 -0
- package/src/test/boundary-modes.test.ts +45 -5
- package/src/test/execution-record-boundaries.test.ts +12 -2
- package/src/test/listen-execution-records.test.ts +295 -0
- package/src/test/metrics-collection.test.ts +476 -0
- package/src/test/performance-edge-cases.test.ts +596 -0
- package/src/test/run-boundary.test.ts +27 -3
- package/src/test/safe-replay-complex-boundary.test.ts +115 -10
- package/src/test/safe-replay.test.ts +35 -5
- package/src/test/safe-run.test.ts +46 -4
- package/src/test/setmetrics-boundary.test.ts +223 -0
- package/src/test/task-with-boundaries.test.ts +71 -5
- package/src/test/timing-capture.test.ts +348 -0
- package/src/test/timing-utilities.test.ts +145 -0
- package/src/types.ts +139 -0
- package/src/utils/boundary.ts +15 -2
|
@@ -54,7 +54,15 @@ describe('Boundaries tasks tests', () => {
|
|
|
54
54
|
},
|
|
55
55
|
boundariesData: {
|
|
56
56
|
fetchExternalData: [
|
|
57
|
-
{
|
|
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
|
-
{
|
|
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
|
+
})
|