@0xsequence/catapult 1.4.0 → 1.5.0

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 (163) hide show
  1. package/README.md +27 -0
  2. package/dist/lib/__tests__/network-loader.spec.js.map +1 -1
  3. package/dist/lib/core/__tests__/resolver.spec.js +22 -0
  4. package/dist/lib/core/__tests__/resolver.spec.js.map +1 -1
  5. package/dist/lib/core/__tests__/sign-actions.spec.d.ts +2 -0
  6. package/dist/lib/core/__tests__/sign-actions.spec.d.ts.map +1 -0
  7. package/dist/lib/core/__tests__/sign-actions.spec.js +128 -0
  8. package/dist/lib/core/__tests__/sign-actions.spec.js.map +1 -0
  9. package/dist/lib/core/__tests__/signer.spec.d.ts +2 -0
  10. package/dist/lib/core/__tests__/signer.spec.d.ts.map +1 -0
  11. package/dist/lib/core/__tests__/signer.spec.js +40 -0
  12. package/dist/lib/core/__tests__/signer.spec.js.map +1 -0
  13. package/dist/lib/core/context.d.ts +3 -2
  14. package/dist/lib/core/context.d.ts.map +1 -1
  15. package/dist/lib/core/context.js +3 -2
  16. package/dist/lib/core/context.js.map +1 -1
  17. package/dist/lib/core/engine.d.ts +4 -0
  18. package/dist/lib/core/engine.d.ts.map +1 -1
  19. package/dist/lib/core/engine.js +173 -0
  20. package/dist/lib/core/engine.js.map +1 -1
  21. package/dist/lib/core/signer.d.ts +7 -0
  22. package/dist/lib/core/signer.d.ts.map +1 -0
  23. package/dist/lib/core/signer.js +60 -0
  24. package/dist/lib/core/signer.js.map +1 -0
  25. package/dist/lib/parsers/__tests__/source.spec.js +37 -0
  26. package/dist/lib/parsers/__tests__/source.spec.js.map +1 -1
  27. package/dist/lib/parsers/source.js +1 -1
  28. package/dist/lib/parsers/source.js.map +1 -1
  29. package/dist/lib/provenance.js +51 -2
  30. package/dist/lib/provenance.js.map +1 -1
  31. package/dist/lib/types/actions.d.ts +26 -2
  32. package/dist/lib/types/actions.d.ts.map +1 -1
  33. package/dist/lib/types/actions.js +3 -0
  34. package/dist/lib/types/actions.js.map +1 -1
  35. package/dist/lib/types/source.d.ts +2 -0
  36. package/dist/lib/types/source.d.ts.map +1 -1
  37. package/package.json +4 -1
  38. package/.eslintrc.json +0 -29
  39. package/.github/workflows/ci.yml +0 -181
  40. package/CONCEPT.md +0 -24
  41. package/contracts/checked-call.huff +0 -65
  42. package/eslint.config.js +0 -48
  43. package/examples/jobs/guards-v1.yaml +0 -17
  44. package/examples/jobs/sequence-seq-0001-patch.yaml +0 -59
  45. package/examples/jobs/sequence-v1.yaml +0 -59
  46. package/examples/templates/sequence-factory-v1.yaml +0 -56
  47. package/jest.config.js +0 -25
  48. package/src/cli.ts +0 -18
  49. package/src/commands/common.ts +0 -61
  50. package/src/commands/dry.ts +0 -209
  51. package/src/commands/etherscan.ts +0 -360
  52. package/src/commands/index.ts +0 -6
  53. package/src/commands/list.ts +0 -262
  54. package/src/commands/provenance.ts +0 -120
  55. package/src/commands/run.ts +0 -146
  56. package/src/commands/utils.ts +0 -215
  57. package/src/index.ts +0 -67
  58. package/src/lib/__tests__/deployer-events.spec.ts +0 -338
  59. package/src/lib/__tests__/deployer.spec.ts +0 -2269
  60. package/src/lib/__tests__/network-loader.spec.ts +0 -150
  61. package/src/lib/__tests__/network-selection.spec.ts +0 -41
  62. package/src/lib/__tests__/network-utils.spec.ts +0 -230
  63. package/src/lib/__tests__/provenance.spec.ts +0 -208
  64. package/src/lib/artifacts/__tests__/fixtures/contract1.json +0 -19
  65. package/src/lib/artifacts/__tests__/fixtures/contract2.json +0 -19
  66. package/src/lib/artifacts/__tests__/fixtures/duplicate-name.json +0 -19
  67. package/src/lib/artifacts/__tests__/fixtures/nested/nested-contract.json +0 -18
  68. package/src/lib/artifacts/__tests__/fixtures/not-an-artifact.json +0 -8
  69. package/src/lib/artifacts/__tests__/fixtures/readme.txt +0 -2
  70. package/src/lib/contracts/__tests__/repository.spec.ts +0 -612
  71. package/src/lib/contracts/repository.ts +0 -411
  72. package/src/lib/core/__tests__/assert-action.spec.ts +0 -474
  73. package/src/lib/core/__tests__/context.spec.ts +0 -37
  74. package/src/lib/core/__tests__/engine.spec.ts +0 -2005
  75. package/src/lib/core/__tests__/graph.spec.ts +0 -125
  76. package/src/lib/core/__tests__/json-integration.spec.ts +0 -425
  77. package/src/lib/core/__tests__/loader.spec.ts +0 -367
  78. package/src/lib/core/__tests__/multi-platform-verification.spec.ts +0 -406
  79. package/src/lib/core/__tests__/resolver.spec.ts +0 -2496
  80. package/src/lib/core/__tests__/static-action.spec.ts +0 -172
  81. package/src/lib/core/context.ts +0 -127
  82. package/src/lib/core/engine.ts +0 -1834
  83. package/src/lib/core/graph.ts +0 -252
  84. package/src/lib/core/loader.ts +0 -253
  85. package/src/lib/core/resolver.ts +0 -873
  86. package/src/lib/deployer.ts +0 -1005
  87. package/src/lib/events/__tests__/event-system.spec.ts +0 -392
  88. package/src/lib/events/cli-adapter.ts +0 -369
  89. package/src/lib/events/emitter.ts +0 -62
  90. package/src/lib/events/index.ts +0 -3
  91. package/src/lib/events/types.ts +0 -520
  92. package/src/lib/index.ts +0 -17
  93. package/src/lib/network-loader.ts +0 -90
  94. package/src/lib/network-selection.ts +0 -73
  95. package/src/lib/network-utils.ts +0 -64
  96. package/src/lib/parsers/__tests__/buildinfo.spec.ts +0 -122
  97. package/src/lib/parsers/__tests__/fixtures/buildinfo/invalid-bytecode-buildinfo.json +0 -62
  98. package/src/lib/parsers/__tests__/fixtures/buildinfo/invalid-json.txt +0 -2
  99. package/src/lib/parsers/__tests__/fixtures/buildinfo/multi-contract-buildinfo.json +0 -89
  100. package/src/lib/parsers/__tests__/fixtures/buildinfo/no-contracts-buildinfo.json +0 -17
  101. package/src/lib/parsers/__tests__/fixtures/buildinfo/simple-buildinfo.json +0 -63
  102. package/src/lib/parsers/__tests__/fixtures/buildinfo/wrong-format.json +0 -4
  103. package/src/lib/parsers/__tests__/job.spec.ts +0 -439
  104. package/src/lib/parsers/__tests__/source.spec.ts +0 -134
  105. package/src/lib/parsers/__tests__/template.spec.ts +0 -111
  106. package/src/lib/parsers/artifact/__tests__/artifact.spec.ts +0 -117
  107. package/src/lib/parsers/artifact/__tests__/fixtures/empty-bytecode.json +0 -5
  108. package/src/lib/parsers/artifact/__tests__/fixtures/hardhat-artifact.json +0 -67
  109. package/src/lib/parsers/artifact/__tests__/fixtures/invalid-bytecode.json +0 -5
  110. package/src/lib/parsers/artifact/__tests__/fixtures/invalid-json.txt +0 -11
  111. package/src/lib/parsers/artifact/__tests__/fixtures/minimal-artifact.json +0 -5
  112. package/src/lib/parsers/artifact/__tests__/fixtures/missing-abi.json +0 -4
  113. package/src/lib/parsers/artifact/__tests__/fixtures/missing-bytecode.json +0 -11
  114. package/src/lib/parsers/artifact/__tests__/fixtures/missing-contract-name.json +0 -11
  115. package/src/lib/parsers/artifact/__tests__/fixtures/simple-artifact.json +0 -40
  116. package/src/lib/parsers/artifact/__tests__/fixtures/wrong-types.json +0 -7
  117. package/src/lib/parsers/artifact/foundry-1.2.ts +0 -72
  118. package/src/lib/parsers/artifact/index.ts +0 -27
  119. package/src/lib/parsers/artifact/types.ts +0 -9
  120. package/src/lib/parsers/buildinfo.ts +0 -127
  121. package/src/lib/parsers/constants.ts +0 -56
  122. package/src/lib/parsers/index.ts +0 -6
  123. package/src/lib/parsers/job.ts +0 -160
  124. package/src/lib/parsers/source.ts +0 -129
  125. package/src/lib/parsers/template.ts +0 -135
  126. package/src/lib/provenance.ts +0 -785
  127. package/src/lib/std/templates/arachnid-deterministic-deployment-proxy.yaml +0 -68
  128. package/src/lib/std/templates/assured-deployment.yaml +0 -46
  129. package/src/lib/std/templates/era-evm-predeploy.yaml +0 -35
  130. package/src/lib/std/templates/erc-2470.yaml +0 -70
  131. package/src/lib/std/templates/min-balance.yaml +0 -35
  132. package/src/lib/std/templates/nano-universal-deployer.yaml +0 -61
  133. package/src/lib/std/templates/raw-erc-2470.yaml +0 -62
  134. package/src/lib/std/templates/raw-nano-universal-deployer.yaml +0 -54
  135. package/src/lib/std/templates/raw-sequence-universal-deployer-2.yaml +0 -52
  136. package/src/lib/std/templates/sequence-universal-deployer-2.yaml +0 -61
  137. package/src/lib/types/__tests__/json-request-action.spec.ts +0 -243
  138. package/src/lib/types/__tests__/read-json-value.spec.ts +0 -278
  139. package/src/lib/types/__tests__/resolve-json-value.spec.ts +0 -769
  140. package/src/lib/types/actions.ts +0 -148
  141. package/src/lib/types/artifacts.ts +0 -21
  142. package/src/lib/types/buildinfo.ts +0 -116
  143. package/src/lib/types/conditions.ts +0 -50
  144. package/src/lib/types/contracts.ts +0 -26
  145. package/src/lib/types/definitions.ts +0 -77
  146. package/src/lib/types/index.ts +0 -9
  147. package/src/lib/types/network.ts +0 -33
  148. package/src/lib/types/project.ts +0 -9
  149. package/src/lib/types/source.ts +0 -26
  150. package/src/lib/types/task.ts +0 -9
  151. package/src/lib/types/values.ts +0 -221
  152. package/src/lib/utils/assertion.ts +0 -24
  153. package/src/lib/utils/validation.ts +0 -116
  154. package/src/lib/validation/contract-references.ts +0 -210
  155. package/src/lib/validation/index.ts +0 -1
  156. package/src/lib/verification/__tests__/etherscan.spec.ts +0 -710
  157. package/src/lib/verification/__tests__/sourcify.spec.ts +0 -288
  158. package/src/lib/verification/etherscan.ts +0 -547
  159. package/src/lib/verification/sourcify.ts +0 -248
  160. package/test_validation/artifacts/TestContract.json +0 -9
  161. package/test_validation/jobs/test-missing.yaml +0 -16
  162. package/test_validation/networks.yaml +0 -3
  163. package/tsconfig.json +0 -36
@@ -1,2269 +0,0 @@
1
- import * as fs from 'fs/promises'
2
- import { Deployer, DeployerOptions } from '../deployer'
3
- import { ProjectLoader } from '../core/loader'
4
- import { DependencyGraph } from '../core/graph'
5
- import { ExecutionEngine } from '../core/engine'
6
- import { ExecutionContext } from '../core/context'
7
- import { Network, Job, Template } from '../types'
8
-
9
- // Mock all dependencies
10
- jest.mock('fs/promises')
11
- jest.mock('../core/loader')
12
- jest.mock('../core/graph')
13
- jest.mock('../core/engine')
14
- jest.mock('../core/context')
15
-
16
- const mockFs = fs as jest.Mocked<typeof fs>
17
- const MockProjectLoader = ProjectLoader as jest.MockedClass<typeof ProjectLoader>
18
- const MockDependencyGraph = DependencyGraph as jest.MockedClass<typeof DependencyGraph>
19
- const MockExecutionEngine = ExecutionEngine as jest.MockedClass<typeof ExecutionEngine>
20
- const MockExecutionContext = ExecutionContext as jest.MockedClass<typeof ExecutionContext>
21
-
22
- describe('Deployer', () => {
23
- let deployerOptions: DeployerOptions
24
- let mockNetwork1: Network
25
- let mockNetwork2: Network
26
- let mockJob1: Job
27
- let mockJob2: Job
28
- let mockJob3: Job
29
- let deprecatedJob: Job
30
- let mockTemplate1: Template
31
- let mockLoader: jest.Mocked<ProjectLoader>
32
- let mockGraph: jest.Mocked<DependencyGraph>
33
- let mockEngine: jest.Mocked<ExecutionEngine>
34
- let mockContext: jest.Mocked<ExecutionContext>
35
-
36
- beforeEach(() => {
37
- // Clear all mocks
38
- jest.clearAllMocks()
39
-
40
- // Setup mock networks
41
- mockNetwork1 = { name: 'mainnet', chainId: 1, rpcUrl: 'https://eth.rpc' }
42
- mockNetwork2 = { name: 'polygon', chainId: 137, rpcUrl: 'https://polygon.rpc' }
43
-
44
- // Setup mock jobs
45
- mockJob1 = {
46
- name: 'job1',
47
- version: '1.0.0',
48
- description: 'First job',
49
- actions: [
50
- { name: 'action1', template: 'template1', arguments: {} }
51
- ]
52
- }
53
-
54
- mockJob2 = {
55
- name: 'job2',
56
- version: '1.0.0',
57
- description: 'Second job',
58
- depends_on: ['job1'],
59
- actions: [
60
- { name: 'action2', template: 'template1', arguments: {} }
61
- ]
62
- }
63
-
64
- mockJob3 = {
65
- name: 'job3',
66
- version: '1.0.0',
67
- description: 'Third job with network filters',
68
- only_networks: [1], // Only mainnet
69
- actions: [
70
- { name: 'action3', template: 'template1', arguments: {} }
71
- ]
72
- }
73
-
74
- deprecatedJob = {
75
- name: 'legacy-job',
76
- version: '0.1.0',
77
- description: 'Deprecated job',
78
- deprecated: true,
79
- actions: [
80
- { name: 'legacy-action', template: 'template1', arguments: {} }
81
- ]
82
- }
83
-
84
- // Setup mock template
85
- mockTemplate1 = {
86
- name: 'template1',
87
- actions: [
88
- { type: 'send-transaction', arguments: {} }
89
- ]
90
- }
91
-
92
- // Basic deployer options
93
- deployerOptions = {
94
- projectRoot: '/test/project',
95
- privateKey: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
96
- networks: [mockNetwork1, mockNetwork2],
97
- flatOutput: true
98
- }
99
-
100
- // Setup mocks
101
- mockLoader = {
102
- load: jest.fn(),
103
- jobs: new Map([
104
- ['job1', mockJob1],
105
- ['job2', mockJob2],
106
- ['job3', mockJob3]
107
- ]),
108
- templates: new Map([
109
- ['template1', mockTemplate1]
110
- ]),
111
- contractRepository: {} as any
112
- } as any
113
-
114
- mockGraph = {
115
- getExecutionOrder: jest.fn().mockReturnValue(['job1', 'job2', 'job3']),
116
- getDependencies: jest.fn().mockReturnValue(new Set())
117
- } as any
118
-
119
- mockEngine = {
120
- executeJob: jest.fn().mockResolvedValue(undefined),
121
- getVerificationWarnings: jest.fn().mockReturnValue([])
122
- } as any
123
-
124
- mockContext = {
125
- getOutputs: jest.fn().mockReturnValue(new Map<string, any>([
126
- ['action1.hash', '0xhash1'],
127
- ['action1.receipt', { status: 1 }]
128
- ])),
129
- dispose: jest.fn().mockResolvedValue(undefined),
130
- setOutput: jest.fn(),
131
- getOutput: jest.fn()
132
- } as any
133
-
134
- MockProjectLoader.mockImplementation(() => mockLoader)
135
- MockDependencyGraph.mockImplementation(() => mockGraph)
136
- MockExecutionEngine.mockImplementation(() => mockEngine)
137
- MockExecutionContext.mockImplementation(() => mockContext)
138
-
139
- // Mock fs operations
140
- mockFs.mkdir.mockResolvedValue(undefined)
141
- mockFs.writeFile.mockResolvedValue(undefined)
142
-
143
- // Mock console methods to prevent test output pollution
144
- jest.spyOn(console, 'log').mockImplementation()
145
- jest.spyOn(console, 'error').mockImplementation()
146
- })
147
-
148
- afterEach(() => {
149
- jest.clearAllMocks()
150
- jest.restoreAllMocks()
151
- })
152
-
153
- describe('constructor', () => {
154
- it('should create a deployer with valid options', () => {
155
- const deployer = new Deployer(deployerOptions)
156
- expect(deployer).toBeInstanceOf(Deployer)
157
- })
158
-
159
- it('should initialize ProjectLoader with correct project root', () => {
160
- new Deployer(deployerOptions)
161
- expect(MockProjectLoader).toHaveBeenCalledWith('/test/project', undefined)
162
- })
163
- })
164
-
165
- describe('run', () => {
166
- describe('happy paths', () => {
167
- it('should successfully run a simple deployment', async () => {
168
- const deployer = new Deployer(deployerOptions)
169
-
170
- await deployer.run()
171
-
172
- // Verify the flow
173
- expect(mockLoader.load).toHaveBeenCalledTimes(1)
174
- expect(MockDependencyGraph).toHaveBeenCalledWith(mockLoader.jobs, mockLoader.templates)
175
- expect(mockGraph.getExecutionOrder).toHaveBeenCalledTimes(1)
176
- expect(MockExecutionEngine).toHaveBeenCalledWith(mockLoader.templates, expect.any(Object))
177
- expect(mockEngine.executeJob).toHaveBeenCalledTimes(5) // job1&job2 on 2 networks + job3 on 1 network
178
- expect(MockExecutionContext).toHaveBeenCalledTimes(5)
179
- expect(mockFs.mkdir).toHaveBeenCalledWith('/test/project/output', { recursive: true })
180
- expect(mockFs.writeFile).toHaveBeenCalledTimes(3) // One file per job
181
- })
182
-
183
- it('should run only specified jobs and their dependencies', async () => {
184
- // Mock getDependencies for this specific test
185
- mockGraph.getDependencies.mockImplementation((jobName: string) => {
186
- if (jobName === 'job2') return new Set(['job1'])
187
- return new Set()
188
- })
189
-
190
- const options: DeployerOptions = {
191
- ...deployerOptions,
192
- runJobs: ['job2'] // This should also include job1 due to dependency
193
- }
194
-
195
- const deployer = new Deployer(options)
196
- await deployer.run()
197
-
198
- // Should execute job1 (dependency) and job2, but not job3
199
- expect(mockEngine.executeJob).toHaveBeenCalledTimes(4) // 2 jobs × 2 networks
200
-
201
- // Verify it was called with the right jobs
202
- const executedJobs = mockEngine.executeJob.mock.calls.map(call => call[0].name)
203
- expect(executedJobs).toContain('job1')
204
- expect(executedJobs).toContain('job2')
205
- expect(executedJobs).not.toContain('job3')
206
- })
207
-
208
- it('should run only on specified networks', async () => {
209
- const options: DeployerOptions = {
210
- ...deployerOptions,
211
- runOnNetworks: [1] // Only mainnet
212
- }
213
-
214
- const deployer = new Deployer(options)
215
- await deployer.run()
216
-
217
- expect(mockEngine.executeJob).toHaveBeenCalledTimes(3) // 3 jobs × 1 network
218
-
219
- // Verify all calls were with mainnet
220
- const usedNetworks = MockExecutionContext.mock.calls.map(call => call[0])
221
- expect(usedNetworks).toHaveLength(3)
222
- usedNetworks.forEach(network => {
223
- expect(network.chainId).toBe(1)
224
- })
225
- })
226
-
227
- it('should skip jobs based on network filters', async () => {
228
- const deployer = new Deployer(deployerOptions)
229
- await deployer.run()
230
-
231
- // job3 has only_networks: [1], so should only run on mainnet
232
- const job3Calls = mockEngine.executeJob.mock.calls.filter(call => call[0].name === 'job3')
233
- expect(job3Calls).toHaveLength(1) // Only on mainnet
234
-
235
- // Verify it was called with mainnet (check the MockExecutionContext calls)
236
- const contextCallsForJob3 = MockExecutionContext.mock.calls.filter((_, index) => {
237
- const engineCall = mockEngine.executeJob.mock.calls[index]
238
- return engineCall && engineCall[0].name === 'job3'
239
- })
240
- expect(contextCallsForJob3[0][0].chainId).toBe(1)
241
- })
242
-
243
- it('should handle jobs with skip_networks filter', async () => {
244
- const jobWithSkipNetworks: Job = {
245
- ...mockJob1,
246
- name: 'job-skip-polygon',
247
- skip_networks: [137] // Skip polygon
248
- }
249
-
250
- mockLoader.jobs.set('job-skip-polygon', jobWithSkipNetworks)
251
- mockGraph.getExecutionOrder.mockReturnValue(['job-skip-polygon'])
252
-
253
- const deployer = new Deployer(deployerOptions)
254
- await deployer.run()
255
-
256
- // Should only run on mainnet (chainId 1), not polygon (chainId 137)
257
- expect(mockEngine.executeJob).toHaveBeenCalledTimes(1)
258
- const usedNetwork = MockExecutionContext.mock.calls[0][0]
259
- expect(usedNetwork.chainId).toBe(1)
260
- })
261
-
262
- it('should create correct output files in flat mode', async () => {
263
- const deployer = new Deployer({ ...deployerOptions, flatOutput: true })
264
- await deployer.run()
265
-
266
- // Verify output directory creation
267
- expect(mockFs.mkdir).toHaveBeenCalledWith('/test/project/output', { recursive: true })
268
-
269
- // Verify output files (flat)
270
- expect(mockFs.writeFile).toHaveBeenCalledTimes(3)
271
-
272
- // Check job1 output file (flat path)
273
- const job1OutputCall = mockFs.writeFile.mock.calls.find(call =>
274
- call[0] === '/test/project/output/job1.json'
275
- )
276
- expect(job1OutputCall).toBeDefined()
277
-
278
- const job1Content = JSON.parse(job1OutputCall![1] as string)
279
- expect(job1Content).toMatchObject({
280
- jobName: 'job1',
281
- jobVersion: '1.0.0',
282
- lastRun: expect.any(String),
283
- networks: [
284
- {
285
- status: 'success',
286
- chainIds: expect.arrayContaining(['1', '137']),
287
- outputs: expect.any(Object)
288
- }
289
- ]
290
- })
291
- })
292
-
293
- it('should mirror jobs directory structure by default', async () => {
294
- // Attach source paths to jobs to simulate their locations
295
- const job1 = mockLoader.jobs.get('job1') as any
296
- const job2 = mockLoader.jobs.get('job2') as any
297
- const job3 = mockLoader.jobs.get('job3') as any
298
- job1._path = '/test/project/jobs/core/job1.yaml'
299
- job2._path = '/test/project/jobs/patches/job2.yml'
300
- job3._path = '/test/project/jobs/job3.yaml'
301
-
302
- const deployer = new Deployer({ ...deployerOptions, flatOutput: undefined })
303
- await deployer.run()
304
-
305
- // Should create nested directories
306
- expect(mockFs.mkdir).toHaveBeenCalledWith('/test/project/output/core', { recursive: true })
307
- expect(mockFs.mkdir).toHaveBeenCalledWith('/test/project/output/patches', { recursive: true })
308
-
309
- // job1.json under core
310
- const job1OutputCall = mockFs.writeFile.mock.calls.find(call =>
311
- call[0] === '/test/project/output/core/job1.json'
312
- )
313
- expect(job1OutputCall).toBeDefined()
314
-
315
- // job2.json under patches
316
- const job2OutputCall = mockFs.writeFile.mock.calls.find(call =>
317
- call[0] === '/test/project/output/patches/job2.json'
318
- )
319
- expect(job2OutputCall).toBeDefined()
320
-
321
- // job3 at root (no subdir)
322
- const job3OutputCall = mockFs.writeFile.mock.calls.find(call =>
323
- call[0] === '/test/project/output/job3.json'
324
- )
325
- expect(job3OutputCall).toBeDefined()
326
- })
327
-
328
- it('should handle empty project gracefully', async () => {
329
- mockLoader.jobs.clear()
330
- mockLoader.templates.clear()
331
- mockGraph.getExecutionOrder.mockReturnValue([])
332
-
333
- const deployer = new Deployer(deployerOptions)
334
- await deployer.run()
335
-
336
- expect(mockEngine.executeJob).not.toHaveBeenCalled()
337
- expect(mockFs.writeFile).not.toHaveBeenCalled()
338
- })
339
-
340
- it('should filter outputs based on action output flags', async () => {
341
- // Create a job with mixed output flags
342
- const jobWithOutputFlags: Job = {
343
- name: 'job-with-output-flags',
344
- version: '1.0.0',
345
- description: 'Job with output filtering',
346
- actions: [
347
- { name: 'deploy-action', template: 'template1', arguments: {}, output: true },
348
- { name: 'verify-action', template: 'template1', arguments: {}, output: false },
349
- { name: 'other-action', template: 'template1', arguments: {} } // no output flag
350
- ]
351
- }
352
-
353
- mockLoader.jobs.clear()
354
- mockLoader.jobs.set('job-with-output-flags', jobWithOutputFlags)
355
- mockGraph.getExecutionOrder.mockReturnValue(['job-with-output-flags'])
356
-
357
- // Mock context to return outputs from all actions
358
- mockContext.getOutputs.mockReturnValue(new Map<string, any>([
359
- ['deploy-action.address', '0xdeployaddress'],
360
- ['deploy-action.hash', '0xdeployhash'],
361
- ['verify-action.guid', 'verification-guid'],
362
- ['other-action.result', 'some-result']
363
- ]))
364
-
365
- const deployer = new Deployer(deployerOptions)
366
- await deployer.run()
367
-
368
- // Verify output file was written
369
- expect(mockFs.writeFile).toHaveBeenCalledTimes(1)
370
-
371
- const outputCall = mockFs.writeFile.mock.calls[0]
372
- expect(outputCall[0]).toBe('/test/project/output/job-with-output-flags.json')
373
-
374
- const outputContent = JSON.parse(outputCall[1] as string)
375
- expect(outputContent.networks).toHaveLength(1)
376
- expect(outputContent.networks[0].status).toBe('success')
377
-
378
- // Should only include outputs from deploy-action (output: true)
379
- // Should NOT include verify-action (output: false) or other-action (no flag)
380
- expect(outputContent.networks[0].outputs).toEqual({
381
- 'deploy-action.address': '0xdeployaddress',
382
- 'deploy-action.hash': '0xdeployhash'
383
- })
384
- })
385
-
386
- it('should include all outputs when no actions have output: true (backward compatibility)', async () => {
387
- // Create a job where no actions explicitly set output: true
388
- const jobWithoutOutputFlags: Job = {
389
- name: 'job-without-output-flags',
390
- version: '1.0.0',
391
- description: 'Job without output flags',
392
- actions: [
393
- { name: 'action1', template: 'template1', arguments: {} },
394
- { name: 'action2', template: 'template1', arguments: {}, output: false }
395
- ]
396
- }
397
-
398
- mockLoader.jobs.clear()
399
- mockLoader.jobs.set('job-without-output-flags', jobWithoutOutputFlags)
400
- mockGraph.getExecutionOrder.mockReturnValue(['job-without-output-flags'])
401
-
402
- // Mock context to return outputs from all actions
403
- mockContext.getOutputs.mockReturnValue(new Map<string, any>([
404
- ['action1.result', 'result1'],
405
- ['action2.result', 'result2']
406
- ]))
407
-
408
- const deployer = new Deployer(deployerOptions)
409
- await deployer.run()
410
-
411
- // Verify output file was written
412
- expect(mockFs.writeFile).toHaveBeenCalledTimes(1)
413
-
414
- const outputCall = mockFs.writeFile.mock.calls[0]
415
- const outputContent = JSON.parse(outputCall[1] as string)
416
-
417
- // Should include all outputs (backward compatibility)
418
- expect(outputContent.networks[0].outputs).toEqual({
419
- 'action1.result': 'result1',
420
- 'action2.result': 'result2'
421
- })
422
- })
423
-
424
- it('should filter outputs correctly when multiple actions have output: true', async () => {
425
- // Create a job with multiple actions marked for output
426
- const jobWithMultipleOutputs: Job = {
427
- name: 'job-multiple-outputs',
428
- version: '1.0.0',
429
- description: 'Job with multiple output actions',
430
- actions: [
431
- { name: 'deploy1', template: 'template1', arguments: {}, output: true },
432
- { name: 'deploy2', template: 'template1', arguments: {}, output: true },
433
- { name: 'verify1', template: 'template1', arguments: {}, output: false },
434
- { name: 'verify2', template: 'template1', arguments: {}, output: false }
435
- ]
436
- }
437
-
438
- mockLoader.jobs.clear()
439
- mockLoader.jobs.set('job-multiple-outputs', jobWithMultipleOutputs)
440
- mockGraph.getExecutionOrder.mockReturnValue(['job-multiple-outputs'])
441
-
442
- // Mock context to return outputs from all actions
443
- mockContext.getOutputs.mockReturnValue(new Map<string, any>([
444
- ['deploy1.address', '0xdeploy1'],
445
- ['deploy2.address', '0xdeploy2'],
446
- ['verify1.guid', 'verify1-guid'],
447
- ['verify2.guid', 'verify2-guid']
448
- ]))
449
-
450
- const deployer = new Deployer(deployerOptions)
451
- await deployer.run()
452
-
453
- // Verify output file was written
454
- const outputCall = mockFs.writeFile.mock.calls[0]
455
- const outputContent = JSON.parse(outputCall[1] as string)
456
-
457
- // Should include outputs from both deploy actions, but not verify actions
458
- expect(outputContent.networks[0].outputs).toEqual({
459
- 'deploy1.address': '0xdeploy1',
460
- 'deploy2.address': '0xdeploy2'
461
- })
462
- })
463
-
464
- it('should keep outputs empty when only skipped opt-in actions produce no keys', async () => {
465
- const jobWithSkippedOptIn: Job = {
466
- name: 'job-skipped-opt-in',
467
- version: '1.0.0',
468
- description: 'Job where the only opted-in action produced no outputs',
469
- actions: [
470
- { name: 'deploy-action', template: 'template1', arguments: {}, output: false },
471
- { name: 'skipped-action', template: 'template1', arguments: {}, output: true }
472
- ]
473
- }
474
-
475
- mockLoader.jobs.clear()
476
- mockLoader.jobs.set('job-skipped-opt-in', jobWithSkippedOptIn)
477
- mockGraph.getExecutionOrder.mockReturnValue(['job-skipped-opt-in'])
478
-
479
- mockContext.getOutputs.mockReturnValue(new Map<string, any>([
480
- ['deploy-action.address', '0xdeployaddress']
481
- ]))
482
-
483
- const deployer = new Deployer(deployerOptions)
484
- await deployer.run()
485
-
486
- const outputCall = mockFs.writeFile.mock.calls[0]
487
- const outputContent = JSON.parse(outputCall[1] as string)
488
-
489
- expect(outputContent.networks[0].outputs).toEqual({})
490
- })
491
- })
492
-
493
- describe('error handling', () => {
494
- it('should throw when project loading fails', async () => {
495
- mockLoader.load.mockRejectedValue(new Error('Failed to load project'))
496
-
497
- const deployer = new Deployer(deployerOptions)
498
-
499
- await expect(deployer.run()).rejects.toThrow('Failed to load project')
500
- // Note: Error handling is now done via events, not console.error directly
501
- })
502
-
503
- it('should throw when dependency graph creation fails', async () => {
504
- MockDependencyGraph.mockImplementation(() => {
505
- throw new Error('Circular dependency detected')
506
- })
507
-
508
- const deployer = new Deployer(deployerOptions)
509
-
510
- await expect(deployer.run()).rejects.toThrow('Circular dependency detected')
511
- })
512
-
513
- it('should capture job execution failures and then throw', async () => {
514
- mockEngine.executeJob.mockRejectedValue(new Error('Transaction failed'))
515
-
516
- const deployer = new Deployer(deployerOptions)
517
-
518
- await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
519
-
520
- // Should still write output files with error entries before throwing
521
- expect(mockFs.writeFile).toHaveBeenCalled()
522
-
523
- // Check that error entries are recorded
524
- const writeFileCalls = mockFs.writeFile.mock.calls
525
- const outputFile = writeFileCalls[0]
526
- const outputContent = JSON.parse(outputFile[1] as string)
527
-
528
- // Should have error entries for failed executions
529
- const errorEntries = outputContent.networks.filter((entry: any) => entry.status === 'error')
530
- expect(errorEntries.length).toBeGreaterThan(0)
531
- expect(errorEntries[0].error).toBe('Transaction failed')
532
- })
533
-
534
- it('should throw when output directory creation fails', async () => {
535
- mockFs.mkdir.mockRejectedValue(new Error('Permission denied'))
536
-
537
- const deployer = new Deployer(deployerOptions)
538
-
539
- await expect(deployer.run()).rejects.toThrow('Permission denied')
540
- })
541
-
542
- it('should throw when output file writing fails', async () => {
543
- mockFs.writeFile.mockRejectedValue(new Error('Disk full'))
544
-
545
- const deployer = new Deployer(deployerOptions)
546
-
547
- await expect(deployer.run()).rejects.toThrow('Disk full')
548
- })
549
-
550
- it('should handle execution context creation failure and then throw', async () => {
551
- MockExecutionContext.mockImplementation(() => {
552
- throw new Error('Invalid private key')
553
- })
554
-
555
- const deployer = new Deployer(deployerOptions)
556
-
557
- await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
558
-
559
- // Should record context creation failures as error entries before throwing
560
- const writeFileCalls = mockFs.writeFile.mock.calls
561
- const outputFile = writeFileCalls[0]
562
- const outputContent = JSON.parse(outputFile[1] as string)
563
-
564
- const errorEntries = outputContent.networks.filter((entry: any) => entry.status === 'error')
565
- expect(errorEntries.length).toBeGreaterThan(0)
566
- expect(errorEntries[0].error).toBe('Invalid private key')
567
- })
568
- })
569
-
570
- describe('edge cases and weird scenarios', () => {
571
- it('should handle job with only_networks that includes non-existent network', async () => {
572
- const weirdJob: Job = {
573
- ...mockJob1,
574
- name: 'weird-job',
575
- only_networks: [999] // Non-existent network
576
- }
577
-
578
- mockLoader.jobs.clear()
579
- mockLoader.jobs.set('weird-job', weirdJob)
580
- mockGraph.getExecutionOrder.mockReturnValue(['weird-job'])
581
-
582
- const deployer = new Deployer(deployerOptions)
583
- await deployer.run()
584
-
585
- // Should not execute on any network
586
- expect(mockEngine.executeJob).not.toHaveBeenCalled()
587
- })
588
-
589
- it('should handle job with skip_networks that includes all networks', async () => {
590
- const weirdJob: Job = {
591
- ...mockJob1,
592
- name: 'weird-job',
593
- skip_networks: [1, 137] // Skip all available networks
594
- }
595
-
596
- mockLoader.jobs.clear()
597
- mockLoader.jobs.set('weird-job', weirdJob)
598
- mockGraph.getExecutionOrder.mockReturnValue(['weird-job'])
599
-
600
- const deployer = new Deployer(deployerOptions)
601
- await deployer.run()
602
-
603
- // Should not execute on any network
604
- expect(mockEngine.executeJob).not.toHaveBeenCalled()
605
- })
606
-
607
- it('should handle runOnNetworks with non-existent chain IDs', async () => {
608
- const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
609
-
610
- const options: DeployerOptions = {
611
- ...deployerOptions,
612
- runOnNetworks: [1, 999, 888] // 999 and 888 don't exist
613
- }
614
-
615
- const deployer = new Deployer(options)
616
- await deployer.run()
617
-
618
- // Note: Warnings are now emitted as events, not console.warn directly
619
- // The CLI adapter converts events to console output
620
-
621
- // Should only execute on the existing network (chainId 1)
622
- expect(mockEngine.executeJob).toHaveBeenCalledTimes(3) // 3 jobs × 1 network
623
- })
624
-
625
- it('should handle runJobs with non-existent job names', async () => {
626
- const options: DeployerOptions = {
627
- ...deployerOptions,
628
- runJobs: ['non-existent-job']
629
- }
630
-
631
- const deployer = new Deployer(options)
632
-
633
- await expect(deployer.run()).rejects.toThrow(
634
- 'Specified job "non-existent-job" not found in project.'
635
- )
636
- })
637
-
638
- it('should handle execution context without getOutputs method and then throw', async () => {
639
- const brokenContext = {
640
- // Missing getOutputs method
641
- } as any
642
-
643
- MockExecutionContext.mockImplementation(() => brokenContext)
644
-
645
- const deployer = new Deployer(deployerOptions)
646
-
647
- await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
648
-
649
- // Should record the missing method error before throwing
650
- const writeFileCalls = mockFs.writeFile.mock.calls
651
- const outputFile = writeFileCalls[0]
652
- const outputContent = JSON.parse(outputFile[1] as string)
653
-
654
- const errorEntries = outputContent.networks.filter((entry: any) => entry.status === 'error')
655
- expect(errorEntries.length).toBeGreaterThan(0)
656
- })
657
-
658
- it('should handle empty networks array', async () => {
659
- const options: DeployerOptions = {
660
- ...deployerOptions,
661
- networks: []
662
- }
663
-
664
- const deployer = new Deployer(options)
665
- await deployer.run()
666
-
667
- // Should not execute anything
668
- expect(mockEngine.executeJob).not.toHaveBeenCalled()
669
- expect(mockFs.writeFile).not.toHaveBeenCalled()
670
- })
671
-
672
- it('should handle empty runJobs array', async () => {
673
- const options: DeployerOptions = {
674
- ...deployerOptions,
675
- runJobs: []
676
- }
677
-
678
- const deployer = new Deployer(options)
679
- await deployer.run()
680
-
681
- // Should run all jobs
682
- expect(mockEngine.executeJob).toHaveBeenCalledTimes(5) // job1&job2 on 2 networks + job3 on 1 network
683
- })
684
-
685
- it('should handle empty runOnNetworks array', async () => {
686
- const options: DeployerOptions = {
687
- ...deployerOptions,
688
- runOnNetworks: []
689
- }
690
-
691
- const deployer = new Deployer(options)
692
- await deployer.run()
693
-
694
- // Should run on all networks
695
- expect(mockEngine.executeJob).toHaveBeenCalledTimes(5) // job1&job2 on 2 networks + job3 on 1 network
696
- })
697
-
698
- it('should handle job with both only_networks and skip_networks', async () => {
699
- const conflictedJob: Job = {
700
- ...mockJob1,
701
- name: 'conflicted-job',
702
- only_networks: [1, 137],
703
- skip_networks: [137]
704
- }
705
-
706
- mockLoader.jobs.clear()
707
- mockLoader.jobs.set('conflicted-job', conflictedJob)
708
- mockGraph.getExecutionOrder.mockReturnValue(['conflicted-job'])
709
-
710
- const deployer = new Deployer(deployerOptions)
711
- await deployer.run()
712
-
713
- // only_networks takes precedence, so should run on [1, 137]
714
- // skip_networks is ignored when only_networks is present
715
- expect(mockEngine.executeJob).toHaveBeenCalledTimes(2)
716
- const usedNetworks = MockExecutionContext.mock.calls.map(call => call[0].chainId)
717
- expect(usedNetworks).toEqual(expect.arrayContaining([1, 137]))
718
- })
719
-
720
- it('should write output files even when all executions fail and then throw', async () => {
721
- // Make all executions fail
722
- mockEngine.executeJob.mockImplementation(() => {
723
- throw new Error('Execution failed')
724
- })
725
-
726
- const deployer = new Deployer(deployerOptions)
727
-
728
- await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
729
-
730
- // Should write output files with error entries before throwing
731
- expect(mockFs.writeFile).toHaveBeenCalled()
732
-
733
- const writeFileCalls = mockFs.writeFile.mock.calls
734
- const outputFile = writeFileCalls[0]
735
- const outputContent = JSON.parse(outputFile[1] as string)
736
-
737
- // All entries should be error entries
738
- const errorEntries = outputContent.networks.filter((entry: any) => entry.status === 'error')
739
- expect(errorEntries.length).toBeGreaterThan(0)
740
-
741
- // No success entries
742
- const successEntries = outputContent.networks.filter((entry: any) => entry.status === 'success')
743
- expect(successEntries.length).toBe(0)
744
- })
745
-
746
- it('should handle very long execution order', async () => {
747
- // Create 100 jobs to test performance/memory
748
- const manyJobs = Array.from({ length: 100 }, (_, i) => `job${i}`)
749
- mockGraph.getExecutionOrder.mockReturnValue(manyJobs)
750
-
751
- // Mock loader to have all these jobs
752
- for (let i = 0; i < 100; i++) {
753
- mockLoader.jobs.set(`job${i}`, {
754
- ...mockJob1,
755
- name: `job${i}`
756
- })
757
- }
758
-
759
- const deployer = new Deployer(deployerOptions)
760
- await deployer.run()
761
-
762
- expect(mockEngine.executeJob).toHaveBeenCalledTimes(200) // 100 jobs × 2 networks
763
- expect(mockFs.writeFile).toHaveBeenCalledTimes(100) // One file per job
764
- })
765
- })
766
-
767
- describe('private method testing', () => {
768
- let deployer: Deployer
769
-
770
- beforeEach(() => {
771
- deployer = new Deployer(deployerOptions)
772
- })
773
-
774
- describe('getJobExecutionPlan', () => {
775
- it('should return full order when no runJobs specified', () => {
776
- const fullOrder = ['job1', 'job2', 'job3']
777
- const plan = (deployer as any).getJobExecutionPlan(fullOrder)
778
- expect(plan).toEqual(fullOrder)
779
- })
780
-
781
- it('should filter and include dependencies', async () => {
782
- const options: DeployerOptions = {
783
- ...deployerOptions,
784
- runJobs: ['job2']
785
- }
786
- const deployer = new Deployer(options)
787
-
788
- // Initialize the deployer's graph by calling load
789
- await mockLoader.load()
790
- ;(deployer as any).graph = mockGraph
791
-
792
- // Mock getDependencies to return job1 as dependency of job2
793
- mockGraph.getDependencies.mockReturnValueOnce(new Set(['job1']))
794
-
795
- const fullOrder = ['job1', 'job2', 'job3']
796
- const plan = (deployer as any).getJobExecutionPlan(fullOrder)
797
- expect(plan).toEqual(['job1', 'job2'])
798
- })
799
-
800
- it('should include deprecated dependencies when no runJobs specified', () => {
801
- // Add deprecated job and make job2 depend on it transitively
802
- ;(mockLoader.jobs as Map<string, Job>).set('legacy-job', deprecatedJob)
803
-
804
- // full order includes all
805
- const fullOrder = ['legacy-job', 'job1', 'job2', 'job3']
806
-
807
- // Mock dependency graph: job2 depends on job1 and legacy-job
808
- mockGraph.getDependencies.mockImplementation((jobName: string) => {
809
- if (jobName === 'job2') return new Set(['job1', 'legacy-job'])
810
- return new Set()
811
- })
812
- ;(deployer as any).graph = mockGraph
813
- const plan = (deployer as any).getJobExecutionPlan(fullOrder)
814
- // Expect legacy-job to be included because it is a dependency of non-deprecated job2
815
- expect(plan).toEqual(['legacy-job', 'job1', 'job2', 'job3'])
816
- })
817
-
818
- it('should keep deprecated dependencies when specific jobs are requested', async () => {
819
- // Add deprecated job and dependency relation
820
- ;(mockLoader.jobs as Map<string, Job>).set('legacy-job', deprecatedJob)
821
- const options: DeployerOptions = {
822
- ...deployerOptions,
823
- runJobs: ['job2']
824
- }
825
- const depDeployer = new Deployer(options)
826
- ;(depDeployer as any).graph = mockGraph
827
-
828
- mockGraph.getDependencies.mockImplementation((jobName: string) => {
829
- if (jobName === 'job2') return new Set(['job1', 'legacy-job'])
830
- return new Set()
831
- })
832
-
833
- const fullOrder = ['legacy-job', 'job1', 'job2', 'job3']
834
- const plan = (depDeployer as any).getJobExecutionPlan(fullOrder)
835
- expect(plan).toEqual(['legacy-job', 'job1', 'job2'])
836
- })
837
-
838
- it('should expand wildcard patterns in runJobs and preserve execution order', async () => {
839
- ;(mockLoader.jobs as Map<string, Job>).set('job10', { ...mockJob1, name: 'job10' })
840
- ;(mockLoader.jobs as Map<string, Job>).set('another', { ...mockJob1, name: 'another' })
841
-
842
- const fullOrder = ['another', 'job1', 'job2', 'job3', 'job10']
843
- mockGraph.getExecutionOrder.mockReturnValue(fullOrder)
844
-
845
- const options: DeployerOptions = {
846
- ...deployerOptions,
847
- runJobs: ['job*']
848
- }
849
- const dep = new Deployer(options)
850
- ;(dep as any).loader = mockLoader
851
- ;(dep as any).graph = mockGraph
852
-
853
- const plan = (dep as any).getJobExecutionPlan(fullOrder)
854
- expect(plan).toEqual(['job1', 'job2', 'job3', 'job10'])
855
- })
856
-
857
- it('should support mixed exact names and patterns', async () => {
858
- const fullOrder = ['job1', 'job2', 'job3']
859
- mockGraph.getExecutionOrder.mockReturnValue(fullOrder)
860
-
861
- const options: DeployerOptions = {
862
- ...deployerOptions,
863
- runJobs: ['job1', 'job?']
864
- }
865
- const dep = new Deployer(options)
866
- ;(dep as any).loader = mockLoader
867
- ;(dep as any).graph = mockGraph
868
-
869
- const plan = (dep as any).getJobExecutionPlan(fullOrder)
870
- expect(plan).toEqual(['job1', 'job2', 'job3'])
871
- })
872
-
873
- it('should throw when a pattern matches no jobs', async () => {
874
- const fullOrder = ['job1', 'job2', 'job3']
875
- mockGraph.getExecutionOrder.mockReturnValue(fullOrder)
876
-
877
- const options: DeployerOptions = {
878
- ...deployerOptions,
879
- runJobs: ['does-not-exist*']
880
- }
881
- const dep = new Deployer(options)
882
- ;(dep as any).loader = mockLoader
883
- ;(dep as any).graph = mockGraph
884
-
885
- expect(() => (dep as any).getJobExecutionPlan(fullOrder)).toThrow(
886
- 'Job pattern "does-not-exist*" did not match any jobs in project.'
887
- )
888
- })
889
-
890
- it('should match names containing slashes with patterns', async () => {
891
- const jA: Job = { ...mockJob1, name: 'sequence_v3/beta_4' }
892
- const jB: Job = { ...mockJob1, name: 'sequence_v3/rc_1' }
893
- ;(mockLoader.jobs as Map<string, Job>).set(jA.name, jA)
894
- ;(mockLoader.jobs as Map<string, Job>).set(jB.name, jB)
895
-
896
- const fullOrder = ['job1', jA.name, jB.name, 'job2']
897
- mockGraph.getExecutionOrder.mockReturnValue(fullOrder)
898
-
899
- const options: DeployerOptions = {
900
- ...deployerOptions,
901
- runJobs: ['sequence_v3/*']
902
- }
903
- const dep = new Deployer(options)
904
- ;(dep as any).loader = mockLoader
905
- ;(dep as any).graph = mockGraph
906
-
907
- const plan = (dep as any).getJobExecutionPlan(fullOrder)
908
- expect(plan).toEqual(['sequence_v3/beta_4', 'sequence_v3/rc_1'])
909
- })
910
- })
911
-
912
- describe('getTargetNetworks', () => {
913
- it('should return all networks when no runOnNetworks specified', () => {
914
- const networks = (deployer as any).getTargetNetworks()
915
- expect(networks).toEqual([mockNetwork1, mockNetwork2])
916
- })
917
-
918
- it('should filter networks by chain ID', () => {
919
- const options: DeployerOptions = {
920
- ...deployerOptions,
921
- runOnNetworks: [1]
922
- }
923
- const deployer = new Deployer(options)
924
-
925
- const networks = (deployer as any).getTargetNetworks()
926
- expect(networks).toEqual([mockNetwork1])
927
- })
928
- })
929
-
930
- describe('shouldSkipJobOnNetwork', () => {
931
- it('should return false for job with no network filters', () => {
932
- const result = (deployer as any).shouldSkipJobOnNetwork(mockJob1, mockNetwork1)
933
- expect(result).toBe(false)
934
- })
935
-
936
- it('should return true when network not in only_networks', () => {
937
- const result = (deployer as any).shouldSkipJobOnNetwork(mockJob3, mockNetwork2)
938
- expect(result).toBe(true)
939
- })
940
-
941
- it('should return false when network is in only_networks', () => {
942
- const result = (deployer as any).shouldSkipJobOnNetwork(mockJob3, mockNetwork1)
943
- expect(result).toBe(false)
944
- })
945
-
946
- it('should return true when network is in skip_networks', () => {
947
- const jobWithSkip = {
948
- ...mockJob1,
949
- skip_networks: [1]
950
- }
951
- const result = (deployer as any).shouldSkipJobOnNetwork(jobWithSkip, mockNetwork1)
952
- expect(result).toBe(true)
953
- })
954
- })
955
- })
956
-
957
- describe('integration-like scenarios', () => {
958
- it('should handle complex dependency chain with network filtering', async () => {
959
- // Create a complex scenario:
960
- // job1 -> job2 -> job3
961
- // job3 only runs on mainnet
962
- // job4 skips polygon
963
- const job4: Job = {
964
- name: 'job4',
965
- version: '1.0.0',
966
- depends_on: ['job3'],
967
- skip_networks: [137],
968
- actions: [{ name: 'action4', template: 'template1', arguments: {} }]
969
- }
970
-
971
- mockLoader.jobs.set('job4', job4)
972
- mockGraph.getExecutionOrder.mockReturnValue(['job1', 'job2', 'job3', 'job4'])
973
-
974
- // Mock dependencies
975
- mockGraph.getDependencies
976
- .mockReturnValueOnce(new Set()) // job1 has no deps
977
- .mockReturnValueOnce(new Set(['job1'])) // job2 depends on job1
978
- .mockReturnValueOnce(new Set(['job1', 'job2'])) // job3 depends on job1, job2
979
- .mockReturnValueOnce(new Set(['job1', 'job2', 'job3'])) // job4 depends on all
980
-
981
- const deployer = new Deployer(deployerOptions)
982
- await deployer.run()
983
-
984
- // job1, job2: run on both networks (2 + 2 = 4)
985
- // job3: only mainnet (1)
986
- // job4: skip polygon, so only mainnet (1)
987
- // Total: 6 executions
988
- expect(mockEngine.executeJob).toHaveBeenCalledTimes(6)
989
-
990
- // Verify network distribution by checking ExecutionContext constructor calls
991
- const contextCalls = MockExecutionContext.mock.calls
992
- const mainnetCalls = contextCalls.filter(call => call[0].chainId === 1)
993
- const polygonCalls = contextCalls.filter(call => call[0].chainId === 137)
994
-
995
- expect(mainnetCalls).toHaveLength(4) // All jobs run on mainnet
996
- expect(polygonCalls).toHaveLength(2) // Only job1 and job2 run on polygon
997
- })
998
-
999
- it('should handle partial failure scenario', async () => {
1000
- // Make job2 fail on polygon only
1001
- let callCount = 0
1002
- mockEngine.executeJob.mockImplementation((job, context) => {
1003
- const currentCall = MockExecutionContext.mock.calls[callCount]
1004
- const network = currentCall ? currentCall[0] : null
1005
- callCount++
1006
-
1007
- if (job.name === 'job2' && network && network.chainId === 137) {
1008
- throw new Error('Polygon execution failed')
1009
- }
1010
- return Promise.resolve()
1011
- })
1012
-
1013
- const deployer = new Deployer(deployerOptions)
1014
-
1015
- await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
1016
-
1017
- // Should capture the partial failure in output files before throwing
1018
- const writeFileCalls = mockFs.writeFile.mock.calls
1019
- const job2Output = writeFileCalls.find(call =>
1020
- String(call[0]).includes('job2.json')
1021
- )
1022
-
1023
- if (job2Output) {
1024
- const job2Content = JSON.parse(job2Output[1] as string)
1025
- const errorEntries = job2Content.networks.filter((entry: any) => entry.status === 'error')
1026
- expect(errorEntries.some((entry: any) =>
1027
- entry.chainId === '137' && entry.error === 'Polygon execution failed'
1028
- )).toBe(true)
1029
- }
1030
- })
1031
-
1032
- it('should handle context output aggregation correctly', async () => {
1033
- // Mock different outputs for different networks
1034
- MockExecutionContext.mockImplementation((network) => ({
1035
- network,
1036
- getOutputs: jest.fn().mockReturnValue(new Map<string, any>([
1037
- [`action.hash`, `0xhash-${network.chainId}`],
1038
- [`action.receipt`, { status: 1, blockNumber: network.chainId * 100 }]
1039
- ])),
1040
- dispose: jest.fn().mockResolvedValue(undefined),
1041
- setOutput: jest.fn(),
1042
- getOutput: jest.fn()
1043
- } as any))
1044
-
1045
- const deployer = new Deployer(deployerOptions)
1046
- await deployer.run()
1047
-
1048
- // Verify outputs are correctly segregated by network since they have different outputs
1049
- const writeFileCalls = mockFs.writeFile.mock.calls
1050
- const job1Output = writeFileCalls.find(call =>
1051
- call[0] === '/test/project/output/job1.json'
1052
- )
1053
-
1054
- const job1Content = JSON.parse(job1Output![1] as string)
1055
- // Since outputs differ by network, they should be in separate entries
1056
- expect(job1Content.networks).toHaveLength(2)
1057
-
1058
- // Find entries for each network
1059
- const network1Entry = job1Content.networks.find((entry: any) =>
1060
- entry.chainIds && entry.chainIds.includes('1')
1061
- )
1062
- const network137Entry = job1Content.networks.find((entry: any) =>
1063
- entry.chainIds && entry.chainIds.includes('137')
1064
- )
1065
-
1066
- expect(network1Entry.outputs['action.hash']).toBe('0xhash-1')
1067
- expect(network137Entry.outputs['action.hash']).toBe('0xhash-137')
1068
- })
1069
-
1070
- it('should group networks with identical outputs together', async () => {
1071
- // Mock identical outputs for different networks
1072
- MockExecutionContext.mockImplementation(() => ({
1073
- getOutputs: jest.fn().mockReturnValue(new Map<string, any>([
1074
- [`contract.address`, `0x1234567890123456789012345678901234567890`],
1075
- [`contract.txHash`, `0xabcdef1234567890abcdef1234567890abcdef12`]
1076
- ])),
1077
- dispose: jest.fn().mockResolvedValue(undefined),
1078
- setOutput: jest.fn(),
1079
- getOutput: jest.fn()
1080
- } as any))
1081
-
1082
- const deployer = new Deployer(deployerOptions)
1083
- await deployer.run()
1084
-
1085
- // Verify identical outputs are grouped together
1086
- const writeFileCalls = mockFs.writeFile.mock.calls
1087
- const job1Output = writeFileCalls.find(call =>
1088
- call[0] === '/test/project/output/job1.json'
1089
- )
1090
-
1091
- const job1Content = JSON.parse(job1Output![1] as string)
1092
- // Since outputs are identical, they should be grouped into one entry
1093
- expect(job1Content.networks).toHaveLength(1)
1094
- expect(job1Content.networks[0].status).toBe('success')
1095
- expect(job1Content.networks[0].chainIds).toEqual(['1', '137'])
1096
- expect(job1Content.networks[0].outputs['contract.address']).toBe('0x1234567890123456789012345678901234567890')
1097
- })
1098
-
1099
- it('should handle partial failure scenario with proper grouping', async () => {
1100
- // Make job1 fail on polygon only
1101
- let callCount = 0
1102
- mockEngine.executeJob.mockImplementation((job, context) => {
1103
- const currentCall = MockExecutionContext.mock.calls[callCount]
1104
- const network = currentCall ? currentCall[0] : null
1105
- callCount++
1106
-
1107
- if (job.name === 'job1' && network && network.chainId === 137) {
1108
- throw new Error('Polygon execution failed')
1109
- }
1110
- return Promise.resolve()
1111
- })
1112
-
1113
- // Mock successful outputs for mainnet
1114
- MockExecutionContext.mockImplementation((network) => ({
1115
- network,
1116
- getOutputs: jest.fn().mockReturnValue(new Map<string, any>([
1117
- [`contract.address`, `0x1234567890123456789012345678901234567890`]
1118
- ])),
1119
- dispose: jest.fn().mockResolvedValue(undefined),
1120
- setOutput: jest.fn(),
1121
- getOutput: jest.fn()
1122
- } as any))
1123
-
1124
- const deployer = new Deployer(deployerOptions)
1125
- await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
1126
-
1127
- // Verify outputs show both success and error states before throwing
1128
- const writeFileCalls = mockFs.writeFile.mock.calls
1129
- const job1Output = writeFileCalls.find(call =>
1130
- call[0] === '/test/project/output/job1.json'
1131
- )
1132
-
1133
- const job1Content = JSON.parse(job1Output![1] as string)
1134
- expect(job1Content.networks).toHaveLength(2) // One success entry, one error entry
1135
-
1136
- // Find success and error entries
1137
- const successEntry = job1Content.networks.find((entry: any) => entry.status === 'success')
1138
- const errorEntry = job1Content.networks.find((entry: any) => entry.status === 'error')
1139
-
1140
- expect(successEntry).toBeDefined()
1141
- expect(successEntry.chainIds).toEqual(['1'])
1142
- expect(successEntry.outputs['contract.address']).toBe('0x1234567890123456789012345678901234567890')
1143
-
1144
- expect(errorEntry).toBeDefined()
1145
- expect(errorEntry.chainId).toBe('137')
1146
- expect(errorEntry.error).toBe('Polygon execution failed')
1147
- })
1148
- })
1149
- })
1150
-
1151
- describe('fail-early functionality', () => {
1152
- beforeEach(() => {
1153
- // Clear mock call counts for this test suite
1154
- mockEngine.executeJob.mockClear()
1155
- })
1156
-
1157
- it('should stop execution immediately when failEarly is true', async () => {
1158
- const options: DeployerOptions = {
1159
- ...deployerOptions,
1160
- runJobs: ['job1'], // Only run job1 to have predictable call count
1161
- failEarly: true
1162
- }
1163
-
1164
- // Make the first execution fail
1165
- mockEngine.executeJob.mockRejectedValueOnce(new Error('First job failed'))
1166
-
1167
- const deployer = new Deployer(options)
1168
-
1169
- await expect(deployer.run()).rejects.toThrow('First job failed')
1170
-
1171
- // Should only attempt the first execution, not continue to other networks/jobs
1172
- expect(mockEngine.executeJob).toHaveBeenCalledTimes(1)
1173
- })
1174
-
1175
- it('should continue through all jobs/networks when failEarly is false', async () => {
1176
- const options: DeployerOptions = {
1177
- ...deployerOptions,
1178
- runJobs: ['job1'], // Only run job1 to have predictable call count
1179
- failEarly: false // explicit false
1180
- }
1181
-
1182
- // Make the first execution fail but others succeed
1183
- mockEngine.executeJob.mockRejectedValueOnce(new Error('First job failed'))
1184
- mockEngine.executeJob.mockResolvedValue(undefined)
1185
-
1186
- const deployer = new Deployer(options)
1187
-
1188
- await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
1189
-
1190
- // Should attempt all executions (2 networks * 1 job = 2 calls)
1191
- expect(mockEngine.executeJob).toHaveBeenCalledTimes(2)
1192
- })
1193
-
1194
- it('should default to failEarly: false when option is not provided', async () => {
1195
- const options: DeployerOptions = {
1196
- ...deployerOptions,
1197
- runJobs: ['job1'] // Only run job1 to have predictable call count
1198
- // failEarly not specified, should default to false
1199
- }
1200
-
1201
- // Make the first execution fail but others succeed
1202
- mockEngine.executeJob.mockRejectedValueOnce(new Error('First job failed'))
1203
- mockEngine.executeJob.mockResolvedValue(undefined)
1204
-
1205
- const deployer = new Deployer(options)
1206
-
1207
- await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
1208
-
1209
- // Should attempt all executions
1210
- expect(mockEngine.executeJob).toHaveBeenCalledTimes(2)
1211
- })
1212
-
1213
- it('should not throw when all jobs succeed, regardless of failEarly setting', async () => {
1214
- const options: DeployerOptions = {
1215
- ...deployerOptions,
1216
- runJobs: ['job1'], // Only run job1 to have predictable call count
1217
- failEarly: true
1218
- }
1219
-
1220
- // All executions succeed
1221
- mockEngine.executeJob.mockResolvedValue(undefined)
1222
-
1223
- const deployer = new Deployer(options)
1224
-
1225
- await expect(deployer.run()).resolves.not.toThrow()
1226
-
1227
- // Should complete all executions
1228
- expect(mockEngine.executeJob).toHaveBeenCalledTimes(2)
1229
- })
1230
- })
1231
-
1232
- describe('ignore verify errors feature', () => {
1233
- it('should pass ignoreVerifyErrors option to ExecutionEngine', async () => {
1234
- const optionsWithIgnoreVerifyErrors = {
1235
- ...deployerOptions,
1236
- ignoreVerifyErrors: true
1237
- }
1238
-
1239
- const deployer = new Deployer(optionsWithIgnoreVerifyErrors)
1240
- await deployer.run()
1241
-
1242
- // Verify that ExecutionEngine was created with ignoreVerifyErrors option
1243
- expect(MockExecutionEngine).toHaveBeenCalledWith(
1244
- expect.anything(),
1245
- expect.objectContaining({
1246
- ignoreVerifyErrors: true
1247
- })
1248
- )
1249
- })
1250
-
1251
- it('should emit verification warnings report when ignoreVerifyErrors is enabled', async () => {
1252
- const mockWarnings = [
1253
- {
1254
- actionName: 'verify-test',
1255
- address: '0x1234567890123456789012345678901234567890',
1256
- contractName: 'TestContract',
1257
- platform: 'etherscan_v2',
1258
- error: 'Failed to verify contract',
1259
- networkName: 'mainnet'
1260
- }
1261
- ]
1262
-
1263
- // Mock engine to return warnings
1264
- mockEngine.getVerificationWarnings = jest.fn().mockReturnValue(mockWarnings)
1265
-
1266
- const optionsWithIgnoreVerifyErrors = {
1267
- ...deployerOptions,
1268
- ignoreVerifyErrors: true
1269
- }
1270
-
1271
- const deployer = new Deployer(optionsWithIgnoreVerifyErrors)
1272
-
1273
- // Mock event emitter to track events
1274
- const mockEmitEvent = jest.fn()
1275
- ;(deployer as any).events = { emitEvent: mockEmitEvent }
1276
-
1277
- await deployer.run()
1278
-
1279
- // Verify that verification warnings report was emitted
1280
- expect(mockEmitEvent).toHaveBeenCalledWith({
1281
- type: 'verification_warnings_report',
1282
- level: 'warn',
1283
- data: {
1284
- totalWarnings: 1,
1285
- warnings: mockWarnings
1286
- }
1287
- })
1288
- })
1289
-
1290
- it('should not emit verification warnings report when ignoreVerifyErrors is disabled', async () => {
1291
- const optionsWithoutIgnoreVerifyErrors = {
1292
- ...deployerOptions,
1293
- ignoreVerifyErrors: false
1294
- }
1295
-
1296
- const deployer = new Deployer(optionsWithoutIgnoreVerifyErrors)
1297
-
1298
- // Mock event emitter to track events
1299
- const mockEmitEvent = jest.fn()
1300
- ;(deployer as any).events = { emitEvent: mockEmitEvent }
1301
-
1302
- await deployer.run()
1303
-
1304
- // Verify that verification warnings report was NOT emitted
1305
- expect(mockEmitEvent).not.toHaveBeenCalledWith(
1306
- expect.objectContaining({
1307
- type: 'verification_warnings_report'
1308
- })
1309
- )
1310
- })
1311
-
1312
- it('should not emit verification warnings report when there are no warnings', async () => {
1313
- // Mock engine to return no warnings
1314
- mockEngine.getVerificationWarnings = jest.fn().mockReturnValue([])
1315
-
1316
- const optionsWithIgnoreVerifyErrors = {
1317
- ...deployerOptions,
1318
- ignoreVerifyErrors: true
1319
- }
1320
-
1321
- const deployer = new Deployer(optionsWithIgnoreVerifyErrors)
1322
-
1323
- // Mock event emitter to track events
1324
- const mockEmitEvent = jest.fn()
1325
- ;(deployer as any).events = { emitEvent: mockEmitEvent }
1326
-
1327
- await deployer.run()
1328
-
1329
- // Verify that verification warnings report was NOT emitted when no warnings
1330
- expect(mockEmitEvent).not.toHaveBeenCalledWith(
1331
- expect.objectContaining({
1332
- type: 'verification_warnings_report'
1333
- })
1334
- )
1335
- })
1336
- })
1337
-
1338
- describe('job dependency failure handling', () => {
1339
- // Test constants
1340
- const TEST_BYTECODES = {
1341
- SIMPLE_CONTRACT: '0x6080604052348015600e575f5ffd5b5060c180601a5f395ff3fe6080604052348015600e575f5ffd5b50600436106030575f3560e01c806390c52443146034578063d09de08a14604d575b5f5ffd5b603b5f5481565b60405190815260200160405180910390f35b60536055565b005b5f805490806061836068565b9190505550565b5f60018201608457634e487b7160e01b5f52601160045260245ffd5b506001019056fea264697066735822122061c8cc43c72d6b23b16f7a7337dd15b93d71eb94a9d5247911e39f486e1f94f964736f6c634300081e0033',
1342
- BROKEN_BYTECODE: '0xff'
1343
- } as const
1344
-
1345
- it('should fail job B when job A fails due to dependency failure', async () => {
1346
- // Create jobs with dependency
1347
- const jobA: Job = {
1348
- name: 'job-a',
1349
- version: '1',
1350
- description: 'Deploy a contract with broken bytecode (will fail)',
1351
- actions: [
1352
- {
1353
- name: 'failing-deploy',
1354
- type: 'create-contract',
1355
- arguments: {
1356
- bytecode: TEST_BYTECODES.BROKEN_BYTECODE,
1357
- value: '0'
1358
- },
1359
- output: true
1360
- }
1361
- ]
1362
- }
1363
-
1364
- const jobB: Job = {
1365
- name: 'job-b',
1366
- version: '1',
1367
- description: 'Use output from failed job A',
1368
- depends_on: ['job-a'],
1369
- actions: [
1370
- {
1371
- name: 'use-failed-output',
1372
- type: 'send-transaction',
1373
- arguments: {
1374
- to: '{{job-a.failing-deploy.address}}',
1375
- data: '0x',
1376
- value: '0'
1377
- }
1378
- }
1379
- ]
1380
- }
1381
-
1382
- // Setup mocks
1383
- const mockJobs = new Map<string, Job>()
1384
- mockJobs.set('job-a', jobA)
1385
- mockJobs.set('job-b', jobB)
1386
-
1387
- const mockTemplates = new Map<string, Template>()
1388
-
1389
- MockProjectLoader.mockImplementation(() => ({
1390
- load: jest.fn().mockResolvedValue(undefined),
1391
- jobs: mockJobs,
1392
- templates: mockTemplates
1393
- } as any))
1394
-
1395
- MockDependencyGraph.mockImplementation(() => ({
1396
- getExecutionOrder: jest.fn().mockReturnValue(['job-a', 'job-b']),
1397
- getDependencies: jest.fn().mockReturnValue(new Set())
1398
- } as any))
1399
-
1400
- // Mock engine to simulate job A failure
1401
- MockExecutionEngine.mockImplementation(() => ({
1402
- executeJob: jest.fn().mockImplementation(async (job: Job) => {
1403
- if (job.name === 'job-a') {
1404
- throw new Error('Contract deployment failed: invalid bytecode')
1405
- }
1406
- // job-b should not be executed due to dependency failure
1407
- })
1408
- } as any))
1409
-
1410
- MockExecutionContext.mockImplementation(() => ({
1411
- dispose: jest.fn().mockResolvedValue(undefined),
1412
- getNetwork: jest.fn().mockReturnValue(mockNetwork1)
1413
- } as any))
1414
-
1415
- const deployer = new Deployer(deployerOptions)
1416
-
1417
- // Execute deployer - should fail due to job A failure
1418
- await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
1419
-
1420
- // Verify that job A failed
1421
- const results = (deployer as any).results
1422
- const jobAResult = results.get('job-a')
1423
- expect(jobAResult).toBeDefined()
1424
- expect(jobAResult.outputs.get(mockNetwork1.chainId).status).toBe('error')
1425
-
1426
- // Verify that job B failed due to dependency failure
1427
- const jobBResult = results.get('job-b')
1428
- expect(jobBResult).toBeDefined()
1429
- const jobBError = jobBResult.outputs.get(mockNetwork1.chainId)
1430
- expect(jobBError.status).toBe('error')
1431
- expect(jobBError.data).toContain('depends on "job-a", but "job-a" failed')
1432
- })
1433
-
1434
- it('should fail job B when referencing non-existent job outputs', async () => {
1435
- // Create job B that references non-existent job
1436
- const jobB: Job = {
1437
- name: 'job-b',
1438
- version: '1',
1439
- description: 'Reference non-existent job outputs',
1440
- actions: [
1441
- {
1442
- name: 'use-output-step',
1443
- type: 'send-transaction',
1444
- arguments: {
1445
- to: '{{non-existent-job.deploy-step.address}}',
1446
- data: '0x',
1447
- value: '0'
1448
- }
1449
- }
1450
- ]
1451
- }
1452
-
1453
- // Setup mocks
1454
- const mockJobs = new Map<string, Job>()
1455
- mockJobs.set('job-b', jobB)
1456
-
1457
- const mockTemplates = new Map<string, Template>()
1458
-
1459
- MockProjectLoader.mockImplementation(() => ({
1460
- load: jest.fn().mockResolvedValue(undefined),
1461
- jobs: mockJobs,
1462
- templates: mockTemplates
1463
- } as any))
1464
-
1465
- MockDependencyGraph.mockImplementation(() => ({
1466
- getExecutionOrder: jest.fn().mockReturnValue(['job-b']),
1467
- getDependencies: jest.fn().mockReturnValue(new Set())
1468
- } as any))
1469
-
1470
- // Mock engine to simulate expression resolution failure
1471
- MockExecutionEngine.mockImplementation(() => ({
1472
- executeJob: jest.fn().mockRejectedValue(new Error('Output for key "non-existent-job.deploy-step.address" not found in context'))
1473
- } as any))
1474
-
1475
- MockExecutionContext.mockImplementation(() => ({
1476
- dispose: jest.fn().mockResolvedValue(undefined),
1477
- getNetwork: jest.fn().mockReturnValue(mockNetwork1)
1478
- } as any))
1479
-
1480
- const deployer = new Deployer(deployerOptions)
1481
-
1482
- // Execute deployer - should fail due to expression resolution
1483
- await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
1484
-
1485
- // Verify that job B failed
1486
- const results = (deployer as any).results
1487
- const jobBResult = results.get('job-b')
1488
- expect(jobBResult).toBeDefined()
1489
- expect(jobBResult.outputs.get(mockNetwork1.chainId).status).toBe('error')
1490
- })
1491
-
1492
- it('should handle job B with no dependency on job A but references job A outputs', async () => {
1493
- // This tests the edge case where a job references outputs from another job
1494
- // without explicitly declaring a dependency. This should work if job A succeeds.
1495
-
1496
- const jobA: Job = {
1497
- name: 'job-a',
1498
- version: '1',
1499
- description: 'Deploy a contract successfully',
1500
- actions: [
1501
- {
1502
- name: 'deploy-step',
1503
- type: 'create-contract',
1504
- arguments: {
1505
- bytecode: TEST_BYTECODES.SIMPLE_CONTRACT,
1506
- value: '0'
1507
- },
1508
- output: true
1509
- }
1510
- ]
1511
- }
1512
-
1513
- const jobB: Job = {
1514
- name: 'job-b',
1515
- version: '1',
1516
- description: 'Reference job A outputs without explicit dependency',
1517
- // No depends_on field - this is the key difference
1518
- actions: [
1519
- {
1520
- name: 'use-output-step',
1521
- type: 'send-transaction',
1522
- arguments: {
1523
- to: '{{job-a.deploy-step.address}}',
1524
- data: '0x',
1525
- value: '0'
1526
- }
1527
- }
1528
- ]
1529
- }
1530
-
1531
- // Setup mocks
1532
- const mockJobs = new Map<string, Job>()
1533
- mockJobs.set('job-a', jobA)
1534
- mockJobs.set('job-b', jobB)
1535
-
1536
- const mockTemplates = new Map<string, Template>()
1537
-
1538
- MockProjectLoader.mockImplementation(() => ({
1539
- load: jest.fn().mockResolvedValue(undefined),
1540
- jobs: mockJobs,
1541
- templates: mockTemplates
1542
- } as any))
1543
-
1544
- MockDependencyGraph.mockImplementation(() => ({
1545
- getExecutionOrder: jest.fn().mockReturnValue(['job-a', 'job-b']),
1546
- getDependencies: jest.fn().mockReturnValue(new Set())
1547
- } as any))
1548
-
1549
- // Mock engine to simulate successful execution
1550
- MockExecutionEngine.mockImplementation(() => ({
1551
- executeJob: jest.fn().mockImplementation(async (job: Job) => {
1552
- if (job.name === 'job-a') {
1553
- // Simulate successful job A execution
1554
- return Promise.resolve()
1555
- } else if (job.name === 'job-b') {
1556
- // Simulate successful job B execution (no dependency check needed)
1557
- return Promise.resolve()
1558
- }
1559
- })
1560
- } as any))
1561
-
1562
- MockExecutionContext.mockImplementation(() => ({
1563
- dispose: jest.fn().mockResolvedValue(undefined),
1564
- getNetwork: jest.fn().mockReturnValue(mockNetwork1),
1565
- getOutputs: jest.fn().mockReturnValue(new Map([
1566
- ['deploy-step.address', '0x5FbDB2315678afecb367f032d93F642f64180aa3'],
1567
- ['deploy-step.hash', '0xmockdeployhash123']
1568
- ]))
1569
- } as any))
1570
-
1571
- const deployer = new Deployer(deployerOptions)
1572
-
1573
- // Execute deployer - should succeed since job A succeeds and job B has no explicit dependency
1574
- await expect(deployer.run()).resolves.not.toThrow()
1575
-
1576
- // Verify both jobs succeeded
1577
- const results = (deployer as any).results
1578
- const jobAResult = results.get('job-a')
1579
- const jobBResult = results.get('job-b')
1580
-
1581
- expect(jobAResult).toBeDefined()
1582
- expect(jobAResult.outputs.get(mockNetwork1.chainId).status).toBe('success')
1583
-
1584
- expect(jobBResult).toBeDefined()
1585
- expect(jobBResult.outputs.get(mockNetwork1.chainId).status).toBe('success')
1586
- })
1587
-
1588
- it('should allow job B to run when job A is skipped', async () => {
1589
- // This tests the scenario where job A is skipped (e.g., due to skip_condition)
1590
- // but job B should still be allowed to run since it doesn't depend on job A's outputs
1591
-
1592
- const jobA: Job = {
1593
- name: 'job-a',
1594
- version: '1',
1595
- description: 'Job A that will be skipped',
1596
- skip_condition: [true], // This will cause job A to be skipped
1597
- actions: [
1598
- {
1599
- name: 'deploy-step',
1600
- type: 'create-contract',
1601
- arguments: {
1602
- bytecode: TEST_BYTECODES.SIMPLE_CONTRACT,
1603
- value: '0'
1604
- },
1605
- output: true
1606
- }
1607
- ]
1608
- }
1609
-
1610
- const jobB: Job = {
1611
- name: 'job-b',
1612
- version: '1',
1613
- description: 'Job B that should run even if job A is skipped',
1614
- depends_on: ['job-a'], // Declares dependency on job A
1615
- actions: [
1616
- {
1617
- name: 'independent-action',
1618
- type: 'send-transaction',
1619
- arguments: {
1620
- to: '0x1234567890123456789012345678901234567890',
1621
- data: '0x',
1622
- value: '0'
1623
- }
1624
- }
1625
- ]
1626
- }
1627
-
1628
- // Setup mocks
1629
- const mockJobs = new Map<string, Job>()
1630
- mockJobs.set('job-a', jobA)
1631
- mockJobs.set('job-b', jobB)
1632
-
1633
- const mockTemplates = new Map<string, Template>()
1634
-
1635
- MockProjectLoader.mockImplementation(() => ({
1636
- load: jest.fn().mockResolvedValue(undefined),
1637
- jobs: mockJobs,
1638
- templates: mockTemplates
1639
- } as any))
1640
-
1641
- MockDependencyGraph.mockImplementation(() => ({
1642
- getExecutionOrder: jest.fn().mockReturnValue(['job-a', 'job-b']),
1643
- getDependencies: jest.fn().mockReturnValue(new Set())
1644
- } as any))
1645
-
1646
- // Mock engine to simulate job A being skipped and job B running successfully
1647
- MockExecutionEngine.mockImplementation(() => ({
1648
- executeJob: jest.fn().mockImplementation(async (job: Job) => {
1649
- if (job.name === 'job-a') {
1650
- // Job A should be skipped due to skip_condition
1651
- throw new Error('Job "job-a" skipped due to skip condition')
1652
- } else if (job.name === 'job-b') {
1653
- // Job B should run successfully even though job A was skipped
1654
- return Promise.resolve()
1655
- }
1656
- }),
1657
- evaluateSkipConditions: jest.fn().mockImplementation(async (conditions: any, context: any, scope: any) => {
1658
- // For job-a with skip_condition: [true], return true (should skip)
1659
- // For other jobs, return false (should not skip)
1660
- return conditions && conditions.length > 0 && conditions[0] === true
1661
- })
1662
- } as any))
1663
-
1664
- MockExecutionContext.mockImplementation(() => ({
1665
- dispose: jest.fn().mockResolvedValue(undefined),
1666
- getNetwork: jest.fn().mockReturnValue(mockNetwork1),
1667
- getOutputs: jest.fn().mockReturnValue(new Map([
1668
- ['independent-action.hash', '0xmocktransactionhash']
1669
- ]))
1670
- } as any))
1671
-
1672
- const deployer = new Deployer(deployerOptions)
1673
-
1674
- // Execute deployer - should succeed since job B can run independently
1675
- await expect(deployer.run()).resolves.not.toThrow()
1676
-
1677
- // Verify that job A was skipped
1678
- const results = (deployer as any).results
1679
- const jobAResult = results.get('job-a')
1680
- expect(jobAResult).toBeDefined()
1681
- expect(jobAResult.outputs.get(mockNetwork1.chainId).status).toBe('skipped')
1682
- expect(jobAResult.outputs.get(mockNetwork1.chainId).data).toContain('skip_condition')
1683
-
1684
- // Verify that job B ran successfully
1685
- const jobBResult = results.get('job-b')
1686
- expect(jobBResult).toBeDefined()
1687
- expect(jobBResult.outputs.get(mockNetwork1.chainId).status).toBe('success')
1688
- })
1689
-
1690
- it('should fail job B when job A fails, even with complex output references', async () => {
1691
- // This tests the scenario where job B references multiple outputs from job A
1692
- // and job A fails, ensuring job B fails due to dependency failure, not expression resolution
1693
-
1694
- const jobA: Job = {
1695
- name: 'job-a',
1696
- version: '1',
1697
- description: 'Deploy a contract with broken bytecode (will fail)',
1698
- actions: [
1699
- {
1700
- name: 'failing-deploy',
1701
- type: 'create-contract',
1702
- arguments: {
1703
- bytecode: TEST_BYTECODES.BROKEN_BYTECODE,
1704
- value: '0'
1705
- },
1706
- output: true
1707
- }
1708
- ]
1709
- }
1710
-
1711
- const jobB: Job = {
1712
- name: 'job-b',
1713
- version: '1',
1714
- description: 'Use multiple outputs from failed job A',
1715
- depends_on: ['job-a'],
1716
- actions: [
1717
- {
1718
- name: 'use-multiple-outputs',
1719
- type: 'send-transaction',
1720
- arguments: {
1721
- to: '{{job-a.failing-deploy.address}}',
1722
- data: '0x',
1723
- value: '0'
1724
- }
1725
- }
1726
- ]
1727
- }
1728
-
1729
- // Setup mocks
1730
- const mockJobs = new Map<string, Job>()
1731
- mockJobs.set('job-a', jobA)
1732
- mockJobs.set('job-b', jobB)
1733
-
1734
- const mockTemplates = new Map<string, Template>()
1735
-
1736
- MockProjectLoader.mockImplementation(() => ({
1737
- load: jest.fn().mockResolvedValue(undefined),
1738
- jobs: mockJobs,
1739
- templates: mockTemplates
1740
- } as any))
1741
-
1742
- MockDependencyGraph.mockImplementation(() => ({
1743
- getExecutionOrder: jest.fn().mockReturnValue(['job-a', 'job-b']),
1744
- getDependencies: jest.fn().mockReturnValue(new Set())
1745
- } as any))
1746
-
1747
- // Mock engine to simulate job A failure
1748
- MockExecutionEngine.mockImplementation(() => ({
1749
- executeJob: jest.fn().mockImplementation(async (job: Job) => {
1750
- if (job.name === 'job-a') {
1751
- throw new Error('Contract deployment failed: invalid bytecode')
1752
- }
1753
- // job-b should not be executed due to dependency failure
1754
- })
1755
- } as any))
1756
-
1757
- MockExecutionContext.mockImplementation(() => ({
1758
- dispose: jest.fn().mockResolvedValue(undefined),
1759
- getNetwork: jest.fn().mockReturnValue(mockNetwork1)
1760
- } as any))
1761
-
1762
- const deployer = new Deployer(deployerOptions)
1763
-
1764
- // Execute deployer - should fail due to job A failure
1765
- await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
1766
-
1767
- // Verify that job A failed
1768
- const results = (deployer as any).results
1769
- const jobAResult = results.get('job-a')
1770
- expect(jobAResult).toBeDefined()
1771
- expect(jobAResult.outputs.get(mockNetwork1.chainId).status).toBe('error')
1772
-
1773
- // Verify that job B failed due to dependency failure
1774
- const jobBResult = results.get('job-b')
1775
- expect(jobBResult).toBeDefined()
1776
- const jobBError = jobBResult.outputs.get(mockNetwork1.chainId)
1777
- expect(jobBError.status).toBe('error')
1778
- expect(jobBError.data).toContain('depends on "job-a", but "job-a" failed')
1779
- })
1780
- })
1781
-
1782
- describe('run summary functionality', () => {
1783
- let mockEventEmitter: jest.Mocked<any>
1784
- let deployer: Deployer
1785
-
1786
- beforeEach(() => {
1787
- // Create a mock event emitter to track events
1788
- mockEventEmitter = {
1789
- emitEvent: jest.fn()
1790
- }
1791
-
1792
- // Create deployer with mock event emitter
1793
- deployer = new Deployer({
1794
- ...deployerOptions,
1795
- eventEmitter: mockEventEmitter as any
1796
- })
1797
- })
1798
-
1799
- it('should emit run summary with success counts when all jobs succeed', () => {
1800
- // Mock the results property to simulate successful job execution
1801
- const mockResults = new Map()
1802
- mockResults.set('job1', {
1803
- job: mockJob1,
1804
- outputs: new Map([
1805
- [1, { status: 'success', data: new Map([['action1.hash', '0xhash1'], ['action1.address', '0x1234567890123456789012345678901234567890']]) }],
1806
- [137, { status: 'success', data: new Map([['action1.hash', '0xhash1'], ['action1.address', '0x1234567890123456789012345678901234567890']]) }]
1807
- ])
1808
- })
1809
- mockResults.set('job2', {
1810
- job: mockJob2,
1811
- outputs: new Map([
1812
- [1, { status: 'success', data: new Map([['action2.hash', '0xhash2'], ['action2.address', '0x9876543210987654321098765432109876543210']]) }],
1813
- [137, { status: 'success', data: new Map([['action2.hash', '0xhash2'], ['action2.address', '0x9876543210987654321098765432109876543210']]) }]
1814
- ])
1815
- })
1816
- mockResults.set('job3', {
1817
- job: mockJob3,
1818
- outputs: new Map([
1819
- [1, { status: 'success', data: new Map([['action3.hash', '0xhash3']]) }]
1820
- ])
1821
- })
1822
-
1823
- // Set the results property
1824
- ;(deployer as any).results = mockResults
1825
-
1826
- // Call emitRunSummary directly
1827
- ;(deployer as any).emitRunSummary(false)
1828
-
1829
- // Verify run summary was emitted
1830
- expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(
1831
- expect.objectContaining({
1832
- type: 'run_summary',
1833
- level: 'info', // Should be 'info' when no failures
1834
- data: expect.objectContaining({
1835
- networkCount: 2, // mainnet and polygon
1836
- jobCount: 3, // job1, job2, job3
1837
- successCount: 5, // job1&job2 on 2 networks + job3 on 1 network
1838
- failedCount: 0,
1839
- skippedCount: 0,
1840
- keyContracts: expect.arrayContaining([
1841
- { job: 'job1', action: 'action1', address: '0x1234567890123456789012345678901234567890' },
1842
- { job: 'job2', action: 'action2', address: '0x9876543210987654321098765432109876543210' }
1843
- ])
1844
- })
1845
- })
1846
- )
1847
- })
1848
-
1849
- it('should emit run summary with failure counts when some jobs fail', () => {
1850
- // Mock the results property to simulate mixed success/failure
1851
- const mockResults = new Map()
1852
- mockResults.set('job1', {
1853
- job: mockJob1,
1854
- outputs: new Map([
1855
- [1, { status: 'error', data: 'Job1 failed' }],
1856
- [137, { status: 'error', data: 'Job1 failed' }]
1857
- ])
1858
- })
1859
- mockResults.set('job2', {
1860
- job: mockJob2,
1861
- outputs: new Map([
1862
- [1, { status: 'success', data: new Map([['action2.hash', '0xhash2'], ['action2.address', '0x9876543210987654321098765432109876543210']]) }],
1863
- [137, { status: 'success', data: new Map([['action2.hash', '0xhash2'], ['action2.address', '0x9876543210987654321098765432109876543210']]) }]
1864
- ])
1865
- })
1866
- mockResults.set('job3', {
1867
- job: mockJob3,
1868
- outputs: new Map([
1869
- [1, { status: 'success', data: new Map([['action3.hash', '0xhash3']]) }]
1870
- ])
1871
- })
1872
-
1873
- // Set the results property
1874
- ;(deployer as any).results = mockResults
1875
-
1876
- // Call emitRunSummary with hasFailures = true
1877
- ;(deployer as any).emitRunSummary(true)
1878
-
1879
- // Verify run summary was emitted with failure info
1880
- expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(
1881
- expect.objectContaining({
1882
- type: 'run_summary',
1883
- level: 'warn', // Should be 'warn' when there are failures
1884
- data: expect.objectContaining({
1885
- networkCount: 2,
1886
- jobCount: 3,
1887
- successCount: 3, // job2&job3 succeed on their networks
1888
- failedCount: 2, // job1 fails on both networks
1889
- skippedCount: 0,
1890
- keyContracts: expect.arrayContaining([
1891
- { job: 'job2', action: 'action2', address: '0x9876543210987654321098765432109876543210' }
1892
- ])
1893
- })
1894
- })
1895
- )
1896
- })
1897
-
1898
- it('should emit run summary with skipped counts when jobs are skipped', () => {
1899
- // Mock the results property to simulate skipped jobs
1900
- const mockResults = new Map()
1901
- mockResults.set('job1', {
1902
- job: mockJob1,
1903
- outputs: new Map([
1904
- [1, { status: 'skipped', data: 'Job skipped due to network filter' }],
1905
- [137, { status: 'skipped', data: 'Job skipped due to network filter' }]
1906
- ])
1907
- })
1908
-
1909
- // Set the results property
1910
- ;(deployer as any).results = mockResults
1911
-
1912
- // Call emitRunSummary with hasFailures = false
1913
- ;(deployer as any).emitRunSummary(false)
1914
-
1915
- // Verify run summary was emitted with skipped info
1916
- expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(
1917
- expect.objectContaining({
1918
- type: 'run_summary',
1919
- level: 'info',
1920
- data: expect.objectContaining({
1921
- networkCount: 2,
1922
- jobCount: 1,
1923
- successCount: 0,
1924
- failedCount: 0,
1925
- skippedCount: 2, // job1 skipped on both networks
1926
- keyContracts: []
1927
- })
1928
- })
1929
- )
1930
- })
1931
-
1932
- it('should limit key contracts to 10 entries', () => {
1933
- // Create a job with many contract addresses
1934
- const manyContractsJob: Job = {
1935
- name: 'many-contracts-job',
1936
- version: '1.0.0',
1937
- actions: Array.from({ length: 15 }, (_, i) => ({
1938
- name: `action${i}`,
1939
- template: 'template1',
1940
- arguments: {}
1941
- }))
1942
- }
1943
-
1944
- // Mock the results property with many contract addresses
1945
- const mockResults = new Map()
1946
- const manyOutputs = new Map<string, any>()
1947
- for (let i = 0; i < 15; i++) {
1948
- manyOutputs.set(`action${i}.address`, `0x${i.toString().padStart(40, '0')}`)
1949
- manyOutputs.set(`action${i}.hash`, `0xhash${i}`)
1950
- }
1951
-
1952
- mockResults.set('many-contracts-job', {
1953
- job: manyContractsJob,
1954
- outputs: new Map([
1955
- [1, { status: 'success', data: manyOutputs }]
1956
- ])
1957
- })
1958
-
1959
- // Set the results property
1960
- ;(deployer as any).results = mockResults
1961
-
1962
- // Call emitRunSummary
1963
- ;(deployer as any).emitRunSummary(false)
1964
-
1965
- // Verify run summary limits key contracts to 10
1966
- expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(
1967
- expect.objectContaining({
1968
- type: 'run_summary',
1969
- data: expect.objectContaining({
1970
- keyContracts: expect.arrayContaining([
1971
- { job: 'many-contracts-job', action: 'action0', address: '0x0000000000000000000000000000000000000000' },
1972
- { job: 'many-contracts-job', action: 'action1', address: '0x0000000000000000000000000000000000000001' },
1973
- // ... up to action9
1974
- { job: 'many-contracts-job', action: 'action9', address: '0x0000000000000000000000000000000000000009' }
1975
- ])
1976
- })
1977
- })
1978
- )
1979
-
1980
- // Verify only 10 contracts are included
1981
- const summaryCall = mockEventEmitter.emitEvent.mock.calls.find((call: any) =>
1982
- call[0].type === 'run_summary'
1983
- )
1984
- expect(summaryCall![0].data.keyContracts).toHaveLength(10)
1985
- })
1986
-
1987
- it('should not emit run summary when showSummary is false', () => {
1988
- const deployerWithoutSummary = new Deployer({
1989
- ...deployerOptions,
1990
- eventEmitter: mockEventEmitter as any,
1991
- showSummary: false
1992
- })
1993
-
1994
- // Verify that showSummary is false
1995
- expect((deployerWithoutSummary as any).showSummary).toBe(false)
1996
-
1997
- // Mock the results property
1998
- const mockResults = new Map()
1999
- mockResults.set('job1', {
2000
- job: mockJob1,
2001
- outputs: new Map([
2002
- [1, { status: 'success', data: new Map([['action1.hash', '0xhash1']]) }]
2003
- ])
2004
- })
2005
- ;(deployerWithoutSummary as any).results = mockResults
2006
-
2007
- // Call emitRunSummary directly - this will emit regardless of showSummary
2008
- // The showSummary check happens in the run() method, not in emitRunSummary itself
2009
- ;(deployerWithoutSummary as any).emitRunSummary(false)
2010
-
2011
- // Verify run summary WAS emitted (because we called it directly)
2012
- expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(
2013
- expect.objectContaining({
2014
- type: 'run_summary'
2015
- })
2016
- )
2017
- })
2018
-
2019
- it('should emit run summary with mixed success/failure/skipped counts', () => {
2020
- // Mock the results property to simulate mixed outcomes
2021
- const mockResults = new Map()
2022
- mockResults.set('success-job', {
2023
- job: { name: 'success-job', version: '1.0.0', actions: [{ name: 'success-action', template: 'template1', arguments: {} }] },
2024
- outputs: new Map([
2025
- [1, { status: 'success', data: new Map([['success-action.hash', '0xsuccess'], ['success-action.address', '0x1234567890123456789012345678901234567890']]) }],
2026
- [137, { status: 'success', data: new Map([['success-action.hash', '0xsuccess'], ['success-action.address', '0x1234567890123456789012345678901234567890']]) }]
2027
- ])
2028
- })
2029
- mockResults.set('fail-job', {
2030
- job: { name: 'fail-job', version: '1.0.0', actions: [{ name: 'fail-action', template: 'template1', arguments: {} }] },
2031
- outputs: new Map([
2032
- [1, { status: 'error', data: 'Fail job failed' }],
2033
- [137, { status: 'error', data: 'Fail job failed' }]
2034
- ])
2035
- })
2036
- mockResults.set('skipped-job', {
2037
- job: { name: 'skipped-job', version: '1.0.0', actions: [{ name: 'skipped-action', template: 'template1', arguments: {} }] },
2038
- outputs: new Map([
2039
- [1, { status: 'skipped', data: 'Job skipped' }],
2040
- [137, { status: 'skipped', data: 'Job skipped' }]
2041
- ])
2042
- })
2043
-
2044
- // Set the results property
2045
- ;(deployer as any).results = mockResults
2046
-
2047
- // Call emitRunSummary with hasFailures = true
2048
- ;(deployer as any).emitRunSummary(true)
2049
-
2050
- // Verify run summary with mixed counts
2051
- expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(
2052
- expect.objectContaining({
2053
- type: 'run_summary',
2054
- level: 'warn', // Should be 'warn' due to failures
2055
- data: expect.objectContaining({
2056
- networkCount: 2,
2057
- jobCount: 3,
2058
- successCount: 2, // success-job on both networks
2059
- failedCount: 2, // fail-job on both networks
2060
- skippedCount: 2, // skipped-job on both networks
2061
- keyContracts: expect.arrayContaining([
2062
- { job: 'success-job', action: 'success-action', address: '0x1234567890123456789012345678901234567890' }
2063
- ])
2064
- })
2065
- })
2066
- )
2067
- })
2068
-
2069
- it('should handle empty results gracefully', () => {
2070
- // Set empty results
2071
- ;(deployer as any).results = new Map()
2072
-
2073
- // Call emitRunSummary
2074
- ;(deployer as any).emitRunSummary(false)
2075
-
2076
- // Verify run summary with empty results
2077
- expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(
2078
- expect.objectContaining({
2079
- type: 'run_summary',
2080
- level: 'info',
2081
- data: expect.objectContaining({
2082
- networkCount: 2,
2083
- jobCount: 0,
2084
- successCount: 0,
2085
- failedCount: 0,
2086
- skippedCount: 0,
2087
- keyContracts: []
2088
- })
2089
- })
2090
- )
2091
- })
2092
- })
2093
-
2094
- describe('skip_if functionality', () => {
2095
- it('should skip job when skip_if condition is true', async () => {
2096
- const jobWithSkipIf: Job = {
2097
- name: 'job-with-skip-if',
2098
- version: '1.0.0',
2099
- description: 'Job with skip_if condition',
2100
- skip_if: [{ type: 'contract-exists', arguments: { address: '0xabc' } }],
2101
- actions: [
2102
- { name: 'action1', template: 'template1', arguments: {} }
2103
- ]
2104
- }
2105
-
2106
- mockLoader.jobs.clear()
2107
- mockLoader.jobs.set('job-with-skip-if', jobWithSkipIf)
2108
- mockGraph.getExecutionOrder.mockReturnValue(['job-with-skip-if'])
2109
-
2110
- // Mock engine.evaluateSkipConditions to return true (condition met)
2111
- mockEngine.evaluateSkipConditions = jest.fn().mockResolvedValue(true)
2112
-
2113
- const deployer = new Deployer(deployerOptions)
2114
- await deployer.run()
2115
-
2116
- // Job should be skipped
2117
- const results = (deployer as any).results
2118
- const jobResult = results.get('job-with-skip-if')
2119
- expect(jobResult).toBeDefined()
2120
- expect(jobResult.outputs.get(mockNetwork1.chainId).status).toBe('skipped')
2121
- expect(jobResult.outputs.get(mockNetwork1.chainId).data).toContain('skip_if')
2122
-
2123
- // executeJob should NOT be called for skipped job
2124
- expect(mockEngine.executeJob).not.toHaveBeenCalled()
2125
- })
2126
-
2127
- it('should run job when skip_if condition is false', async () => {
2128
- const jobWithSkipIf: Job = {
2129
- name: 'job-with-skip-if-false',
2130
- version: '1.0.0',
2131
- description: 'Job with skip_if condition that is false',
2132
- skip_if: [{ type: 'contract-exists', arguments: { address: '0xabc' } }],
2133
- actions: [
2134
- { name: 'action1', template: 'template1', arguments: {} }
2135
- ]
2136
- }
2137
-
2138
- mockLoader.jobs.clear()
2139
- mockLoader.jobs.set('job-with-skip-if-false', jobWithSkipIf)
2140
- mockGraph.getExecutionOrder.mockReturnValue(['job-with-skip-if-false'])
2141
-
2142
- // Mock engine.evaluateSkipConditions to return false (condition not met)
2143
- mockEngine.evaluateSkipConditions = jest.fn().mockResolvedValue(false)
2144
-
2145
- const deployer = new Deployer(deployerOptions)
2146
- await deployer.run()
2147
-
2148
- // Job should run normally
2149
- const results = (deployer as any).results
2150
- const jobResult = results.get('job-with-skip-if-false')
2151
- expect(jobResult).toBeDefined()
2152
- expect(jobResult.outputs.get(mockNetwork1.chainId).status).toBe('success')
2153
-
2154
- // executeJob should be called
2155
- expect(mockEngine.executeJob).toHaveBeenCalledWith(jobWithSkipIf, expect.anything())
2156
- })
2157
-
2158
- it('should skip job when skip_if OR skip_condition is true', async () => {
2159
- const jobWithBoth: Job = {
2160
- name: 'job-with-both',
2161
- version: '1.0.0',
2162
- description: 'Job with both skip_if and skip_condition',
2163
- skip_condition: [{ type: 'contract-exists', arguments: { address: '0xabc' } }],
2164
- skip_if: [{ type: 'job-completed', arguments: { job: 'other-job' } }],
2165
- actions: [
2166
- { name: 'action1', template: 'template1', arguments: {} }
2167
- ]
2168
- }
2169
-
2170
- mockLoader.jobs.clear()
2171
- mockLoader.jobs.set('job-with-both', jobWithBoth)
2172
- mockGraph.getExecutionOrder.mockReturnValue(['job-with-both'])
2173
-
2174
- // Mock engine.evaluateSkipConditions to return false for skip_condition but true for skip_if
2175
- // The combined conditions should be evaluated together
2176
- mockEngine.evaluateSkipConditions = jest.fn().mockResolvedValue(true)
2177
-
2178
- const deployer = new Deployer(deployerOptions)
2179
- await deployer.run()
2180
-
2181
- // Job should be skipped (skip_if is true)
2182
- const results = (deployer as any).results
2183
- const jobResult = results.get('job-with-both')
2184
- expect(jobResult).toBeDefined()
2185
- expect(jobResult.outputs.get(mockNetwork1.chainId).status).toBe('skipped')
2186
-
2187
- // executeJob should NOT be called
2188
- expect(mockEngine.executeJob).not.toHaveBeenCalled()
2189
- })
2190
-
2191
- it('should NOT post-check skip_if after execution (only skip_condition is post-checked)', async () => {
2192
- // This is the key test: skip_if should NOT cause post-execution failures
2193
- const jobWithSkipIfOnly: Job = {
2194
- name: 'job-skip-if-only',
2195
- version: '1.0.0',
2196
- description: 'Job with skip_if that remains false after execution',
2197
- only_networks: [1], // Only run on mainnet to have predictable call count
2198
- skip_if: [{ type: 'contract-exists', arguments: { address: '0xabc' } }],
2199
- actions: [
2200
- { name: 'action1', template: 'template1', arguments: {} }
2201
- ]
2202
- }
2203
-
2204
- mockLoader.jobs.clear()
2205
- mockLoader.jobs.set('job-skip-if-only', jobWithSkipIfOnly)
2206
- mockGraph.getExecutionOrder.mockReturnValue(['job-skip-if-only'])
2207
-
2208
- // Pre-skip: skip_if is false, so job runs
2209
- mockEngine.evaluateSkipConditions = jest.fn().mockResolvedValue(false)
2210
-
2211
- // Job runs successfully - no post-check for skip_if
2212
- mockEngine.executeJob = jest.fn().mockResolvedValue(undefined)
2213
-
2214
- const deployer = new Deployer(deployerOptions)
2215
- await deployer.run()
2216
-
2217
- // Job should succeed (no post-check failure for skip_if)
2218
- const results = (deployer as any).results
2219
- const jobResult = results.get('job-skip-if-only')
2220
- expect(jobResult).toBeDefined()
2221
- expect(jobResult.outputs.get(mockNetwork1.chainId).status).toBe('success')
2222
-
2223
- // evaluateSkipConditions called once for pre-skip check (skip_if on mainnet only)
2224
- // skip_if is NOT post-checked, so no additional calls from engine
2225
- expect(mockEngine.evaluateSkipConditions).toHaveBeenCalledTimes(1)
2226
- // executeJob should be called once (job ran on mainnet only)
2227
- expect(mockEngine.executeJob).toHaveBeenCalledTimes(1)
2228
- })
2229
-
2230
- it('should still post-check skip_condition (existing behavior unchanged)', async () => {
2231
- // Verify that skip_condition still gets post-checked
2232
- // For this test, we need to use the real ExecutionEngine behavior
2233
- // Since the mock doesn't easily support this, we'll verify the behavior indirectly
2234
-
2235
- // The key is that skip_condition triggers post-check in the engine,
2236
- // while skip_if does not. This is verified by the engine.spec.ts tests.
2237
-
2238
- const jobWithSkipCondition: Job = {
2239
- name: 'job-skip-condition',
2240
- version: '1.0.0',
2241
- description: 'Job with skip_condition',
2242
- skip_condition: [{ type: 'contract-exists', arguments: { address: '0xabc' } }],
2243
- actions: [
2244
- { name: 'action1', template: 'template1', arguments: {} }
2245
- ]
2246
- }
2247
-
2248
- mockLoader.jobs.clear()
2249
- mockLoader.jobs.set('job-skip-condition', jobWithSkipCondition)
2250
- mockGraph.getExecutionOrder.mockReturnValue(['job-skip-condition'])
2251
-
2252
- // Pre-skip: skip_condition is false, so job runs
2253
- mockEngine.evaluateSkipConditions = jest.fn().mockResolvedValue(false)
2254
-
2255
- // Job runs successfully
2256
- mockEngine.executeJob = jest.fn().mockResolvedValue(undefined)
2257
-
2258
- const deployer = new Deployer(deployerOptions)
2259
- await deployer.run()
2260
-
2261
- // Job should succeed in this mock scenario
2262
- // The actual post-check behavior is tested in engine.spec.ts
2263
- const results = (deployer as any).results
2264
- const jobResult = results.get('job-skip-condition')
2265
- expect(jobResult).toBeDefined()
2266
- expect(jobResult.outputs.get(mockNetwork1.chainId).status).toBe('success')
2267
- })
2268
- })
2269
- })