@forgehive/task 0.1.7 → 0.1.9

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,231 @@
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
+
190
+ it('Should handle boundaries with both output and error as null', async () => {
191
+ // Create a manual execution log with null output and error in the boundary
192
+ const executionLog: ExecutionRecord = {
193
+ input: { ticker: 'AAPL' },
194
+ output: { ticker: 'AAPL', price: 160.23 },
195
+ boundaries: {
196
+ fetchData: [
197
+ {
198
+ input: ['AAPL'],
199
+ output: 160.23,
200
+ error: null
201
+ }
202
+ ]
203
+ }
204
+ }
205
+
206
+ // Use replay mode for fetchData
207
+ const [replayResult, replayError, replayLog] = await getTickerPrice.safeReplay(
208
+ executionLog,
209
+ {
210
+ boundaries: {
211
+ fetchData: 'replay',
212
+ }
213
+ }
214
+ )
215
+
216
+ // Verify the replay execution
217
+ expect(replayError).toBeNull()
218
+ expect(replayResult).toMatchObject({
219
+ ticker: 'AAPL',
220
+ price: 160.23
221
+ })
222
+
223
+ // The log should contain the output from the boundary
224
+ expect(replayLog.output).toMatchObject({
225
+ ticker: 'AAPL',
226
+ price: 160.23
227
+ })
228
+ expect(replayLog.boundaries.fetchData[0].output).toBe(160.23)
229
+ expect(replayLog.boundaries.fetchData[0].error).toBeNull()
230
+ })
231
+ })
@@ -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?: null;
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?: null;
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 && record.error !== null) {
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
  }