@forgehive/task 0.1.7 → 0.1.8

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,385 @@
1
+ import { Schema } from '@forgehive/schema'
2
+ import { createTask, ExecutionRecord } from '../index'
3
+
4
+ describe('Complex boundary replay tests', () => {
5
+ // Define types for our portfolio data
6
+ type Stock = {
7
+ ticker: string;
8
+ quantity: number;
9
+ }
10
+
11
+ type Portfolio = {
12
+ id: string;
13
+ userId: string;
14
+ stocks: Stock[];
15
+ }
16
+
17
+ // Test data
18
+ let priceData: Record<string, number>
19
+ let portfolioData: Record<string, Portfolio>
20
+
21
+ // Boundaries for the portfolio task
22
+ let boundaries: {
23
+ fetchPrice: (ticker: string) => Promise<number>;
24
+ fetchPortfolio: (userId: string) => Promise<Portfolio>;
25
+ }
26
+
27
+ // The task - using eslint-disable to allow any type for this test
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ let calculatePortfolioValue: any
30
+
31
+ beforeEach(() => {
32
+ // Setup mock data
33
+ priceData = {
34
+ 'AAPL': 182.63,
35
+ 'MSFT': 421.90,
36
+ 'GOOGL': 171.04,
37
+ 'AMZN': 184.72
38
+ }
39
+
40
+ portfolioData = {
41
+ 'user1': {
42
+ id: 'portfolio1',
43
+ userId: 'user1',
44
+ stocks: [
45
+ { ticker: 'AAPL', quantity: 10 },
46
+ { ticker: 'MSFT', quantity: 5 },
47
+ { ticker: 'GOOGL', quantity: 8 }
48
+ ]
49
+ },
50
+ 'user2': {
51
+ id: 'portfolio2',
52
+ userId: 'user2',
53
+ stocks: [
54
+ { ticker: 'AMZN', quantity: 15 },
55
+ { ticker: 'MSFT', quantity: 3 }
56
+ ]
57
+ }
58
+ }
59
+
60
+ // Define boundaries with realistic functions
61
+ boundaries = {
62
+ fetchPrice: async (ticker: string): Promise<number> => {
63
+ // Check if we have a price for this ticker
64
+ if (!priceData[ticker]) {
65
+ throw new Error(`Price data not available for ticker: ${ticker}`)
66
+ }
67
+ return priceData[ticker]
68
+ },
69
+ fetchPortfolio: async (userId: string): Promise<Portfolio> => {
70
+ // Check if we have a portfolio for this user
71
+ if (!portfolioData[userId]) {
72
+ throw new Error(`Portfolio not found for user: ${userId}`)
73
+ }
74
+ return portfolioData[userId]
75
+ }
76
+ }
77
+
78
+ // Create a schema for the task
79
+ const schema = new Schema({
80
+ userId: Schema.string()
81
+ })
82
+
83
+ // Create the portfolio value calculation task
84
+ calculatePortfolioValue = createTask(
85
+ schema,
86
+ boundaries,
87
+ async ({ userId }, { fetchPortfolio, fetchPrice }) => {
88
+ // First fetch the portfolio for the user
89
+ const portfolio = await fetchPortfolio(userId)
90
+
91
+ // Then calculate the value of each stock and the total portfolio value
92
+ const stocksWithValue = await Promise.all(
93
+ portfolio.stocks.map(async (stock) => {
94
+ const price = await fetchPrice(stock.ticker)
95
+ const value = price * stock.quantity
96
+
97
+ return {
98
+ ticker: stock.ticker,
99
+ quantity: stock.quantity,
100
+ price,
101
+ value
102
+ }
103
+ })
104
+ )
105
+
106
+ // Calculate total value
107
+ const totalValue = stocksWithValue.reduce((sum, stock) => sum + stock.value, 0)
108
+
109
+ // Return portfolio value information
110
+ return {
111
+ id: portfolio.id,
112
+ userId: portfolio.userId,
113
+ totalValue,
114
+ stocks: stocksWithValue
115
+ }
116
+ }
117
+ )
118
+ })
119
+
120
+ it('Should calculate portfolio value using multiple boundaries', async () => {
121
+ // Run the task for user1
122
+ const result = await calculatePortfolioValue.run({ userId: 'user1' })
123
+
124
+ // Verify the structure of the result
125
+ expect(result).toHaveProperty('id', 'portfolio1')
126
+ expect(result).toHaveProperty('userId', 'user1')
127
+ expect(result).toHaveProperty('totalValue')
128
+ expect(result).toHaveProperty('stocks')
129
+
130
+ // Verify portfolio calculations
131
+ expect(result.stocks).toHaveLength(3)
132
+
133
+ // Calculate expected values
134
+ const expectedTotalValue =
135
+ (priceData['AAPL'] * 10) +
136
+ (priceData['MSFT'] * 5) +
137
+ (priceData['GOOGL'] * 8)
138
+
139
+ // Verify total value matches expected
140
+ expect(result.totalValue).toBeCloseTo(expectedTotalValue)
141
+
142
+ // Verify each stock's value
143
+ const applStock = result.stocks.find((s: { ticker: string }) => s.ticker === 'AAPL')
144
+ expect(applStock).toBeDefined()
145
+ expect(applStock?.price).toBe(priceData['AAPL'])
146
+ expect(applStock?.value).toBeCloseTo(priceData['AAPL'] * 10)
147
+
148
+ const msftStock = result.stocks.find((s: { ticker: string }) => s.ticker === 'MSFT')
149
+ expect(msftStock).toBeDefined()
150
+ expect(msftStock?.price).toBe(priceData['MSFT'])
151
+ expect(msftStock?.value).toBeCloseTo(priceData['MSFT'] * 5)
152
+
153
+ const googlStock = result.stocks.find((s: { ticker: string }) => s.ticker === 'GOOGL')
154
+ expect(googlStock).toBeDefined()
155
+ expect(googlStock?.price).toBe(priceData['GOOGL'])
156
+ expect(googlStock?.value).toBeCloseTo(priceData['GOOGL'] * 8)
157
+ })
158
+
159
+ it('Should replay portfolio calculation from an execution log', async () => {
160
+ // First create a portfolio value calculation execution log
161
+ const executionLog: ExecutionRecord = {
162
+ input: { userId: 'user1' },
163
+ output: {
164
+ id: 'portfolio1',
165
+ userId: 'user1',
166
+ totalValue: 4363.02,
167
+ stocks: [
168
+ { ticker: 'AAPL', quantity: 10, price: 190.50, value: 1905.00 },
169
+ { ticker: 'MSFT', quantity: 5, price: 405.75, value: 2028.75 },
170
+ { ticker: 'GOOGL', quantity: 8, price: 161.16, value: 429.27 }
171
+ ]
172
+ },
173
+ boundaries: {
174
+ fetchPortfolio: [
175
+ {
176
+ input: ['user1'],
177
+ output: {
178
+ id: 'portfolio1',
179
+ userId: 'user1',
180
+ stocks: [
181
+ { ticker: 'AAPL', quantity: 10 },
182
+ { ticker: 'MSFT', quantity: 5 },
183
+ { ticker: 'GOOGL', quantity: 8 }
184
+ ]
185
+ }
186
+ }
187
+ ],
188
+ fetchPrice: [
189
+ {
190
+ input: ['AAPL'],
191
+ output: 190.50
192
+ },
193
+ {
194
+ input: ['MSFT'],
195
+ output: 405.75
196
+ },
197
+ {
198
+ input: ['GOOGL'],
199
+ output: 161.16
200
+ }
201
+ ]
202
+ }
203
+ }
204
+
205
+ // Use replay mode for all boundaries
206
+ const [replayResult, replayError, replayLog] = await calculatePortfolioValue.safeReplay(
207
+ executionLog,
208
+ {
209
+ boundaries: {
210
+ fetchPortfolio: 'replay',
211
+ fetchPrice: 'replay'
212
+ }
213
+ }
214
+ )
215
+
216
+ // Verify there was no error
217
+ expect(replayError).toBeNull()
218
+
219
+ // Verify the replayed result
220
+ expect(replayResult).toHaveProperty('id', 'portfolio1')
221
+ expect(replayResult).toHaveProperty('userId', 'user1')
222
+ expect(replayResult).toHaveProperty('totalValue', 5223.03)
223
+ expect(replayResult).toHaveProperty('stocks')
224
+ expect(replayResult.stocks).toHaveLength(3)
225
+
226
+ // Check values match the execution log exactly
227
+ const applStock = replayResult.stocks.find((s: { ticker: string }) => s.ticker === 'AAPL')
228
+ expect(applStock?.price).toBe(190.50)
229
+ expect(applStock?.value).toBe(1905.00)
230
+
231
+ // Verify that boundary calls were properly replayed
232
+ expect(replayLog.boundaries.fetchPortfolio).toHaveLength(1)
233
+ expect(replayLog.boundaries.fetchPrice).toHaveLength(3)
234
+ })
235
+
236
+ it('Should handle errors during replay', async () => {
237
+ // Create an execution log with an error in one of the price fetches
238
+ const executionLog: ExecutionRecord = {
239
+ input: { userId: 'user1' },
240
+ error: 'Price data not available for ticker: GOOGL',
241
+ boundaries: {
242
+ fetchPortfolio: [
243
+ {
244
+ input: ['user1'],
245
+ output: {
246
+ id: 'portfolio1',
247
+ userId: 'user1',
248
+ stocks: [
249
+ { ticker: 'AAPL', quantity: 10 },
250
+ { ticker: 'MSFT', quantity: 5 },
251
+ { ticker: 'GOOGL', quantity: 8 }
252
+ ]
253
+ }
254
+ }
255
+ ],
256
+ fetchPrice: [
257
+ {
258
+ input: ['AAPL'],
259
+ output: 190.50
260
+ },
261
+ {
262
+ input: ['MSFT'],
263
+ output: 405.75
264
+ },
265
+ {
266
+ input: ['GOOGL'],
267
+ error: 'Price data not available for ticker: GOOGL'
268
+ }
269
+ ]
270
+ }
271
+ }
272
+
273
+ // Use replay mode for all boundaries
274
+ const [replayResult, replayError, replayLog] = await calculatePortfolioValue.safeReplay(
275
+ executionLog,
276
+ {
277
+ boundaries: {
278
+ fetchPortfolio: 'replay',
279
+ fetchPrice: 'replay'
280
+ }
281
+ }
282
+ )
283
+
284
+ // Verify error was properly replayed
285
+ expect(replayResult).toBeNull()
286
+ expect(replayError).not.toBeNull()
287
+ expect(replayError?.message).toBe('Price data not available for ticker: GOOGL')
288
+
289
+ // Verify that boundary calls were properly replayed up to the error
290
+ expect(replayLog.boundaries.fetchPortfolio).toHaveLength(1)
291
+ expect(replayLog.boundaries.fetchPrice).toHaveLength(3)
292
+
293
+ // Check the error in the boundary
294
+ const googPriceCall = replayLog.boundaries.fetchPrice.find(
295
+ (call: { input?: unknown[] }) => call.input && call.input[0] === 'GOOGL'
296
+ )
297
+ expect(googPriceCall).toBeDefined()
298
+ expect(googPriceCall?.error).toBe('Price data not available for ticker: GOOGL')
299
+ })
300
+
301
+ it('Should support mixed replay with some boundaries in replay mode and others in proxy mode', async () => {
302
+ // Update prices for the test
303
+ priceData['AAPL'] = 195.00 // Different from replay data
304
+
305
+ // Create an execution log with historical data
306
+ const executionLog: ExecutionRecord = {
307
+ input: { userId: 'user1' },
308
+ output: {
309
+ id: 'portfolio1',
310
+ userId: 'user1',
311
+ totalValue: 4363.02,
312
+ stocks: [
313
+ { ticker: 'AAPL', quantity: 10, price: 190.50, value: 1905.00 },
314
+ { ticker: 'MSFT', quantity: 5, price: 405.75, value: 2028.75 },
315
+ { ticker: 'GOOGL', quantity: 8, price: 161.16, value: 429.27 }
316
+ ]
317
+ },
318
+ boundaries: {
319
+ fetchPortfolio: [
320
+ {
321
+ input: ['user1'],
322
+ output: {
323
+ id: 'portfolio1',
324
+ userId: 'user1',
325
+ stocks: [
326
+ { ticker: 'AAPL', quantity: 10 },
327
+ { ticker: 'MSFT', quantity: 5 },
328
+ { ticker: 'GOOGL', quantity: 8 }
329
+ ]
330
+ }
331
+ }
332
+ ],
333
+ fetchPrice: [
334
+ {
335
+ input: ['AAPL'],
336
+ output: 190.50
337
+ },
338
+ {
339
+ input: ['MSFT'],
340
+ output: 405.75
341
+ },
342
+ {
343
+ input: ['GOOGL'],
344
+ output: 161.16
345
+ }
346
+ ]
347
+ }
348
+ }
349
+
350
+ // Use replay mode for portfolio but proxy mode for prices
351
+ const [replayResult, replayError, replayLog] = await calculatePortfolioValue.safeReplay(
352
+ executionLog,
353
+ {
354
+ boundaries: {
355
+ fetchPortfolio: 'replay', // Use replay for portfolio
356
+ fetchPrice: 'proxy' // Use proxy (real execution) for prices
357
+ }
358
+ }
359
+ )
360
+
361
+ // Verify there was no error
362
+ expect(replayError).toBeNull()
363
+
364
+ // The result should reflect the updated AAPL price of 195.00
365
+ expect(replayResult).toHaveProperty('id', 'portfolio1')
366
+ expect(replayResult).toHaveProperty('userId', 'user1')
367
+
368
+ // Calculate expected value with current prices
369
+ const expectedValue =
370
+ (priceData['AAPL'] * 10) + // AAPL: 195.00 * 10
371
+ (priceData['MSFT'] * 5) + // MSFT: 421.90 * 5
372
+ (priceData['GOOGL'] * 8) // GOOGL: 171.04 * 8
373
+
374
+ expect(replayResult.totalValue).toBeCloseTo(expectedValue)
375
+
376
+ // Check AAPL price reflects the current price, not the replay data
377
+ const applStock = replayResult.stocks.find((s: { ticker: string }) => s.ticker === 'AAPL')
378
+ expect(applStock?.price).toBe(195.00)
379
+ expect(applStock?.value).toBeCloseTo(195.00 * 10)
380
+
381
+ // Verify the log shows the mixed sources correctly
382
+ expect(replayLog.boundaries.fetchPortfolio).toHaveLength(1)
383
+ expect(replayLog.boundaries.fetchPrice).toHaveLength(3)
384
+ })
385
+ })
@@ -0,0 +1,189 @@
1
+ import { Schema } from '@forgehive/schema'
2
+ import { createTask, ExecutionRecord } from '../index'
3
+
4
+ describe('safeReplay functionality tests', () => {
5
+ // Common variables
6
+ let prices: Record<string, number>
7
+ let boundaries: {
8
+ fetchData: (ticker: string) => Promise<number>
9
+ }
10
+
11
+ // ToDo: Add correct type for schema and getTickerPrice
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ let schema: Schema<Record<string, any>>
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ let getTickerPrice: any // Using any temporarily until we implement safeReplay
16
+
17
+ beforeEach(() => {
18
+ // Create a schema for the task
19
+ schema = new Schema({
20
+ ticker: Schema.string()
21
+ })
22
+
23
+ // Mock price data
24
+ prices = {
25
+ 'AAPL': 150.23
26
+ }
27
+
28
+ // Define the boundaries
29
+ boundaries = {
30
+ fetchData: async (ticker: string): Promise<number> => {
31
+ // check if the ticker is in the prices object
32
+ if (!prices[ticker as keyof typeof prices]) {
33
+ throw new Error(`Ticker ${ticker} not found in prices`)
34
+ }
35
+
36
+ return prices[ticker as keyof typeof prices]
37
+ }
38
+ }
39
+
40
+ // Create the task using createTask
41
+ getTickerPrice = createTask(
42
+ schema,
43
+ boundaries,
44
+ async ({ ticker }, { fetchData }) => {
45
+ const price = await fetchData(ticker)
46
+ return {
47
+ ticker,
48
+ price
49
+ }
50
+ }
51
+ )
52
+ })
53
+
54
+ it('Should replay a previous execution using the execution log and replay the fetchData boundary', async () => {
55
+ // Create a manual execution log
56
+ const executionLog: ExecutionRecord = {
57
+ input: { ticker: 'AAPL' },
58
+ output: {
59
+ ticker: 'AAPL',
60
+ price: 160.23
61
+ },
62
+ boundaries: {
63
+ fetchData: [
64
+ {
65
+ input: ['AAPL'],
66
+ output: 160.23
67
+ }
68
+ ]
69
+ }
70
+ }
71
+
72
+ // No safeReplay method yet, this will be implemented later
73
+ // This will be our test for that functionality
74
+ const [replayResult, replayError, replayLog] = await getTickerPrice.safeReplay(
75
+ executionLog
76
+ )
77
+
78
+ // Verify the replay execution
79
+ expect(replayError).toBeNull()
80
+ expect(replayResult).toMatchObject({
81
+ ticker: 'AAPL',
82
+ price: 150.23
83
+ })
84
+
85
+ expect(replayLog).toMatchObject({
86
+ input: { ticker: 'AAPL' },
87
+ output: {
88
+ ticker: 'AAPL',
89
+ price: 150.23
90
+ },
91
+ boundaries: {
92
+ fetchData: [
93
+ {
94
+ input: ['AAPL'],
95
+ output: 150.23,
96
+ }
97
+ ]
98
+ }
99
+ })
100
+ })
101
+
102
+ it('Should execute with mixed boundaries modes', async () => {
103
+ // Create a manual execution log for testing
104
+ const executionLog: ExecutionRecord = {
105
+ input: { ticker: 'AAPL' },
106
+ output: {
107
+ ticker: 'AAPL',
108
+ price: 160.23
109
+ },
110
+ boundaries: {
111
+ fetchData: [
112
+ {
113
+ input: ['AAPL'],
114
+ output: 160.23
115
+ }
116
+ ]
117
+ }
118
+ }
119
+
120
+ // Use mixed mode - replay for fetchData but execute logAccess
121
+ const [replayResult, replayError, replayLog] = await getTickerPrice.safeReplay(
122
+ executionLog,
123
+ {
124
+ boundaries: {
125
+ fetchData: 'replay',
126
+ }
127
+ }
128
+ )
129
+
130
+ // Verify the replay execution
131
+ expect(replayError).toBeNull()
132
+ expect(replayResult).toMatchObject({
133
+ ticker: 'AAPL',
134
+ price: 160.23
135
+ })
136
+
137
+ expect(replayLog).toMatchObject({
138
+ input: { ticker: 'AAPL' },
139
+ output: {
140
+ ticker: 'AAPL',
141
+ price: 160.23
142
+ },
143
+ boundaries: {
144
+ fetchData: [
145
+ {
146
+ input: ['AAPL'],
147
+ output: 160.23
148
+ }
149
+ ]
150
+ }
151
+ })
152
+ })
153
+
154
+ it('Should properly handle errors in boundary replay mode', async () => {
155
+ // Create a manual execution log with an error in the boundary
156
+ const executionLog: ExecutionRecord = {
157
+ input: { ticker: 'AAPL' },
158
+ output: null,
159
+ error: 'API error: Rate limit exceeded',
160
+ boundaries: {
161
+ fetchData: [
162
+ {
163
+ input: ['AAPL'],
164
+ error: 'API error: Rate limit exceeded'
165
+ }
166
+ ]
167
+ }
168
+ }
169
+
170
+ // Use replay mode for fetchData
171
+ const [replayResult, replayError, replayLog] = await getTickerPrice.safeReplay(
172
+ executionLog,
173
+ {
174
+ boundaries: {
175
+ fetchData: 'replay',
176
+ }
177
+ }
178
+ )
179
+
180
+ // Verify the replay execution - should have an error
181
+ expect(replayResult).toBeNull()
182
+ expect(replayError).not.toBeNull()
183
+ expect(replayError?.message).toBe('API error: Rate limit exceeded')
184
+
185
+ // The log should contain the error from the boundary
186
+ expect(replayLog.error).toBeDefined()
187
+ expect(replayLog.boundaries.fetchData[0].error).toBe('API error: Rate limit exceeded')
188
+ })
189
+ })
@@ -1,4 +1,4 @@
1
- import { createTask, Schema, TaskRecord } from '../index'
1
+ import { createTask, Schema, TaskRecord, BoundaryTapeData } from '../index'
2
2
 
3
3
  // Need to add proxy cache mode to the boundaries
4
4
  describe('Boundaries tasks tests', () => {
@@ -316,7 +316,7 @@ describe('Boundaries tasks tests', () => {
316
316
  return value * externalData
317
317
  },
318
318
  {
319
- boundariesData: boundariesData3,
319
+ boundariesData: boundariesData3 as BoundaryTapeData,
320
320
  mode: 'proxy-pass'
321
321
  }
322
322
  )
@@ -14,17 +14,39 @@ export type Mode = 'proxy' | 'proxy-pass' | 'proxy-catch' | 'replay'
14
14
  export type BoundaryFunction<TReturn = any> = (...args: any[]) => Promise<TReturn>
15
15
 
16
16
  /**
17
- * Represents a record of a boundary function call
17
+ * Success record for a boundary function call
18
18
  * @template TInput - The type of input data
19
19
  * @template TOutput - The type of output data
20
20
  */
21
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
- export interface BoundaryRecord<TInput = any[], TOutput = any> {
23
- input: TInput
24
- output?: TOutput
25
- error?: string
21
+ export type BoundarySuccessRecord<TInput = unknown[], TOutput = unknown> = {
22
+ input: TInput;
23
+ output: TOutput;
24
+ error?: undefined;
26
25
  }
27
26
 
27
+ /**
28
+ * Error record for a boundary function call
29
+ * @template TInput - The type of input data
30
+ */
31
+ export type BoundaryErrorRecord<TInput = unknown[]> = {
32
+ input: TInput;
33
+ output?: undefined;
34
+ error: string;
35
+ }
36
+
37
+ /**
38
+ * Represents a record of a boundary function call - either success or error
39
+ * @template TInput - The type of input data
40
+ * @template TOutput - The type of output data
41
+ */
42
+ export type BoundaryRecord<TInput = unknown[], TOutput = unknown> =
43
+ BoundarySuccessRecord<TInput, TOutput> | BoundaryErrorRecord<TInput>;
44
+
45
+ /**
46
+ * Represents the tape data for all boundaries
47
+ */
48
+ export type BoundaryTapeData = Record<string, Array<BoundaryRecord>>;
49
+
28
50
  /**
29
51
  * Represents a wrapped boundary function with additional methods
30
52
  */
@@ -79,10 +101,6 @@ export const createBoundary = <Func extends BaseBoundary>(fn: Func): WrappedBoun
79
101
  return result
80
102
  }
81
103
 
82
- const record: RecordType = {
83
- input: args
84
- }
85
-
86
104
  if (mode === 'proxy-pass') {
87
105
  const record = findRecord(args, cacheTape)
88
106
 
@@ -101,7 +119,8 @@ export const createBoundary = <Func extends BaseBoundary>(fn: Func): WrappedBoun
101
119
  throw new Error('No tape value for this inputs')
102
120
  }
103
121
 
104
- if (typeof record.error !== 'undefined') {
122
+ // Check if this is an error record by checking if error property exists
123
+ if (record.error !== undefined) {
105
124
  throw new Error(record.error)
106
125
  }
107
126
 
@@ -124,18 +143,26 @@ export const createBoundary = <Func extends BaseBoundary>(fn: Func): WrappedBoun
124
143
  return prevRecord.output as unknown as ReturnType<Func>
125
144
  })()
126
145
  } else {
127
- record.error = error.message
146
+ // Create an error record
147
+ const errorRecord: BoundaryErrorRecord<FuncInput> = {
148
+ input: args,
149
+ error: error.message
150
+ }
128
151
 
129
- if (hasRun) { runLog.push(record) }
130
- cacheTape.push(record)
152
+ if (hasRun) { runLog.push(errorRecord) }
153
+ cacheTape.push(errorRecord)
131
154
 
132
155
  throw error
133
156
  }
134
157
  } else {
135
- record.output = result as FuncOutput
158
+ // Create a success record
159
+ const successRecord: BoundarySuccessRecord<FuncInput, FuncOutput> = {
160
+ input: args,
161
+ output: result as FuncOutput
162
+ }
136
163
 
137
- if (hasRun) { runLog.push(record) }
138
- cacheTape.push(record)
164
+ if (hasRun) { runLog.push(successRecord) }
165
+ cacheTape.push(successRecord)
139
166
 
140
167
  return result as ReturnType<Func>
141
168
  }