@0xsequence/catapult 1.3.6 → 1.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/__tests__/deployer.spec.js +263 -0
- package/dist/lib/__tests__/deployer.spec.js.map +1 -1
- package/dist/lib/core/__tests__/engine.spec.js +1 -1
- package/dist/lib/core/__tests__/engine.spec.js.map +1 -1
- package/dist/lib/core/engine.d.ts +2 -1
- package/dist/lib/core/engine.d.ts.map +1 -1
- package/dist/lib/core/engine.js +69 -15
- package/dist/lib/core/engine.js.map +1 -1
- package/dist/lib/deployer.d.ts.map +1 -1
- package/dist/lib/deployer.js +10 -4
- package/dist/lib/deployer.js.map +1 -1
- package/package.json +12 -13
- package/src/lib/__tests__/deployer.spec.ts +435 -93
- package/src/lib/core/__tests__/engine.spec.ts +1 -1
- package/src/lib/core/engine.ts +74 -16
- package/src/lib/deployer.ts +12 -2
|
@@ -36,7 +36,7 @@ describe('Deployer', () => {
|
|
|
36
36
|
beforeEach(() => {
|
|
37
37
|
// Clear all mocks
|
|
38
38
|
jest.clearAllMocks()
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
// Setup mock networks
|
|
41
41
|
mockNetwork1 = { name: 'mainnet', chainId: 1, rpcUrl: 'https://eth.rpc' }
|
|
42
42
|
mockNetwork2 = { name: 'polygon', chainId: 137, rpcUrl: 'https://polygon.rpc' }
|
|
@@ -166,7 +166,7 @@ describe('Deployer', () => {
|
|
|
166
166
|
describe('happy paths', () => {
|
|
167
167
|
it('should successfully run a simple deployment', async () => {
|
|
168
168
|
const deployer = new Deployer(deployerOptions)
|
|
169
|
-
|
|
169
|
+
|
|
170
170
|
await deployer.run()
|
|
171
171
|
|
|
172
172
|
// Verify the flow
|
|
@@ -186,18 +186,18 @@ describe('Deployer', () => {
|
|
|
186
186
|
if (jobName === 'job2') return new Set(['job1'])
|
|
187
187
|
return new Set()
|
|
188
188
|
})
|
|
189
|
-
|
|
189
|
+
|
|
190
190
|
const options: DeployerOptions = {
|
|
191
191
|
...deployerOptions,
|
|
192
192
|
runJobs: ['job2'] // This should also include job1 due to dependency
|
|
193
193
|
}
|
|
194
|
-
|
|
194
|
+
|
|
195
195
|
const deployer = new Deployer(options)
|
|
196
196
|
await deployer.run()
|
|
197
197
|
|
|
198
198
|
// Should execute job1 (dependency) and job2, but not job3
|
|
199
199
|
expect(mockEngine.executeJob).toHaveBeenCalledTimes(4) // 2 jobs × 2 networks
|
|
200
|
-
|
|
200
|
+
|
|
201
201
|
// Verify it was called with the right jobs
|
|
202
202
|
const executedJobs = mockEngine.executeJob.mock.calls.map(call => call[0].name)
|
|
203
203
|
expect(executedJobs).toContain('job1')
|
|
@@ -210,12 +210,12 @@ describe('Deployer', () => {
|
|
|
210
210
|
...deployerOptions,
|
|
211
211
|
runOnNetworks: [1] // Only mainnet
|
|
212
212
|
}
|
|
213
|
-
|
|
213
|
+
|
|
214
214
|
const deployer = new Deployer(options)
|
|
215
215
|
await deployer.run()
|
|
216
216
|
|
|
217
217
|
expect(mockEngine.executeJob).toHaveBeenCalledTimes(3) // 3 jobs × 1 network
|
|
218
|
-
|
|
218
|
+
|
|
219
219
|
// Verify all calls were with mainnet
|
|
220
220
|
const usedNetworks = MockExecutionContext.mock.calls.map(call => call[0])
|
|
221
221
|
expect(usedNetworks).toHaveLength(3)
|
|
@@ -231,7 +231,7 @@ describe('Deployer', () => {
|
|
|
231
231
|
// job3 has only_networks: [1], so should only run on mainnet
|
|
232
232
|
const job3Calls = mockEngine.executeJob.mock.calls.filter(call => call[0].name === 'job3')
|
|
233
233
|
expect(job3Calls).toHaveLength(1) // Only on mainnet
|
|
234
|
-
|
|
234
|
+
|
|
235
235
|
// Verify it was called with mainnet (check the MockExecutionContext calls)
|
|
236
236
|
const contextCallsForJob3 = MockExecutionContext.mock.calls.filter((_, index) => {
|
|
237
237
|
const engineCall = mockEngine.executeJob.mock.calls[index]
|
|
@@ -246,7 +246,7 @@ describe('Deployer', () => {
|
|
|
246
246
|
name: 'job-skip-polygon',
|
|
247
247
|
skip_networks: [137] // Skip polygon
|
|
248
248
|
}
|
|
249
|
-
|
|
249
|
+
|
|
250
250
|
mockLoader.jobs.set('job-skip-polygon', jobWithSkipNetworks)
|
|
251
251
|
mockGraph.getExecutionOrder.mockReturnValue(['job-skip-polygon'])
|
|
252
252
|
|
|
@@ -268,13 +268,13 @@ describe('Deployer', () => {
|
|
|
268
268
|
|
|
269
269
|
// Verify output files (flat)
|
|
270
270
|
expect(mockFs.writeFile).toHaveBeenCalledTimes(3)
|
|
271
|
-
|
|
271
|
+
|
|
272
272
|
// Check job1 output file (flat path)
|
|
273
273
|
const job1OutputCall = mockFs.writeFile.mock.calls.find(call =>
|
|
274
274
|
call[0] === '/test/project/output/job1.json'
|
|
275
275
|
)
|
|
276
276
|
expect(job1OutputCall).toBeDefined()
|
|
277
|
-
|
|
277
|
+
|
|
278
278
|
const job1Content = JSON.parse(job1OutputCall![1] as string)
|
|
279
279
|
expect(job1Content).toMatchObject({
|
|
280
280
|
jobName: 'job1',
|
|
@@ -349,11 +349,11 @@ describe('Deployer', () => {
|
|
|
349
349
|
{ name: 'other-action', template: 'template1', arguments: {} } // no output flag
|
|
350
350
|
]
|
|
351
351
|
}
|
|
352
|
-
|
|
352
|
+
|
|
353
353
|
mockLoader.jobs.clear()
|
|
354
354
|
mockLoader.jobs.set('job-with-output-flags', jobWithOutputFlags)
|
|
355
355
|
mockGraph.getExecutionOrder.mockReturnValue(['job-with-output-flags'])
|
|
356
|
-
|
|
356
|
+
|
|
357
357
|
// Mock context to return outputs from all actions
|
|
358
358
|
mockContext.getOutputs.mockReturnValue(new Map<string, any>([
|
|
359
359
|
['deploy-action.address', '0xdeployaddress'],
|
|
@@ -367,14 +367,14 @@ describe('Deployer', () => {
|
|
|
367
367
|
|
|
368
368
|
// Verify output file was written
|
|
369
369
|
expect(mockFs.writeFile).toHaveBeenCalledTimes(1)
|
|
370
|
-
|
|
370
|
+
|
|
371
371
|
const outputCall = mockFs.writeFile.mock.calls[0]
|
|
372
372
|
expect(outputCall[0]).toBe('/test/project/output/job-with-output-flags.json')
|
|
373
|
-
|
|
373
|
+
|
|
374
374
|
const outputContent = JSON.parse(outputCall[1] as string)
|
|
375
375
|
expect(outputContent.networks).toHaveLength(1)
|
|
376
376
|
expect(outputContent.networks[0].status).toBe('success')
|
|
377
|
-
|
|
377
|
+
|
|
378
378
|
// Should only include outputs from deploy-action (output: true)
|
|
379
379
|
// Should NOT include verify-action (output: false) or other-action (no flag)
|
|
380
380
|
expect(outputContent.networks[0].outputs).toEqual({
|
|
@@ -394,11 +394,11 @@ describe('Deployer', () => {
|
|
|
394
394
|
{ name: 'action2', template: 'template1', arguments: {}, output: false }
|
|
395
395
|
]
|
|
396
396
|
}
|
|
397
|
-
|
|
397
|
+
|
|
398
398
|
mockLoader.jobs.clear()
|
|
399
399
|
mockLoader.jobs.set('job-without-output-flags', jobWithoutOutputFlags)
|
|
400
400
|
mockGraph.getExecutionOrder.mockReturnValue(['job-without-output-flags'])
|
|
401
|
-
|
|
401
|
+
|
|
402
402
|
// Mock context to return outputs from all actions
|
|
403
403
|
mockContext.getOutputs.mockReturnValue(new Map<string, any>([
|
|
404
404
|
['action1.result', 'result1'],
|
|
@@ -410,10 +410,10 @@ describe('Deployer', () => {
|
|
|
410
410
|
|
|
411
411
|
// Verify output file was written
|
|
412
412
|
expect(mockFs.writeFile).toHaveBeenCalledTimes(1)
|
|
413
|
-
|
|
413
|
+
|
|
414
414
|
const outputCall = mockFs.writeFile.mock.calls[0]
|
|
415
415
|
const outputContent = JSON.parse(outputCall[1] as string)
|
|
416
|
-
|
|
416
|
+
|
|
417
417
|
// Should include all outputs (backward compatibility)
|
|
418
418
|
expect(outputContent.networks[0].outputs).toEqual({
|
|
419
419
|
'action1.result': 'result1',
|
|
@@ -434,11 +434,11 @@ describe('Deployer', () => {
|
|
|
434
434
|
{ name: 'verify2', template: 'template1', arguments: {}, output: false }
|
|
435
435
|
]
|
|
436
436
|
}
|
|
437
|
-
|
|
437
|
+
|
|
438
438
|
mockLoader.jobs.clear()
|
|
439
439
|
mockLoader.jobs.set('job-multiple-outputs', jobWithMultipleOutputs)
|
|
440
440
|
mockGraph.getExecutionOrder.mockReturnValue(['job-multiple-outputs'])
|
|
441
|
-
|
|
441
|
+
|
|
442
442
|
// Mock context to return outputs from all actions
|
|
443
443
|
mockContext.getOutputs.mockReturnValue(new Map<string, any>([
|
|
444
444
|
['deploy1.address', '0xdeploy1'],
|
|
@@ -453,7 +453,7 @@ describe('Deployer', () => {
|
|
|
453
453
|
// Verify output file was written
|
|
454
454
|
const outputCall = mockFs.writeFile.mock.calls[0]
|
|
455
455
|
const outputContent = JSON.parse(outputCall[1] as string)
|
|
456
|
-
|
|
456
|
+
|
|
457
457
|
// Should include outputs from both deploy actions, but not verify actions
|
|
458
458
|
expect(outputContent.networks[0].outputs).toEqual({
|
|
459
459
|
'deploy1.address': '0xdeploy1',
|
|
@@ -467,7 +467,7 @@ describe('Deployer', () => {
|
|
|
467
467
|
mockLoader.load.mockRejectedValue(new Error('Failed to load project'))
|
|
468
468
|
|
|
469
469
|
const deployer = new Deployer(deployerOptions)
|
|
470
|
-
|
|
470
|
+
|
|
471
471
|
await expect(deployer.run()).rejects.toThrow('Failed to load project')
|
|
472
472
|
// Note: Error handling is now done via events, not console.error directly
|
|
473
473
|
})
|
|
@@ -478,25 +478,25 @@ describe('Deployer', () => {
|
|
|
478
478
|
})
|
|
479
479
|
|
|
480
480
|
const deployer = new Deployer(deployerOptions)
|
|
481
|
-
|
|
481
|
+
|
|
482
482
|
await expect(deployer.run()).rejects.toThrow('Circular dependency detected')
|
|
483
483
|
})
|
|
484
484
|
|
|
485
485
|
it('should capture job execution failures and then throw', async () => {
|
|
486
486
|
mockEngine.executeJob.mockRejectedValue(new Error('Transaction failed'))
|
|
487
|
-
|
|
487
|
+
|
|
488
488
|
const deployer = new Deployer(deployerOptions)
|
|
489
|
-
|
|
489
|
+
|
|
490
490
|
await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
|
|
491
|
-
|
|
491
|
+
|
|
492
492
|
// Should still write output files with error entries before throwing
|
|
493
493
|
expect(mockFs.writeFile).toHaveBeenCalled()
|
|
494
|
-
|
|
494
|
+
|
|
495
495
|
// Check that error entries are recorded
|
|
496
496
|
const writeFileCalls = mockFs.writeFile.mock.calls
|
|
497
497
|
const outputFile = writeFileCalls[0]
|
|
498
498
|
const outputContent = JSON.parse(outputFile[1] as string)
|
|
499
|
-
|
|
499
|
+
|
|
500
500
|
// Should have error entries for failed executions
|
|
501
501
|
const errorEntries = outputContent.networks.filter((entry: any) => entry.status === 'error')
|
|
502
502
|
expect(errorEntries.length).toBeGreaterThan(0)
|
|
@@ -507,7 +507,7 @@ describe('Deployer', () => {
|
|
|
507
507
|
mockFs.mkdir.mockRejectedValue(new Error('Permission denied'))
|
|
508
508
|
|
|
509
509
|
const deployer = new Deployer(deployerOptions)
|
|
510
|
-
|
|
510
|
+
|
|
511
511
|
await expect(deployer.run()).rejects.toThrow('Permission denied')
|
|
512
512
|
})
|
|
513
513
|
|
|
@@ -515,7 +515,7 @@ describe('Deployer', () => {
|
|
|
515
515
|
mockFs.writeFile.mockRejectedValue(new Error('Disk full'))
|
|
516
516
|
|
|
517
517
|
const deployer = new Deployer(deployerOptions)
|
|
518
|
-
|
|
518
|
+
|
|
519
519
|
await expect(deployer.run()).rejects.toThrow('Disk full')
|
|
520
520
|
})
|
|
521
521
|
|
|
@@ -525,14 +525,14 @@ describe('Deployer', () => {
|
|
|
525
525
|
})
|
|
526
526
|
|
|
527
527
|
const deployer = new Deployer(deployerOptions)
|
|
528
|
-
|
|
528
|
+
|
|
529
529
|
await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
|
|
530
|
-
|
|
530
|
+
|
|
531
531
|
// Should record context creation failures as error entries before throwing
|
|
532
532
|
const writeFileCalls = mockFs.writeFile.mock.calls
|
|
533
533
|
const outputFile = writeFileCalls[0]
|
|
534
534
|
const outputContent = JSON.parse(outputFile[1] as string)
|
|
535
|
-
|
|
535
|
+
|
|
536
536
|
const errorEntries = outputContent.networks.filter((entry: any) => entry.status === 'error')
|
|
537
537
|
expect(errorEntries.length).toBeGreaterThan(0)
|
|
538
538
|
expect(errorEntries[0].error).toBe('Invalid private key')
|
|
@@ -546,7 +546,7 @@ describe('Deployer', () => {
|
|
|
546
546
|
name: 'weird-job',
|
|
547
547
|
only_networks: [999] // Non-existent network
|
|
548
548
|
}
|
|
549
|
-
|
|
549
|
+
|
|
550
550
|
mockLoader.jobs.clear()
|
|
551
551
|
mockLoader.jobs.set('weird-job', weirdJob)
|
|
552
552
|
mockGraph.getExecutionOrder.mockReturnValue(['weird-job'])
|
|
@@ -564,7 +564,7 @@ describe('Deployer', () => {
|
|
|
564
564
|
name: 'weird-job',
|
|
565
565
|
skip_networks: [1, 137] // Skip all available networks
|
|
566
566
|
}
|
|
567
|
-
|
|
567
|
+
|
|
568
568
|
mockLoader.jobs.clear()
|
|
569
569
|
mockLoader.jobs.set('weird-job', weirdJob)
|
|
570
570
|
mockGraph.getExecutionOrder.mockReturnValue(['weird-job'])
|
|
@@ -578,12 +578,12 @@ describe('Deployer', () => {
|
|
|
578
578
|
|
|
579
579
|
it('should handle runOnNetworks with non-existent chain IDs', async () => {
|
|
580
580
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
|
|
581
|
-
|
|
581
|
+
|
|
582
582
|
const options: DeployerOptions = {
|
|
583
583
|
...deployerOptions,
|
|
584
584
|
runOnNetworks: [1, 999, 888] // 999 and 888 don't exist
|
|
585
585
|
}
|
|
586
|
-
|
|
586
|
+
|
|
587
587
|
const deployer = new Deployer(options)
|
|
588
588
|
await deployer.run()
|
|
589
589
|
|
|
@@ -599,9 +599,9 @@ describe('Deployer', () => {
|
|
|
599
599
|
...deployerOptions,
|
|
600
600
|
runJobs: ['non-existent-job']
|
|
601
601
|
}
|
|
602
|
-
|
|
602
|
+
|
|
603
603
|
const deployer = new Deployer(options)
|
|
604
|
-
|
|
604
|
+
|
|
605
605
|
await expect(deployer.run()).rejects.toThrow(
|
|
606
606
|
'Specified job "non-existent-job" not found in project.'
|
|
607
607
|
)
|
|
@@ -611,18 +611,18 @@ describe('Deployer', () => {
|
|
|
611
611
|
const brokenContext = {
|
|
612
612
|
// Missing getOutputs method
|
|
613
613
|
} as any
|
|
614
|
-
|
|
614
|
+
|
|
615
615
|
MockExecutionContext.mockImplementation(() => brokenContext)
|
|
616
616
|
|
|
617
617
|
const deployer = new Deployer(deployerOptions)
|
|
618
|
-
|
|
618
|
+
|
|
619
619
|
await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
|
|
620
|
-
|
|
620
|
+
|
|
621
621
|
// Should record the missing method error before throwing
|
|
622
622
|
const writeFileCalls = mockFs.writeFile.mock.calls
|
|
623
623
|
const outputFile = writeFileCalls[0]
|
|
624
624
|
const outputContent = JSON.parse(outputFile[1] as string)
|
|
625
|
-
|
|
625
|
+
|
|
626
626
|
const errorEntries = outputContent.networks.filter((entry: any) => entry.status === 'error')
|
|
627
627
|
expect(errorEntries.length).toBeGreaterThan(0)
|
|
628
628
|
})
|
|
@@ -632,7 +632,7 @@ describe('Deployer', () => {
|
|
|
632
632
|
...deployerOptions,
|
|
633
633
|
networks: []
|
|
634
634
|
}
|
|
635
|
-
|
|
635
|
+
|
|
636
636
|
const deployer = new Deployer(options)
|
|
637
637
|
await deployer.run()
|
|
638
638
|
|
|
@@ -646,7 +646,7 @@ describe('Deployer', () => {
|
|
|
646
646
|
...deployerOptions,
|
|
647
647
|
runJobs: []
|
|
648
648
|
}
|
|
649
|
-
|
|
649
|
+
|
|
650
650
|
const deployer = new Deployer(options)
|
|
651
651
|
await deployer.run()
|
|
652
652
|
|
|
@@ -659,7 +659,7 @@ describe('Deployer', () => {
|
|
|
659
659
|
...deployerOptions,
|
|
660
660
|
runOnNetworks: []
|
|
661
661
|
}
|
|
662
|
-
|
|
662
|
+
|
|
663
663
|
const deployer = new Deployer(options)
|
|
664
664
|
await deployer.run()
|
|
665
665
|
|
|
@@ -674,7 +674,7 @@ describe('Deployer', () => {
|
|
|
674
674
|
only_networks: [1, 137],
|
|
675
675
|
skip_networks: [137]
|
|
676
676
|
}
|
|
677
|
-
|
|
677
|
+
|
|
678
678
|
mockLoader.jobs.clear()
|
|
679
679
|
mockLoader.jobs.set('conflicted-job', conflictedJob)
|
|
680
680
|
mockGraph.getExecutionOrder.mockReturnValue(['conflicted-job'])
|
|
@@ -696,20 +696,20 @@ describe('Deployer', () => {
|
|
|
696
696
|
})
|
|
697
697
|
|
|
698
698
|
const deployer = new Deployer(deployerOptions)
|
|
699
|
-
|
|
699
|
+
|
|
700
700
|
await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
|
|
701
|
-
|
|
701
|
+
|
|
702
702
|
// Should write output files with error entries before throwing
|
|
703
703
|
expect(mockFs.writeFile).toHaveBeenCalled()
|
|
704
|
-
|
|
704
|
+
|
|
705
705
|
const writeFileCalls = mockFs.writeFile.mock.calls
|
|
706
706
|
const outputFile = writeFileCalls[0]
|
|
707
707
|
const outputContent = JSON.parse(outputFile[1] as string)
|
|
708
|
-
|
|
708
|
+
|
|
709
709
|
// All entries should be error entries
|
|
710
710
|
const errorEntries = outputContent.networks.filter((entry: any) => entry.status === 'error')
|
|
711
711
|
expect(errorEntries.length).toBeGreaterThan(0)
|
|
712
|
-
|
|
712
|
+
|
|
713
713
|
// No success entries
|
|
714
714
|
const successEntries = outputContent.networks.filter((entry: any) => entry.status === 'success')
|
|
715
715
|
expect(successEntries.length).toBe(0)
|
|
@@ -719,7 +719,7 @@ describe('Deployer', () => {
|
|
|
719
719
|
// Create 100 jobs to test performance/memory
|
|
720
720
|
const manyJobs = Array.from({ length: 100 }, (_, i) => `job${i}`)
|
|
721
721
|
mockGraph.getExecutionOrder.mockReturnValue(manyJobs)
|
|
722
|
-
|
|
722
|
+
|
|
723
723
|
// Mock loader to have all these jobs
|
|
724
724
|
for (let i = 0; i < 100; i++) {
|
|
725
725
|
mockLoader.jobs.set(`job${i}`, {
|
|
@@ -756,14 +756,14 @@ describe('Deployer', () => {
|
|
|
756
756
|
runJobs: ['job2']
|
|
757
757
|
}
|
|
758
758
|
const deployer = new Deployer(options)
|
|
759
|
-
|
|
759
|
+
|
|
760
760
|
// Initialize the deployer's graph by calling load
|
|
761
761
|
await mockLoader.load()
|
|
762
762
|
;(deployer as any).graph = mockGraph
|
|
763
|
-
|
|
763
|
+
|
|
764
764
|
// Mock getDependencies to return job1 as dependency of job2
|
|
765
765
|
mockGraph.getDependencies.mockReturnValueOnce(new Set(['job1']))
|
|
766
|
-
|
|
766
|
+
|
|
767
767
|
const fullOrder = ['job1', 'job2', 'job3']
|
|
768
768
|
const plan = (deployer as any).getJobExecutionPlan(fullOrder)
|
|
769
769
|
expect(plan).toEqual(['job1', 'job2'])
|
|
@@ -893,7 +893,7 @@ describe('Deployer', () => {
|
|
|
893
893
|
runOnNetworks: [1]
|
|
894
894
|
}
|
|
895
895
|
const deployer = new Deployer(options)
|
|
896
|
-
|
|
896
|
+
|
|
897
897
|
const networks = (deployer as any).getTargetNetworks()
|
|
898
898
|
expect(networks).toEqual([mockNetwork1])
|
|
899
899
|
})
|
|
@@ -942,7 +942,7 @@ describe('Deployer', () => {
|
|
|
942
942
|
|
|
943
943
|
mockLoader.jobs.set('job4', job4)
|
|
944
944
|
mockGraph.getExecutionOrder.mockReturnValue(['job1', 'job2', 'job3', 'job4'])
|
|
945
|
-
|
|
945
|
+
|
|
946
946
|
// Mock dependencies
|
|
947
947
|
mockGraph.getDependencies
|
|
948
948
|
.mockReturnValueOnce(new Set()) // job1 has no deps
|
|
@@ -963,7 +963,7 @@ describe('Deployer', () => {
|
|
|
963
963
|
const contextCalls = MockExecutionContext.mock.calls
|
|
964
964
|
const mainnetCalls = contextCalls.filter(call => call[0].chainId === 1)
|
|
965
965
|
const polygonCalls = contextCalls.filter(call => call[0].chainId === 137)
|
|
966
|
-
|
|
966
|
+
|
|
967
967
|
expect(mainnetCalls).toHaveLength(4) // All jobs run on mainnet
|
|
968
968
|
expect(polygonCalls).toHaveLength(2) // Only job1 and job2 run on polygon
|
|
969
969
|
})
|
|
@@ -975,7 +975,7 @@ describe('Deployer', () => {
|
|
|
975
975
|
const currentCall = MockExecutionContext.mock.calls[callCount]
|
|
976
976
|
const network = currentCall ? currentCall[0] : null
|
|
977
977
|
callCount++
|
|
978
|
-
|
|
978
|
+
|
|
979
979
|
if (job.name === 'job2' && network && network.chainId === 137) {
|
|
980
980
|
throw new Error('Polygon execution failed')
|
|
981
981
|
}
|
|
@@ -983,19 +983,19 @@ describe('Deployer', () => {
|
|
|
983
983
|
})
|
|
984
984
|
|
|
985
985
|
const deployer = new Deployer(deployerOptions)
|
|
986
|
-
|
|
986
|
+
|
|
987
987
|
await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
|
|
988
|
-
|
|
988
|
+
|
|
989
989
|
// Should capture the partial failure in output files before throwing
|
|
990
990
|
const writeFileCalls = mockFs.writeFile.mock.calls
|
|
991
|
-
const job2Output = writeFileCalls.find(call =>
|
|
991
|
+
const job2Output = writeFileCalls.find(call =>
|
|
992
992
|
String(call[0]).includes('job2.json')
|
|
993
993
|
)
|
|
994
|
-
|
|
994
|
+
|
|
995
995
|
if (job2Output) {
|
|
996
996
|
const job2Content = JSON.parse(job2Output[1] as string)
|
|
997
997
|
const errorEntries = job2Content.networks.filter((entry: any) => entry.status === 'error')
|
|
998
|
-
expect(errorEntries.some((entry: any) =>
|
|
998
|
+
expect(errorEntries.some((entry: any) =>
|
|
999
999
|
entry.chainId === '137' && entry.error === 'Polygon execution failed'
|
|
1000
1000
|
)).toBe(true)
|
|
1001
1001
|
}
|
|
@@ -1019,22 +1019,22 @@ describe('Deployer', () => {
|
|
|
1019
1019
|
|
|
1020
1020
|
// Verify outputs are correctly segregated by network since they have different outputs
|
|
1021
1021
|
const writeFileCalls = mockFs.writeFile.mock.calls
|
|
1022
|
-
const job1Output = writeFileCalls.find(call =>
|
|
1022
|
+
const job1Output = writeFileCalls.find(call =>
|
|
1023
1023
|
call[0] === '/test/project/output/job1.json'
|
|
1024
1024
|
)
|
|
1025
|
-
|
|
1025
|
+
|
|
1026
1026
|
const job1Content = JSON.parse(job1Output![1] as string)
|
|
1027
1027
|
// Since outputs differ by network, they should be in separate entries
|
|
1028
1028
|
expect(job1Content.networks).toHaveLength(2)
|
|
1029
|
-
|
|
1029
|
+
|
|
1030
1030
|
// Find entries for each network
|
|
1031
|
-
const network1Entry = job1Content.networks.find((entry: any) =>
|
|
1031
|
+
const network1Entry = job1Content.networks.find((entry: any) =>
|
|
1032
1032
|
entry.chainIds && entry.chainIds.includes('1')
|
|
1033
1033
|
)
|
|
1034
|
-
const network137Entry = job1Content.networks.find((entry: any) =>
|
|
1034
|
+
const network137Entry = job1Content.networks.find((entry: any) =>
|
|
1035
1035
|
entry.chainIds && entry.chainIds.includes('137')
|
|
1036
1036
|
)
|
|
1037
|
-
|
|
1037
|
+
|
|
1038
1038
|
expect(network1Entry.outputs['action.hash']).toBe('0xhash-1')
|
|
1039
1039
|
expect(network137Entry.outputs['action.hash']).toBe('0xhash-137')
|
|
1040
1040
|
})
|
|
@@ -1056,10 +1056,10 @@ describe('Deployer', () => {
|
|
|
1056
1056
|
|
|
1057
1057
|
// Verify identical outputs are grouped together
|
|
1058
1058
|
const writeFileCalls = mockFs.writeFile.mock.calls
|
|
1059
|
-
const job1Output = writeFileCalls.find(call =>
|
|
1059
|
+
const job1Output = writeFileCalls.find(call =>
|
|
1060
1060
|
call[0] === '/test/project/output/job1.json'
|
|
1061
1061
|
)
|
|
1062
|
-
|
|
1062
|
+
|
|
1063
1063
|
const job1Content = JSON.parse(job1Output![1] as string)
|
|
1064
1064
|
// Since outputs are identical, they should be grouped into one entry
|
|
1065
1065
|
expect(job1Content.networks).toHaveLength(1)
|
|
@@ -1075,7 +1075,7 @@ describe('Deployer', () => {
|
|
|
1075
1075
|
const currentCall = MockExecutionContext.mock.calls[callCount]
|
|
1076
1076
|
const network = currentCall ? currentCall[0] : null
|
|
1077
1077
|
callCount++
|
|
1078
|
-
|
|
1078
|
+
|
|
1079
1079
|
if (job.name === 'job1' && network && network.chainId === 137) {
|
|
1080
1080
|
throw new Error('Polygon execution failed')
|
|
1081
1081
|
}
|
|
@@ -1098,21 +1098,21 @@ describe('Deployer', () => {
|
|
|
1098
1098
|
|
|
1099
1099
|
// Verify outputs show both success and error states before throwing
|
|
1100
1100
|
const writeFileCalls = mockFs.writeFile.mock.calls
|
|
1101
|
-
const job1Output = writeFileCalls.find(call =>
|
|
1101
|
+
const job1Output = writeFileCalls.find(call =>
|
|
1102
1102
|
call[0] === '/test/project/output/job1.json'
|
|
1103
1103
|
)
|
|
1104
|
-
|
|
1104
|
+
|
|
1105
1105
|
const job1Content = JSON.parse(job1Output![1] as string)
|
|
1106
1106
|
expect(job1Content.networks).toHaveLength(2) // One success entry, one error entry
|
|
1107
|
-
|
|
1107
|
+
|
|
1108
1108
|
// Find success and error entries
|
|
1109
1109
|
const successEntry = job1Content.networks.find((entry: any) => entry.status === 'success')
|
|
1110
1110
|
const errorEntry = job1Content.networks.find((entry: any) => entry.status === 'error')
|
|
1111
|
-
|
|
1111
|
+
|
|
1112
1112
|
expect(successEntry).toBeDefined()
|
|
1113
1113
|
expect(successEntry.chainIds).toEqual(['1'])
|
|
1114
1114
|
expect(successEntry.outputs['contract.address']).toBe('0x1234567890123456789012345678901234567890')
|
|
1115
|
-
|
|
1115
|
+
|
|
1116
1116
|
expect(errorEntry).toBeDefined()
|
|
1117
1117
|
expect(errorEntry.chainId).toBe('137')
|
|
1118
1118
|
expect(errorEntry.error).toBe('Polygon execution failed')
|
|
@@ -1137,9 +1137,9 @@ describe('Deployer', () => {
|
|
|
1137
1137
|
mockEngine.executeJob.mockRejectedValueOnce(new Error('First job failed'))
|
|
1138
1138
|
|
|
1139
1139
|
const deployer = new Deployer(options)
|
|
1140
|
-
|
|
1140
|
+
|
|
1141
1141
|
await expect(deployer.run()).rejects.toThrow('First job failed')
|
|
1142
|
-
|
|
1142
|
+
|
|
1143
1143
|
// Should only attempt the first execution, not continue to other networks/jobs
|
|
1144
1144
|
expect(mockEngine.executeJob).toHaveBeenCalledTimes(1)
|
|
1145
1145
|
})
|
|
@@ -1156,9 +1156,9 @@ describe('Deployer', () => {
|
|
|
1156
1156
|
mockEngine.executeJob.mockResolvedValue(undefined)
|
|
1157
1157
|
|
|
1158
1158
|
const deployer = new Deployer(options)
|
|
1159
|
-
|
|
1159
|
+
|
|
1160
1160
|
await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
|
|
1161
|
-
|
|
1161
|
+
|
|
1162
1162
|
// Should attempt all executions (2 networks * 1 job = 2 calls)
|
|
1163
1163
|
expect(mockEngine.executeJob).toHaveBeenCalledTimes(2)
|
|
1164
1164
|
})
|
|
@@ -1175,9 +1175,9 @@ describe('Deployer', () => {
|
|
|
1175
1175
|
mockEngine.executeJob.mockResolvedValue(undefined)
|
|
1176
1176
|
|
|
1177
1177
|
const deployer = new Deployer(options)
|
|
1178
|
-
|
|
1178
|
+
|
|
1179
1179
|
await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
|
|
1180
|
-
|
|
1180
|
+
|
|
1181
1181
|
// Should attempt all executions
|
|
1182
1182
|
expect(mockEngine.executeJob).toHaveBeenCalledTimes(2)
|
|
1183
1183
|
})
|
|
@@ -1193,9 +1193,9 @@ describe('Deployer', () => {
|
|
|
1193
1193
|
mockEngine.executeJob.mockResolvedValue(undefined)
|
|
1194
1194
|
|
|
1195
1195
|
const deployer = new Deployer(options)
|
|
1196
|
-
|
|
1196
|
+
|
|
1197
1197
|
await expect(deployer.run()).resolves.not.toThrow()
|
|
1198
|
-
|
|
1198
|
+
|
|
1199
1199
|
// Should complete all executions
|
|
1200
1200
|
expect(mockEngine.executeJob).toHaveBeenCalledTimes(2)
|
|
1201
1201
|
})
|
|
@@ -1241,7 +1241,7 @@ describe('Deployer', () => {
|
|
|
1241
1241
|
}
|
|
1242
1242
|
|
|
1243
1243
|
const deployer = new Deployer(optionsWithIgnoreVerifyErrors)
|
|
1244
|
-
|
|
1244
|
+
|
|
1245
1245
|
// Mock event emitter to track events
|
|
1246
1246
|
const mockEmitEvent = jest.fn()
|
|
1247
1247
|
;(deployer as any).events = { emitEvent: mockEmitEvent }
|
|
@@ -1266,7 +1266,7 @@ describe('Deployer', () => {
|
|
|
1266
1266
|
}
|
|
1267
1267
|
|
|
1268
1268
|
const deployer = new Deployer(optionsWithoutIgnoreVerifyErrors)
|
|
1269
|
-
|
|
1269
|
+
|
|
1270
1270
|
// Mock event emitter to track events
|
|
1271
1271
|
const mockEmitEvent = jest.fn()
|
|
1272
1272
|
;(deployer as any).events = { emitEvent: mockEmitEvent }
|
|
@@ -1291,7 +1291,7 @@ describe('Deployer', () => {
|
|
|
1291
1291
|
}
|
|
1292
1292
|
|
|
1293
1293
|
const deployer = new Deployer(optionsWithIgnoreVerifyErrors)
|
|
1294
|
-
|
|
1294
|
+
|
|
1295
1295
|
// Mock event emitter to track events
|
|
1296
1296
|
const mockEmitEvent = jest.fn()
|
|
1297
1297
|
;(deployer as any).events = { emitEvent: mockEmitEvent }
|
|
@@ -1306,4 +1306,346 @@ describe('Deployer', () => {
|
|
|
1306
1306
|
)
|
|
1307
1307
|
})
|
|
1308
1308
|
})
|
|
1309
|
-
|
|
1309
|
+
|
|
1310
|
+
describe('job dependency failure handling', () => {
|
|
1311
|
+
// Test constants
|
|
1312
|
+
const TEST_BYTECODES = {
|
|
1313
|
+
SIMPLE_CONTRACT: '0x6080604052348015600e575f5ffd5b5060c180601a5f395ff3fe6080604052348015600e575f5ffd5b50600436106030575f3560e01c806390c52443146034578063d09de08a14604d575b5f5ffd5b603b5f5481565b60405190815260200160405180910390f35b60536055565b005b5f805490806061836068565b9190505550565b5f60018201608457634e487b7160e01b5f52601160045260245ffd5b506001019056fea264697066735822122061c8cc43c72d6b23b16f7a7337dd15b93d71eb94a9d5247911e39f486e1f94f964736f6c634300081e0033',
|
|
1314
|
+
BROKEN_BYTECODE: '0xff'
|
|
1315
|
+
} as const
|
|
1316
|
+
|
|
1317
|
+
it('should fail job B when job A fails due to dependency failure', async () => {
|
|
1318
|
+
// Create jobs with dependency
|
|
1319
|
+
const jobA: Job = {
|
|
1320
|
+
name: 'job-a',
|
|
1321
|
+
version: '1',
|
|
1322
|
+
description: 'Deploy a contract with broken bytecode (will fail)',
|
|
1323
|
+
actions: [
|
|
1324
|
+
{
|
|
1325
|
+
name: 'failing-deploy',
|
|
1326
|
+
type: 'create-contract',
|
|
1327
|
+
arguments: {
|
|
1328
|
+
bytecode: TEST_BYTECODES.BROKEN_BYTECODE,
|
|
1329
|
+
value: '0'
|
|
1330
|
+
},
|
|
1331
|
+
output: true
|
|
1332
|
+
}
|
|
1333
|
+
]
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
const jobB: Job = {
|
|
1337
|
+
name: 'job-b',
|
|
1338
|
+
version: '1',
|
|
1339
|
+
description: 'Use output from failed job A',
|
|
1340
|
+
depends_on: ['job-a'],
|
|
1341
|
+
actions: [
|
|
1342
|
+
{
|
|
1343
|
+
name: 'use-failed-output',
|
|
1344
|
+
type: 'send-transaction',
|
|
1345
|
+
arguments: {
|
|
1346
|
+
to: '{{job-a.failing-deploy.address}}',
|
|
1347
|
+
data: '0x',
|
|
1348
|
+
value: '0'
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
]
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// Setup mocks
|
|
1355
|
+
const mockJobs = new Map<string, Job>()
|
|
1356
|
+
mockJobs.set('job-a', jobA)
|
|
1357
|
+
mockJobs.set('job-b', jobB)
|
|
1358
|
+
|
|
1359
|
+
const mockTemplates = new Map<string, Template>()
|
|
1360
|
+
|
|
1361
|
+
MockProjectLoader.mockImplementation(() => ({
|
|
1362
|
+
load: jest.fn().mockResolvedValue(undefined),
|
|
1363
|
+
jobs: mockJobs,
|
|
1364
|
+
templates: mockTemplates
|
|
1365
|
+
} as any))
|
|
1366
|
+
|
|
1367
|
+
MockDependencyGraph.mockImplementation(() => ({
|
|
1368
|
+
getExecutionOrder: jest.fn().mockReturnValue(['job-a', 'job-b']),
|
|
1369
|
+
getDependencies: jest.fn().mockReturnValue(new Set())
|
|
1370
|
+
} as any))
|
|
1371
|
+
|
|
1372
|
+
// Mock engine to simulate job A failure
|
|
1373
|
+
MockExecutionEngine.mockImplementation(() => ({
|
|
1374
|
+
executeJob: jest.fn().mockImplementation(async (job: Job) => {
|
|
1375
|
+
if (job.name === 'job-a') {
|
|
1376
|
+
throw new Error('Contract deployment failed: invalid bytecode')
|
|
1377
|
+
}
|
|
1378
|
+
// job-b should not be executed due to dependency failure
|
|
1379
|
+
})
|
|
1380
|
+
} as any))
|
|
1381
|
+
|
|
1382
|
+
MockExecutionContext.mockImplementation(() => ({
|
|
1383
|
+
dispose: jest.fn().mockResolvedValue(undefined),
|
|
1384
|
+
getNetwork: jest.fn().mockReturnValue(mockNetwork1)
|
|
1385
|
+
} as any))
|
|
1386
|
+
|
|
1387
|
+
const deployer = new Deployer(deployerOptions)
|
|
1388
|
+
|
|
1389
|
+
// Execute deployer - should fail due to job A failure
|
|
1390
|
+
await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
|
|
1391
|
+
|
|
1392
|
+
// Verify that job A failed
|
|
1393
|
+
const results = (deployer as any).results
|
|
1394
|
+
const jobAResult = results.get('job-a')
|
|
1395
|
+
expect(jobAResult).toBeDefined()
|
|
1396
|
+
expect(jobAResult.outputs.get(mockNetwork1.chainId).status).toBe('error')
|
|
1397
|
+
|
|
1398
|
+
// Verify that job B failed due to dependency failure
|
|
1399
|
+
const jobBResult = results.get('job-b')
|
|
1400
|
+
expect(jobBResult).toBeDefined()
|
|
1401
|
+
const jobBError = jobBResult.outputs.get(mockNetwork1.chainId)
|
|
1402
|
+
expect(jobBError.status).toBe('error')
|
|
1403
|
+
expect(jobBError.data).toContain('depends on "job-a", but "job-a" failed')
|
|
1404
|
+
})
|
|
1405
|
+
|
|
1406
|
+
it('should fail job B when referencing non-existent job outputs', async () => {
|
|
1407
|
+
// Create job B that references non-existent job
|
|
1408
|
+
const jobB: Job = {
|
|
1409
|
+
name: 'job-b',
|
|
1410
|
+
version: '1',
|
|
1411
|
+
description: 'Reference non-existent job outputs',
|
|
1412
|
+
actions: [
|
|
1413
|
+
{
|
|
1414
|
+
name: 'use-output-step',
|
|
1415
|
+
type: 'send-transaction',
|
|
1416
|
+
arguments: {
|
|
1417
|
+
to: '{{non-existent-job.deploy-step.address}}',
|
|
1418
|
+
data: '0x',
|
|
1419
|
+
value: '0'
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
]
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Setup mocks
|
|
1426
|
+
const mockJobs = new Map<string, Job>()
|
|
1427
|
+
mockJobs.set('job-b', jobB)
|
|
1428
|
+
|
|
1429
|
+
const mockTemplates = new Map<string, Template>()
|
|
1430
|
+
|
|
1431
|
+
MockProjectLoader.mockImplementation(() => ({
|
|
1432
|
+
load: jest.fn().mockResolvedValue(undefined),
|
|
1433
|
+
jobs: mockJobs,
|
|
1434
|
+
templates: mockTemplates
|
|
1435
|
+
} as any))
|
|
1436
|
+
|
|
1437
|
+
MockDependencyGraph.mockImplementation(() => ({
|
|
1438
|
+
getExecutionOrder: jest.fn().mockReturnValue(['job-b']),
|
|
1439
|
+
getDependencies: jest.fn().mockReturnValue(new Set())
|
|
1440
|
+
} as any))
|
|
1441
|
+
|
|
1442
|
+
// Mock engine to simulate expression resolution failure
|
|
1443
|
+
MockExecutionEngine.mockImplementation(() => ({
|
|
1444
|
+
executeJob: jest.fn().mockRejectedValue(new Error('Output for key "non-existent-job.deploy-step.address" not found in context'))
|
|
1445
|
+
} as any))
|
|
1446
|
+
|
|
1447
|
+
MockExecutionContext.mockImplementation(() => ({
|
|
1448
|
+
dispose: jest.fn().mockResolvedValue(undefined),
|
|
1449
|
+
getNetwork: jest.fn().mockReturnValue(mockNetwork1)
|
|
1450
|
+
} as any))
|
|
1451
|
+
|
|
1452
|
+
const deployer = new Deployer(deployerOptions)
|
|
1453
|
+
|
|
1454
|
+
// Execute deployer - should fail due to expression resolution
|
|
1455
|
+
await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
|
|
1456
|
+
|
|
1457
|
+
// Verify that job B failed
|
|
1458
|
+
const results = (deployer as any).results
|
|
1459
|
+
const jobBResult = results.get('job-b')
|
|
1460
|
+
expect(jobBResult).toBeDefined()
|
|
1461
|
+
expect(jobBResult.outputs.get(mockNetwork1.chainId).status).toBe('error')
|
|
1462
|
+
})
|
|
1463
|
+
|
|
1464
|
+
it('should handle job B with no dependency on job A but references job A outputs', async () => {
|
|
1465
|
+
// This tests the edge case where a job references outputs from another job
|
|
1466
|
+
// without explicitly declaring a dependency. This should work if job A succeeds.
|
|
1467
|
+
|
|
1468
|
+
const jobA: Job = {
|
|
1469
|
+
name: 'job-a',
|
|
1470
|
+
version: '1',
|
|
1471
|
+
description: 'Deploy a contract successfully',
|
|
1472
|
+
actions: [
|
|
1473
|
+
{
|
|
1474
|
+
name: 'deploy-step',
|
|
1475
|
+
type: 'create-contract',
|
|
1476
|
+
arguments: {
|
|
1477
|
+
bytecode: TEST_BYTECODES.SIMPLE_CONTRACT,
|
|
1478
|
+
value: '0'
|
|
1479
|
+
},
|
|
1480
|
+
output: true
|
|
1481
|
+
}
|
|
1482
|
+
]
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
const jobB: Job = {
|
|
1486
|
+
name: 'job-b',
|
|
1487
|
+
version: '1',
|
|
1488
|
+
description: 'Reference job A outputs without explicit dependency',
|
|
1489
|
+
// No depends_on field - this is the key difference
|
|
1490
|
+
actions: [
|
|
1491
|
+
{
|
|
1492
|
+
name: 'use-output-step',
|
|
1493
|
+
type: 'send-transaction',
|
|
1494
|
+
arguments: {
|
|
1495
|
+
to: '{{job-a.deploy-step.address}}',
|
|
1496
|
+
data: '0x',
|
|
1497
|
+
value: '0'
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
]
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// Setup mocks
|
|
1504
|
+
const mockJobs = new Map<string, Job>()
|
|
1505
|
+
mockJobs.set('job-a', jobA)
|
|
1506
|
+
mockJobs.set('job-b', jobB)
|
|
1507
|
+
|
|
1508
|
+
const mockTemplates = new Map<string, Template>()
|
|
1509
|
+
|
|
1510
|
+
MockProjectLoader.mockImplementation(() => ({
|
|
1511
|
+
load: jest.fn().mockResolvedValue(undefined),
|
|
1512
|
+
jobs: mockJobs,
|
|
1513
|
+
templates: mockTemplates
|
|
1514
|
+
} as any))
|
|
1515
|
+
|
|
1516
|
+
MockDependencyGraph.mockImplementation(() => ({
|
|
1517
|
+
getExecutionOrder: jest.fn().mockReturnValue(['job-a', 'job-b']),
|
|
1518
|
+
getDependencies: jest.fn().mockReturnValue(new Set())
|
|
1519
|
+
} as any))
|
|
1520
|
+
|
|
1521
|
+
// Mock engine to simulate successful execution
|
|
1522
|
+
MockExecutionEngine.mockImplementation(() => ({
|
|
1523
|
+
executeJob: jest.fn().mockImplementation(async (job: Job) => {
|
|
1524
|
+
if (job.name === 'job-a') {
|
|
1525
|
+
// Simulate successful job A execution
|
|
1526
|
+
return Promise.resolve()
|
|
1527
|
+
} else if (job.name === 'job-b') {
|
|
1528
|
+
// Simulate successful job B execution (no dependency check needed)
|
|
1529
|
+
return Promise.resolve()
|
|
1530
|
+
}
|
|
1531
|
+
})
|
|
1532
|
+
} as any))
|
|
1533
|
+
|
|
1534
|
+
MockExecutionContext.mockImplementation(() => ({
|
|
1535
|
+
dispose: jest.fn().mockResolvedValue(undefined),
|
|
1536
|
+
getNetwork: jest.fn().mockReturnValue(mockNetwork1),
|
|
1537
|
+
getOutputs: jest.fn().mockReturnValue(new Map([
|
|
1538
|
+
['deploy-step.address', '0x5FbDB2315678afecb367f032d93F642f64180aa3'],
|
|
1539
|
+
['deploy-step.hash', '0xmockdeployhash123']
|
|
1540
|
+
]))
|
|
1541
|
+
} as any))
|
|
1542
|
+
|
|
1543
|
+
const deployer = new Deployer(deployerOptions)
|
|
1544
|
+
|
|
1545
|
+
// Execute deployer - should succeed since job A succeeds and job B has no explicit dependency
|
|
1546
|
+
await expect(deployer.run()).resolves.not.toThrow()
|
|
1547
|
+
|
|
1548
|
+
// Verify both jobs succeeded
|
|
1549
|
+
const results = (deployer as any).results
|
|
1550
|
+
const jobAResult = results.get('job-a')
|
|
1551
|
+
const jobBResult = results.get('job-b')
|
|
1552
|
+
|
|
1553
|
+
expect(jobAResult).toBeDefined()
|
|
1554
|
+
expect(jobAResult.outputs.get(mockNetwork1.chainId).status).toBe('success')
|
|
1555
|
+
|
|
1556
|
+
expect(jobBResult).toBeDefined()
|
|
1557
|
+
expect(jobBResult.outputs.get(mockNetwork1.chainId).status).toBe('success')
|
|
1558
|
+
})
|
|
1559
|
+
|
|
1560
|
+
it('should fail job B when job A fails, even with complex output references', async () => {
|
|
1561
|
+
// This tests the scenario where job B references multiple outputs from job A
|
|
1562
|
+
// and job A fails, ensuring job B fails due to dependency failure, not expression resolution
|
|
1563
|
+
|
|
1564
|
+
const jobA: Job = {
|
|
1565
|
+
name: 'job-a',
|
|
1566
|
+
version: '1',
|
|
1567
|
+
description: 'Deploy a contract with broken bytecode (will fail)',
|
|
1568
|
+
actions: [
|
|
1569
|
+
{
|
|
1570
|
+
name: 'failing-deploy',
|
|
1571
|
+
type: 'create-contract',
|
|
1572
|
+
arguments: {
|
|
1573
|
+
bytecode: TEST_BYTECODES.BROKEN_BYTECODE,
|
|
1574
|
+
value: '0'
|
|
1575
|
+
},
|
|
1576
|
+
output: true
|
|
1577
|
+
}
|
|
1578
|
+
]
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
const jobB: Job = {
|
|
1582
|
+
name: 'job-b',
|
|
1583
|
+
version: '1',
|
|
1584
|
+
description: 'Use multiple outputs from failed job A',
|
|
1585
|
+
depends_on: ['job-a'],
|
|
1586
|
+
actions: [
|
|
1587
|
+
{
|
|
1588
|
+
name: 'use-multiple-outputs',
|
|
1589
|
+
type: 'send-transaction',
|
|
1590
|
+
arguments: {
|
|
1591
|
+
to: '{{job-a.failing-deploy.address}}',
|
|
1592
|
+
data: '0x',
|
|
1593
|
+
value: '0'
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
]
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// Setup mocks
|
|
1600
|
+
const mockJobs = new Map<string, Job>()
|
|
1601
|
+
mockJobs.set('job-a', jobA)
|
|
1602
|
+
mockJobs.set('job-b', jobB)
|
|
1603
|
+
|
|
1604
|
+
const mockTemplates = new Map<string, Template>()
|
|
1605
|
+
|
|
1606
|
+
MockProjectLoader.mockImplementation(() => ({
|
|
1607
|
+
load: jest.fn().mockResolvedValue(undefined),
|
|
1608
|
+
jobs: mockJobs,
|
|
1609
|
+
templates: mockTemplates
|
|
1610
|
+
} as any))
|
|
1611
|
+
|
|
1612
|
+
MockDependencyGraph.mockImplementation(() => ({
|
|
1613
|
+
getExecutionOrder: jest.fn().mockReturnValue(['job-a', 'job-b']),
|
|
1614
|
+
getDependencies: jest.fn().mockReturnValue(new Set())
|
|
1615
|
+
} as any))
|
|
1616
|
+
|
|
1617
|
+
// Mock engine to simulate job A failure
|
|
1618
|
+
MockExecutionEngine.mockImplementation(() => ({
|
|
1619
|
+
executeJob: jest.fn().mockImplementation(async (job: Job) => {
|
|
1620
|
+
if (job.name === 'job-a') {
|
|
1621
|
+
throw new Error('Contract deployment failed: invalid bytecode')
|
|
1622
|
+
}
|
|
1623
|
+
// job-b should not be executed due to dependency failure
|
|
1624
|
+
})
|
|
1625
|
+
} as any))
|
|
1626
|
+
|
|
1627
|
+
MockExecutionContext.mockImplementation(() => ({
|
|
1628
|
+
dispose: jest.fn().mockResolvedValue(undefined),
|
|
1629
|
+
getNetwork: jest.fn().mockReturnValue(mockNetwork1)
|
|
1630
|
+
} as any))
|
|
1631
|
+
|
|
1632
|
+
const deployer = new Deployer(deployerOptions)
|
|
1633
|
+
|
|
1634
|
+
// Execute deployer - should fail due to job A failure
|
|
1635
|
+
await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
|
|
1636
|
+
|
|
1637
|
+
// Verify that job A failed
|
|
1638
|
+
const results = (deployer as any).results
|
|
1639
|
+
const jobAResult = results.get('job-a')
|
|
1640
|
+
expect(jobAResult).toBeDefined()
|
|
1641
|
+
expect(jobAResult.outputs.get(mockNetwork1.chainId).status).toBe('error')
|
|
1642
|
+
|
|
1643
|
+
// Verify that job B failed due to dependency failure
|
|
1644
|
+
const jobBResult = results.get('job-b')
|
|
1645
|
+
expect(jobBResult).toBeDefined()
|
|
1646
|
+
const jobBError = jobBResult.outputs.get(mockNetwork1.chainId)
|
|
1647
|
+
expect(jobBError.status).toBe('error')
|
|
1648
|
+
expect(jobBError.data).toContain('depends on "job-a", but "job-a" failed')
|
|
1649
|
+
})
|
|
1650
|
+
})
|
|
1651
|
+
})
|