@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.
- package/README.md +27 -0
- package/dist/lib/__tests__/network-loader.spec.js.map +1 -1
- package/dist/lib/core/__tests__/resolver.spec.js +22 -0
- package/dist/lib/core/__tests__/resolver.spec.js.map +1 -1
- package/dist/lib/core/__tests__/sign-actions.spec.d.ts +2 -0
- package/dist/lib/core/__tests__/sign-actions.spec.d.ts.map +1 -0
- package/dist/lib/core/__tests__/sign-actions.spec.js +128 -0
- package/dist/lib/core/__tests__/sign-actions.spec.js.map +1 -0
- package/dist/lib/core/__tests__/signer.spec.d.ts +2 -0
- package/dist/lib/core/__tests__/signer.spec.d.ts.map +1 -0
- package/dist/lib/core/__tests__/signer.spec.js +40 -0
- package/dist/lib/core/__tests__/signer.spec.js.map +1 -0
- package/dist/lib/core/context.d.ts +3 -2
- package/dist/lib/core/context.d.ts.map +1 -1
- package/dist/lib/core/context.js +3 -2
- package/dist/lib/core/context.js.map +1 -1
- package/dist/lib/core/engine.d.ts +4 -0
- package/dist/lib/core/engine.d.ts.map +1 -1
- package/dist/lib/core/engine.js +173 -0
- package/dist/lib/core/engine.js.map +1 -1
- package/dist/lib/core/signer.d.ts +7 -0
- package/dist/lib/core/signer.d.ts.map +1 -0
- package/dist/lib/core/signer.js +60 -0
- package/dist/lib/core/signer.js.map +1 -0
- package/dist/lib/parsers/__tests__/source.spec.js +37 -0
- package/dist/lib/parsers/__tests__/source.spec.js.map +1 -1
- package/dist/lib/parsers/source.js +1 -1
- package/dist/lib/parsers/source.js.map +1 -1
- package/dist/lib/provenance.js +51 -2
- package/dist/lib/provenance.js.map +1 -1
- package/dist/lib/types/actions.d.ts +26 -2
- package/dist/lib/types/actions.d.ts.map +1 -1
- package/dist/lib/types/actions.js +3 -0
- package/dist/lib/types/actions.js.map +1 -1
- package/dist/lib/types/source.d.ts +2 -0
- package/dist/lib/types/source.d.ts.map +1 -1
- package/package.json +4 -1
- package/.eslintrc.json +0 -29
- package/.github/workflows/ci.yml +0 -181
- package/CONCEPT.md +0 -24
- package/contracts/checked-call.huff +0 -65
- package/eslint.config.js +0 -48
- package/examples/jobs/guards-v1.yaml +0 -17
- package/examples/jobs/sequence-seq-0001-patch.yaml +0 -59
- package/examples/jobs/sequence-v1.yaml +0 -59
- package/examples/templates/sequence-factory-v1.yaml +0 -56
- package/jest.config.js +0 -25
- package/src/cli.ts +0 -18
- package/src/commands/common.ts +0 -61
- package/src/commands/dry.ts +0 -209
- package/src/commands/etherscan.ts +0 -360
- package/src/commands/index.ts +0 -6
- package/src/commands/list.ts +0 -262
- package/src/commands/provenance.ts +0 -120
- package/src/commands/run.ts +0 -146
- package/src/commands/utils.ts +0 -215
- package/src/index.ts +0 -67
- package/src/lib/__tests__/deployer-events.spec.ts +0 -338
- package/src/lib/__tests__/deployer.spec.ts +0 -2269
- package/src/lib/__tests__/network-loader.spec.ts +0 -150
- package/src/lib/__tests__/network-selection.spec.ts +0 -41
- package/src/lib/__tests__/network-utils.spec.ts +0 -230
- package/src/lib/__tests__/provenance.spec.ts +0 -208
- package/src/lib/artifacts/__tests__/fixtures/contract1.json +0 -19
- package/src/lib/artifacts/__tests__/fixtures/contract2.json +0 -19
- package/src/lib/artifacts/__tests__/fixtures/duplicate-name.json +0 -19
- package/src/lib/artifacts/__tests__/fixtures/nested/nested-contract.json +0 -18
- package/src/lib/artifacts/__tests__/fixtures/not-an-artifact.json +0 -8
- package/src/lib/artifacts/__tests__/fixtures/readme.txt +0 -2
- package/src/lib/contracts/__tests__/repository.spec.ts +0 -612
- package/src/lib/contracts/repository.ts +0 -411
- package/src/lib/core/__tests__/assert-action.spec.ts +0 -474
- package/src/lib/core/__tests__/context.spec.ts +0 -37
- package/src/lib/core/__tests__/engine.spec.ts +0 -2005
- package/src/lib/core/__tests__/graph.spec.ts +0 -125
- package/src/lib/core/__tests__/json-integration.spec.ts +0 -425
- package/src/lib/core/__tests__/loader.spec.ts +0 -367
- package/src/lib/core/__tests__/multi-platform-verification.spec.ts +0 -406
- package/src/lib/core/__tests__/resolver.spec.ts +0 -2496
- package/src/lib/core/__tests__/static-action.spec.ts +0 -172
- package/src/lib/core/context.ts +0 -127
- package/src/lib/core/engine.ts +0 -1834
- package/src/lib/core/graph.ts +0 -252
- package/src/lib/core/loader.ts +0 -253
- package/src/lib/core/resolver.ts +0 -873
- package/src/lib/deployer.ts +0 -1005
- package/src/lib/events/__tests__/event-system.spec.ts +0 -392
- package/src/lib/events/cli-adapter.ts +0 -369
- package/src/lib/events/emitter.ts +0 -62
- package/src/lib/events/index.ts +0 -3
- package/src/lib/events/types.ts +0 -520
- package/src/lib/index.ts +0 -17
- package/src/lib/network-loader.ts +0 -90
- package/src/lib/network-selection.ts +0 -73
- package/src/lib/network-utils.ts +0 -64
- package/src/lib/parsers/__tests__/buildinfo.spec.ts +0 -122
- package/src/lib/parsers/__tests__/fixtures/buildinfo/invalid-bytecode-buildinfo.json +0 -62
- package/src/lib/parsers/__tests__/fixtures/buildinfo/invalid-json.txt +0 -2
- package/src/lib/parsers/__tests__/fixtures/buildinfo/multi-contract-buildinfo.json +0 -89
- package/src/lib/parsers/__tests__/fixtures/buildinfo/no-contracts-buildinfo.json +0 -17
- package/src/lib/parsers/__tests__/fixtures/buildinfo/simple-buildinfo.json +0 -63
- package/src/lib/parsers/__tests__/fixtures/buildinfo/wrong-format.json +0 -4
- package/src/lib/parsers/__tests__/job.spec.ts +0 -439
- package/src/lib/parsers/__tests__/source.spec.ts +0 -134
- package/src/lib/parsers/__tests__/template.spec.ts +0 -111
- package/src/lib/parsers/artifact/__tests__/artifact.spec.ts +0 -117
- package/src/lib/parsers/artifact/__tests__/fixtures/empty-bytecode.json +0 -5
- package/src/lib/parsers/artifact/__tests__/fixtures/hardhat-artifact.json +0 -67
- package/src/lib/parsers/artifact/__tests__/fixtures/invalid-bytecode.json +0 -5
- package/src/lib/parsers/artifact/__tests__/fixtures/invalid-json.txt +0 -11
- package/src/lib/parsers/artifact/__tests__/fixtures/minimal-artifact.json +0 -5
- package/src/lib/parsers/artifact/__tests__/fixtures/missing-abi.json +0 -4
- package/src/lib/parsers/artifact/__tests__/fixtures/missing-bytecode.json +0 -11
- package/src/lib/parsers/artifact/__tests__/fixtures/missing-contract-name.json +0 -11
- package/src/lib/parsers/artifact/__tests__/fixtures/simple-artifact.json +0 -40
- package/src/lib/parsers/artifact/__tests__/fixtures/wrong-types.json +0 -7
- package/src/lib/parsers/artifact/foundry-1.2.ts +0 -72
- package/src/lib/parsers/artifact/index.ts +0 -27
- package/src/lib/parsers/artifact/types.ts +0 -9
- package/src/lib/parsers/buildinfo.ts +0 -127
- package/src/lib/parsers/constants.ts +0 -56
- package/src/lib/parsers/index.ts +0 -6
- package/src/lib/parsers/job.ts +0 -160
- package/src/lib/parsers/source.ts +0 -129
- package/src/lib/parsers/template.ts +0 -135
- package/src/lib/provenance.ts +0 -785
- package/src/lib/std/templates/arachnid-deterministic-deployment-proxy.yaml +0 -68
- package/src/lib/std/templates/assured-deployment.yaml +0 -46
- package/src/lib/std/templates/era-evm-predeploy.yaml +0 -35
- package/src/lib/std/templates/erc-2470.yaml +0 -70
- package/src/lib/std/templates/min-balance.yaml +0 -35
- package/src/lib/std/templates/nano-universal-deployer.yaml +0 -61
- package/src/lib/std/templates/raw-erc-2470.yaml +0 -62
- package/src/lib/std/templates/raw-nano-universal-deployer.yaml +0 -54
- package/src/lib/std/templates/raw-sequence-universal-deployer-2.yaml +0 -52
- package/src/lib/std/templates/sequence-universal-deployer-2.yaml +0 -61
- package/src/lib/types/__tests__/json-request-action.spec.ts +0 -243
- package/src/lib/types/__tests__/read-json-value.spec.ts +0 -278
- package/src/lib/types/__tests__/resolve-json-value.spec.ts +0 -769
- package/src/lib/types/actions.ts +0 -148
- package/src/lib/types/artifacts.ts +0 -21
- package/src/lib/types/buildinfo.ts +0 -116
- package/src/lib/types/conditions.ts +0 -50
- package/src/lib/types/contracts.ts +0 -26
- package/src/lib/types/definitions.ts +0 -77
- package/src/lib/types/index.ts +0 -9
- package/src/lib/types/network.ts +0 -33
- package/src/lib/types/project.ts +0 -9
- package/src/lib/types/source.ts +0 -26
- package/src/lib/types/task.ts +0 -9
- package/src/lib/types/values.ts +0 -221
- package/src/lib/utils/assertion.ts +0 -24
- package/src/lib/utils/validation.ts +0 -116
- package/src/lib/validation/contract-references.ts +0 -210
- package/src/lib/validation/index.ts +0 -1
- package/src/lib/verification/__tests__/etherscan.spec.ts +0 -710
- package/src/lib/verification/__tests__/sourcify.spec.ts +0 -288
- package/src/lib/verification/etherscan.ts +0 -547
- package/src/lib/verification/sourcify.ts +0 -248
- package/test_validation/artifacts/TestContract.json +0 -9
- package/test_validation/jobs/test-missing.yaml +0 -16
- package/test_validation/networks.yaml +0 -3
- 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
|
-
})
|