@0xsequence/catapult 1.3.7 → 1.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1557,6 +1557,108 @@ describe('Deployer', () => {
1557
1557
  expect(jobBResult.outputs.get(mockNetwork1.chainId).status).toBe('success')
1558
1558
  })
1559
1559
 
1560
+ it('should allow job B to run when job A is skipped', async () => {
1561
+ // This tests the scenario where job A is skipped (e.g., due to skip_condition)
1562
+ // but job B should still be allowed to run since it doesn't depend on job A's outputs
1563
+
1564
+ const jobA: Job = {
1565
+ name: 'job-a',
1566
+ version: '1',
1567
+ description: 'Job A that will be skipped',
1568
+ skip_condition: [true], // This will cause job A to be skipped
1569
+ actions: [
1570
+ {
1571
+ name: 'deploy-step',
1572
+ type: 'create-contract',
1573
+ arguments: {
1574
+ bytecode: TEST_BYTECODES.SIMPLE_CONTRACT,
1575
+ value: '0'
1576
+ },
1577
+ output: true
1578
+ }
1579
+ ]
1580
+ }
1581
+
1582
+ const jobB: Job = {
1583
+ name: 'job-b',
1584
+ version: '1',
1585
+ description: 'Job B that should run even if job A is skipped',
1586
+ depends_on: ['job-a'], // Declares dependency on job A
1587
+ actions: [
1588
+ {
1589
+ name: 'independent-action',
1590
+ type: 'send-transaction',
1591
+ arguments: {
1592
+ to: '0x1234567890123456789012345678901234567890',
1593
+ data: '0x',
1594
+ value: '0'
1595
+ }
1596
+ }
1597
+ ]
1598
+ }
1599
+
1600
+ // Setup mocks
1601
+ const mockJobs = new Map<string, Job>()
1602
+ mockJobs.set('job-a', jobA)
1603
+ mockJobs.set('job-b', jobB)
1604
+
1605
+ const mockTemplates = new Map<string, Template>()
1606
+
1607
+ MockProjectLoader.mockImplementation(() => ({
1608
+ load: jest.fn().mockResolvedValue(undefined),
1609
+ jobs: mockJobs,
1610
+ templates: mockTemplates
1611
+ } as any))
1612
+
1613
+ MockDependencyGraph.mockImplementation(() => ({
1614
+ getExecutionOrder: jest.fn().mockReturnValue(['job-a', 'job-b']),
1615
+ getDependencies: jest.fn().mockReturnValue(new Set())
1616
+ } as any))
1617
+
1618
+ // Mock engine to simulate job A being skipped and job B running successfully
1619
+ MockExecutionEngine.mockImplementation(() => ({
1620
+ executeJob: jest.fn().mockImplementation(async (job: Job) => {
1621
+ if (job.name === 'job-a') {
1622
+ // Job A should be skipped due to skip_condition
1623
+ throw new Error('Job "job-a" skipped due to skip condition')
1624
+ } else if (job.name === 'job-b') {
1625
+ // Job B should run successfully even though job A was skipped
1626
+ return Promise.resolve()
1627
+ }
1628
+ }),
1629
+ evaluateSkipConditions: jest.fn().mockImplementation(async (conditions: any, context: any, scope: any) => {
1630
+ // For job-a with skip_condition: [true], return true (should skip)
1631
+ // For other jobs, return false (should not skip)
1632
+ return conditions && conditions.length > 0 && conditions[0] === true
1633
+ })
1634
+ } as any))
1635
+
1636
+ MockExecutionContext.mockImplementation(() => ({
1637
+ dispose: jest.fn().mockResolvedValue(undefined),
1638
+ getNetwork: jest.fn().mockReturnValue(mockNetwork1),
1639
+ getOutputs: jest.fn().mockReturnValue(new Map([
1640
+ ['independent-action.hash', '0xmocktransactionhash']
1641
+ ]))
1642
+ } as any))
1643
+
1644
+ const deployer = new Deployer(deployerOptions)
1645
+
1646
+ // Execute deployer - should succeed since job B can run independently
1647
+ await expect(deployer.run()).resolves.not.toThrow()
1648
+
1649
+ // Verify that job A was skipped
1650
+ const results = (deployer as any).results
1651
+ const jobAResult = results.get('job-a')
1652
+ expect(jobAResult).toBeDefined()
1653
+ expect(jobAResult.outputs.get(mockNetwork1.chainId).status).toBe('skipped')
1654
+ expect(jobAResult.outputs.get(mockNetwork1.chainId).data).toContain('skipped due to skip condition')
1655
+
1656
+ // Verify that job B ran successfully
1657
+ const jobBResult = results.get('job-b')
1658
+ expect(jobBResult).toBeDefined()
1659
+ expect(jobBResult.outputs.get(mockNetwork1.chainId).status).toBe('success')
1660
+ })
1661
+
1560
1662
  it('should fail job B when job A fails, even with complex output references', async () => {
1561
1663
  // This tests the scenario where job B references multiple outputs from job A
1562
1664
  // and job A fails, ensuring job B fails due to dependency failure, not expression resolution
@@ -1648,4 +1750,316 @@ describe('Deployer', () => {
1648
1750
  expect(jobBError.data).toContain('depends on "job-a", but "job-a" failed')
1649
1751
  })
1650
1752
  })
1753
+
1754
+ describe('run summary functionality', () => {
1755
+ let mockEventEmitter: jest.Mocked<any>
1756
+ let deployer: Deployer
1757
+
1758
+ beforeEach(() => {
1759
+ // Create a mock event emitter to track events
1760
+ mockEventEmitter = {
1761
+ emitEvent: jest.fn()
1762
+ }
1763
+
1764
+ // Create deployer with mock event emitter
1765
+ deployer = new Deployer({
1766
+ ...deployerOptions,
1767
+ eventEmitter: mockEventEmitter as any
1768
+ })
1769
+ })
1770
+
1771
+ it('should emit run summary with success counts when all jobs succeed', () => {
1772
+ // Mock the results property to simulate successful job execution
1773
+ const mockResults = new Map()
1774
+ mockResults.set('job1', {
1775
+ job: mockJob1,
1776
+ outputs: new Map([
1777
+ [1, { status: 'success', data: new Map([['action1.hash', '0xhash1'], ['action1.address', '0x1234567890123456789012345678901234567890']]) }],
1778
+ [137, { status: 'success', data: new Map([['action1.hash', '0xhash1'], ['action1.address', '0x1234567890123456789012345678901234567890']]) }]
1779
+ ])
1780
+ })
1781
+ mockResults.set('job2', {
1782
+ job: mockJob2,
1783
+ outputs: new Map([
1784
+ [1, { status: 'success', data: new Map([['action2.hash', '0xhash2'], ['action2.address', '0x9876543210987654321098765432109876543210']]) }],
1785
+ [137, { status: 'success', data: new Map([['action2.hash', '0xhash2'], ['action2.address', '0x9876543210987654321098765432109876543210']]) }]
1786
+ ])
1787
+ })
1788
+ mockResults.set('job3', {
1789
+ job: mockJob3,
1790
+ outputs: new Map([
1791
+ [1, { status: 'success', data: new Map([['action3.hash', '0xhash3']]) }]
1792
+ ])
1793
+ })
1794
+
1795
+ // Set the results property
1796
+ ;(deployer as any).results = mockResults
1797
+
1798
+ // Call emitRunSummary directly
1799
+ ;(deployer as any).emitRunSummary(false)
1800
+
1801
+ // Verify run summary was emitted
1802
+ expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(
1803
+ expect.objectContaining({
1804
+ type: 'run_summary',
1805
+ level: 'info', // Should be 'info' when no failures
1806
+ data: expect.objectContaining({
1807
+ networkCount: 2, // mainnet and polygon
1808
+ jobCount: 3, // job1, job2, job3
1809
+ successCount: 5, // job1&job2 on 2 networks + job3 on 1 network
1810
+ failedCount: 0,
1811
+ skippedCount: 0,
1812
+ keyContracts: expect.arrayContaining([
1813
+ { job: 'job1', action: 'action1', address: '0x1234567890123456789012345678901234567890' },
1814
+ { job: 'job2', action: 'action2', address: '0x9876543210987654321098765432109876543210' }
1815
+ ])
1816
+ })
1817
+ })
1818
+ )
1819
+ })
1820
+
1821
+ it('should emit run summary with failure counts when some jobs fail', () => {
1822
+ // Mock the results property to simulate mixed success/failure
1823
+ const mockResults = new Map()
1824
+ mockResults.set('job1', {
1825
+ job: mockJob1,
1826
+ outputs: new Map([
1827
+ [1, { status: 'error', data: 'Job1 failed' }],
1828
+ [137, { status: 'error', data: 'Job1 failed' }]
1829
+ ])
1830
+ })
1831
+ mockResults.set('job2', {
1832
+ job: mockJob2,
1833
+ outputs: new Map([
1834
+ [1, { status: 'success', data: new Map([['action2.hash', '0xhash2'], ['action2.address', '0x9876543210987654321098765432109876543210']]) }],
1835
+ [137, { status: 'success', data: new Map([['action2.hash', '0xhash2'], ['action2.address', '0x9876543210987654321098765432109876543210']]) }]
1836
+ ])
1837
+ })
1838
+ mockResults.set('job3', {
1839
+ job: mockJob3,
1840
+ outputs: new Map([
1841
+ [1, { status: 'success', data: new Map([['action3.hash', '0xhash3']]) }]
1842
+ ])
1843
+ })
1844
+
1845
+ // Set the results property
1846
+ ;(deployer as any).results = mockResults
1847
+
1848
+ // Call emitRunSummary with hasFailures = true
1849
+ ;(deployer as any).emitRunSummary(true)
1850
+
1851
+ // Verify run summary was emitted with failure info
1852
+ expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(
1853
+ expect.objectContaining({
1854
+ type: 'run_summary',
1855
+ level: 'warn', // Should be 'warn' when there are failures
1856
+ data: expect.objectContaining({
1857
+ networkCount: 2,
1858
+ jobCount: 3,
1859
+ successCount: 3, // job2&job3 succeed on their networks
1860
+ failedCount: 2, // job1 fails on both networks
1861
+ skippedCount: 0,
1862
+ keyContracts: expect.arrayContaining([
1863
+ { job: 'job2', action: 'action2', address: '0x9876543210987654321098765432109876543210' }
1864
+ ])
1865
+ })
1866
+ })
1867
+ )
1868
+ })
1869
+
1870
+ it('should emit run summary with skipped counts when jobs are skipped', () => {
1871
+ // Mock the results property to simulate skipped jobs
1872
+ const mockResults = new Map()
1873
+ mockResults.set('job1', {
1874
+ job: mockJob1,
1875
+ outputs: new Map([
1876
+ [1, { status: 'skipped', data: 'Job skipped due to network filter' }],
1877
+ [137, { status: 'skipped', data: 'Job skipped due to network filter' }]
1878
+ ])
1879
+ })
1880
+
1881
+ // Set the results property
1882
+ ;(deployer as any).results = mockResults
1883
+
1884
+ // Call emitRunSummary with hasFailures = false
1885
+ ;(deployer as any).emitRunSummary(false)
1886
+
1887
+ // Verify run summary was emitted with skipped info
1888
+ expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(
1889
+ expect.objectContaining({
1890
+ type: 'run_summary',
1891
+ level: 'info',
1892
+ data: expect.objectContaining({
1893
+ networkCount: 2,
1894
+ jobCount: 1,
1895
+ successCount: 0,
1896
+ failedCount: 0,
1897
+ skippedCount: 2, // job1 skipped on both networks
1898
+ keyContracts: []
1899
+ })
1900
+ })
1901
+ )
1902
+ })
1903
+
1904
+ it('should limit key contracts to 10 entries', () => {
1905
+ // Create a job with many contract addresses
1906
+ const manyContractsJob: Job = {
1907
+ name: 'many-contracts-job',
1908
+ version: '1.0.0',
1909
+ actions: Array.from({ length: 15 }, (_, i) => ({
1910
+ name: `action${i}`,
1911
+ template: 'template1',
1912
+ arguments: {}
1913
+ }))
1914
+ }
1915
+
1916
+ // Mock the results property with many contract addresses
1917
+ const mockResults = new Map()
1918
+ const manyOutputs = new Map<string, any>()
1919
+ for (let i = 0; i < 15; i++) {
1920
+ manyOutputs.set(`action${i}.address`, `0x${i.toString().padStart(40, '0')}`)
1921
+ manyOutputs.set(`action${i}.hash`, `0xhash${i}`)
1922
+ }
1923
+
1924
+ mockResults.set('many-contracts-job', {
1925
+ job: manyContractsJob,
1926
+ outputs: new Map([
1927
+ [1, { status: 'success', data: manyOutputs }]
1928
+ ])
1929
+ })
1930
+
1931
+ // Set the results property
1932
+ ;(deployer as any).results = mockResults
1933
+
1934
+ // Call emitRunSummary
1935
+ ;(deployer as any).emitRunSummary(false)
1936
+
1937
+ // Verify run summary limits key contracts to 10
1938
+ expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(
1939
+ expect.objectContaining({
1940
+ type: 'run_summary',
1941
+ data: expect.objectContaining({
1942
+ keyContracts: expect.arrayContaining([
1943
+ { job: 'many-contracts-job', action: 'action0', address: '0x0000000000000000000000000000000000000000' },
1944
+ { job: 'many-contracts-job', action: 'action1', address: '0x0000000000000000000000000000000000000001' },
1945
+ // ... up to action9
1946
+ { job: 'many-contracts-job', action: 'action9', address: '0x0000000000000000000000000000000000000009' }
1947
+ ])
1948
+ })
1949
+ })
1950
+ )
1951
+
1952
+ // Verify only 10 contracts are included
1953
+ const summaryCall = mockEventEmitter.emitEvent.mock.calls.find((call: any) =>
1954
+ call[0].type === 'run_summary'
1955
+ )
1956
+ expect(summaryCall![0].data.keyContracts).toHaveLength(10)
1957
+ })
1958
+
1959
+ it('should not emit run summary when showSummary is false', () => {
1960
+ const deployerWithoutSummary = new Deployer({
1961
+ ...deployerOptions,
1962
+ eventEmitter: mockEventEmitter as any,
1963
+ showSummary: false
1964
+ })
1965
+
1966
+ // Verify that showSummary is false
1967
+ expect((deployerWithoutSummary as any).showSummary).toBe(false)
1968
+
1969
+ // Mock the results property
1970
+ const mockResults = new Map()
1971
+ mockResults.set('job1', {
1972
+ job: mockJob1,
1973
+ outputs: new Map([
1974
+ [1, { status: 'success', data: new Map([['action1.hash', '0xhash1']]) }]
1975
+ ])
1976
+ })
1977
+ ;(deployerWithoutSummary as any).results = mockResults
1978
+
1979
+ // Call emitRunSummary directly - this will emit regardless of showSummary
1980
+ // The showSummary check happens in the run() method, not in emitRunSummary itself
1981
+ ;(deployerWithoutSummary as any).emitRunSummary(false)
1982
+
1983
+ // Verify run summary WAS emitted (because we called it directly)
1984
+ expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(
1985
+ expect.objectContaining({
1986
+ type: 'run_summary'
1987
+ })
1988
+ )
1989
+ })
1990
+
1991
+ it('should emit run summary with mixed success/failure/skipped counts', () => {
1992
+ // Mock the results property to simulate mixed outcomes
1993
+ const mockResults = new Map()
1994
+ mockResults.set('success-job', {
1995
+ job: { name: 'success-job', version: '1.0.0', actions: [{ name: 'success-action', template: 'template1', arguments: {} }] },
1996
+ outputs: new Map([
1997
+ [1, { status: 'success', data: new Map([['success-action.hash', '0xsuccess'], ['success-action.address', '0x1234567890123456789012345678901234567890']]) }],
1998
+ [137, { status: 'success', data: new Map([['success-action.hash', '0xsuccess'], ['success-action.address', '0x1234567890123456789012345678901234567890']]) }]
1999
+ ])
2000
+ })
2001
+ mockResults.set('fail-job', {
2002
+ job: { name: 'fail-job', version: '1.0.0', actions: [{ name: 'fail-action', template: 'template1', arguments: {} }] },
2003
+ outputs: new Map([
2004
+ [1, { status: 'error', data: 'Fail job failed' }],
2005
+ [137, { status: 'error', data: 'Fail job failed' }]
2006
+ ])
2007
+ })
2008
+ mockResults.set('skipped-job', {
2009
+ job: { name: 'skipped-job', version: '1.0.0', actions: [{ name: 'skipped-action', template: 'template1', arguments: {} }] },
2010
+ outputs: new Map([
2011
+ [1, { status: 'skipped', data: 'Job skipped' }],
2012
+ [137, { status: 'skipped', data: 'Job skipped' }]
2013
+ ])
2014
+ })
2015
+
2016
+ // Set the results property
2017
+ ;(deployer as any).results = mockResults
2018
+
2019
+ // Call emitRunSummary with hasFailures = true
2020
+ ;(deployer as any).emitRunSummary(true)
2021
+
2022
+ // Verify run summary with mixed counts
2023
+ expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(
2024
+ expect.objectContaining({
2025
+ type: 'run_summary',
2026
+ level: 'warn', // Should be 'warn' due to failures
2027
+ data: expect.objectContaining({
2028
+ networkCount: 2,
2029
+ jobCount: 3,
2030
+ successCount: 2, // success-job on both networks
2031
+ failedCount: 2, // fail-job on both networks
2032
+ skippedCount: 2, // skipped-job on both networks
2033
+ keyContracts: expect.arrayContaining([
2034
+ { job: 'success-job', action: 'success-action', address: '0x1234567890123456789012345678901234567890' }
2035
+ ])
2036
+ })
2037
+ })
2038
+ )
2039
+ })
2040
+
2041
+ it('should handle empty results gracefully', () => {
2042
+ // Set empty results
2043
+ ;(deployer as any).results = new Map()
2044
+
2045
+ // Call emitRunSummary
2046
+ ;(deployer as any).emitRunSummary(false)
2047
+
2048
+ // Verify run summary with empty results
2049
+ expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(
2050
+ expect.objectContaining({
2051
+ type: 'run_summary',
2052
+ level: 'info',
2053
+ data: expect.objectContaining({
2054
+ networkCount: 2,
2055
+ jobCount: 0,
2056
+ successCount: 0,
2057
+ failedCount: 0,
2058
+ skippedCount: 0,
2059
+ keyContracts: []
2060
+ })
2061
+ })
2062
+ )
2063
+ })
2064
+ })
1651
2065
  })
@@ -1632,7 +1632,7 @@ export class ExecutionEngine {
1632
1632
  /**
1633
1633
  * Evaluates a list of conditions and returns true if any of them are met.
1634
1634
  */
1635
- private async evaluateSkipConditions(
1635
+ public async evaluateSkipConditions(
1636
1636
  conditions: Condition[] | undefined,
1637
1637
  context: ExecutionContext,
1638
1638
  scope: ResolutionScope,