@0xsequence/catapult 1.3.6 → 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.
package/dist/index.js CHANGED
File without changes
@@ -950,5 +950,577 @@ describe('Deployer', () => {
950
950
  }));
951
951
  });
952
952
  });
953
+ describe('job dependency failure handling', () => {
954
+ const TEST_BYTECODES = {
955
+ SIMPLE_CONTRACT: '0x6080604052348015600e575f5ffd5b5060c180601a5f395ff3fe6080604052348015600e575f5ffd5b50600436106030575f3560e01c806390c52443146034578063d09de08a14604d575b5f5ffd5b603b5f5481565b60405190815260200160405180910390f35b60536055565b005b5f805490806061836068565b9190505550565b5f60018201608457634e487b7160e01b5f52601160045260245ffd5b506001019056fea264697066735822122061c8cc43c72d6b23b16f7a7337dd15b93d71eb94a9d5247911e39f486e1f94f964736f6c634300081e0033',
956
+ BROKEN_BYTECODE: '0xff'
957
+ };
958
+ it('should fail job B when job A fails due to dependency failure', async () => {
959
+ const jobA = {
960
+ name: 'job-a',
961
+ version: '1',
962
+ description: 'Deploy a contract with broken bytecode (will fail)',
963
+ actions: [
964
+ {
965
+ name: 'failing-deploy',
966
+ type: 'create-contract',
967
+ arguments: {
968
+ bytecode: TEST_BYTECODES.BROKEN_BYTECODE,
969
+ value: '0'
970
+ },
971
+ output: true
972
+ }
973
+ ]
974
+ };
975
+ const jobB = {
976
+ name: 'job-b',
977
+ version: '1',
978
+ description: 'Use output from failed job A',
979
+ depends_on: ['job-a'],
980
+ actions: [
981
+ {
982
+ name: 'use-failed-output',
983
+ type: 'send-transaction',
984
+ arguments: {
985
+ to: '{{job-a.failing-deploy.address}}',
986
+ data: '0x',
987
+ value: '0'
988
+ }
989
+ }
990
+ ]
991
+ };
992
+ const mockJobs = new Map();
993
+ mockJobs.set('job-a', jobA);
994
+ mockJobs.set('job-b', jobB);
995
+ const mockTemplates = new Map();
996
+ MockProjectLoader.mockImplementation(() => ({
997
+ load: jest.fn().mockResolvedValue(undefined),
998
+ jobs: mockJobs,
999
+ templates: mockTemplates
1000
+ }));
1001
+ MockDependencyGraph.mockImplementation(() => ({
1002
+ getExecutionOrder: jest.fn().mockReturnValue(['job-a', 'job-b']),
1003
+ getDependencies: jest.fn().mockReturnValue(new Set())
1004
+ }));
1005
+ MockExecutionEngine.mockImplementation(() => ({
1006
+ executeJob: jest.fn().mockImplementation(async (job) => {
1007
+ if (job.name === 'job-a') {
1008
+ throw new Error('Contract deployment failed: invalid bytecode');
1009
+ }
1010
+ })
1011
+ }));
1012
+ MockExecutionContext.mockImplementation(() => ({
1013
+ dispose: jest.fn().mockResolvedValue(undefined),
1014
+ getNetwork: jest.fn().mockReturnValue(mockNetwork1)
1015
+ }));
1016
+ const deployer = new deployer_1.Deployer(deployerOptions);
1017
+ await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution');
1018
+ const results = deployer.results;
1019
+ const jobAResult = results.get('job-a');
1020
+ expect(jobAResult).toBeDefined();
1021
+ expect(jobAResult.outputs.get(mockNetwork1.chainId).status).toBe('error');
1022
+ const jobBResult = results.get('job-b');
1023
+ expect(jobBResult).toBeDefined();
1024
+ const jobBError = jobBResult.outputs.get(mockNetwork1.chainId);
1025
+ expect(jobBError.status).toBe('error');
1026
+ expect(jobBError.data).toContain('depends on "job-a", but "job-a" failed');
1027
+ });
1028
+ it('should fail job B when referencing non-existent job outputs', async () => {
1029
+ const jobB = {
1030
+ name: 'job-b',
1031
+ version: '1',
1032
+ description: 'Reference non-existent job outputs',
1033
+ actions: [
1034
+ {
1035
+ name: 'use-output-step',
1036
+ type: 'send-transaction',
1037
+ arguments: {
1038
+ to: '{{non-existent-job.deploy-step.address}}',
1039
+ data: '0x',
1040
+ value: '0'
1041
+ }
1042
+ }
1043
+ ]
1044
+ };
1045
+ const mockJobs = new Map();
1046
+ mockJobs.set('job-b', jobB);
1047
+ const mockTemplates = new Map();
1048
+ MockProjectLoader.mockImplementation(() => ({
1049
+ load: jest.fn().mockResolvedValue(undefined),
1050
+ jobs: mockJobs,
1051
+ templates: mockTemplates
1052
+ }));
1053
+ MockDependencyGraph.mockImplementation(() => ({
1054
+ getExecutionOrder: jest.fn().mockReturnValue(['job-b']),
1055
+ getDependencies: jest.fn().mockReturnValue(new Set())
1056
+ }));
1057
+ MockExecutionEngine.mockImplementation(() => ({
1058
+ executeJob: jest.fn().mockRejectedValue(new Error('Output for key "non-existent-job.deploy-step.address" not found in context'))
1059
+ }));
1060
+ MockExecutionContext.mockImplementation(() => ({
1061
+ dispose: jest.fn().mockResolvedValue(undefined),
1062
+ getNetwork: jest.fn().mockReturnValue(mockNetwork1)
1063
+ }));
1064
+ const deployer = new deployer_1.Deployer(deployerOptions);
1065
+ await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution');
1066
+ const results = deployer.results;
1067
+ const jobBResult = results.get('job-b');
1068
+ expect(jobBResult).toBeDefined();
1069
+ expect(jobBResult.outputs.get(mockNetwork1.chainId).status).toBe('error');
1070
+ });
1071
+ it('should handle job B with no dependency on job A but references job A outputs', async () => {
1072
+ const jobA = {
1073
+ name: 'job-a',
1074
+ version: '1',
1075
+ description: 'Deploy a contract successfully',
1076
+ actions: [
1077
+ {
1078
+ name: 'deploy-step',
1079
+ type: 'create-contract',
1080
+ arguments: {
1081
+ bytecode: TEST_BYTECODES.SIMPLE_CONTRACT,
1082
+ value: '0'
1083
+ },
1084
+ output: true
1085
+ }
1086
+ ]
1087
+ };
1088
+ const jobB = {
1089
+ name: 'job-b',
1090
+ version: '1',
1091
+ description: 'Reference job A outputs without explicit dependency',
1092
+ actions: [
1093
+ {
1094
+ name: 'use-output-step',
1095
+ type: 'send-transaction',
1096
+ arguments: {
1097
+ to: '{{job-a.deploy-step.address}}',
1098
+ data: '0x',
1099
+ value: '0'
1100
+ }
1101
+ }
1102
+ ]
1103
+ };
1104
+ const mockJobs = new Map();
1105
+ mockJobs.set('job-a', jobA);
1106
+ mockJobs.set('job-b', jobB);
1107
+ const mockTemplates = new Map();
1108
+ MockProjectLoader.mockImplementation(() => ({
1109
+ load: jest.fn().mockResolvedValue(undefined),
1110
+ jobs: mockJobs,
1111
+ templates: mockTemplates
1112
+ }));
1113
+ MockDependencyGraph.mockImplementation(() => ({
1114
+ getExecutionOrder: jest.fn().mockReturnValue(['job-a', 'job-b']),
1115
+ getDependencies: jest.fn().mockReturnValue(new Set())
1116
+ }));
1117
+ MockExecutionEngine.mockImplementation(() => ({
1118
+ executeJob: jest.fn().mockImplementation(async (job) => {
1119
+ if (job.name === 'job-a') {
1120
+ return Promise.resolve();
1121
+ }
1122
+ else if (job.name === 'job-b') {
1123
+ return Promise.resolve();
1124
+ }
1125
+ })
1126
+ }));
1127
+ MockExecutionContext.mockImplementation(() => ({
1128
+ dispose: jest.fn().mockResolvedValue(undefined),
1129
+ getNetwork: jest.fn().mockReturnValue(mockNetwork1),
1130
+ getOutputs: jest.fn().mockReturnValue(new Map([
1131
+ ['deploy-step.address', '0x5FbDB2315678afecb367f032d93F642f64180aa3'],
1132
+ ['deploy-step.hash', '0xmockdeployhash123']
1133
+ ]))
1134
+ }));
1135
+ const deployer = new deployer_1.Deployer(deployerOptions);
1136
+ await expect(deployer.run()).resolves.not.toThrow();
1137
+ const results = deployer.results;
1138
+ const jobAResult = results.get('job-a');
1139
+ const jobBResult = results.get('job-b');
1140
+ expect(jobAResult).toBeDefined();
1141
+ expect(jobAResult.outputs.get(mockNetwork1.chainId).status).toBe('success');
1142
+ expect(jobBResult).toBeDefined();
1143
+ expect(jobBResult.outputs.get(mockNetwork1.chainId).status).toBe('success');
1144
+ });
1145
+ it('should allow job B to run when job A is skipped', async () => {
1146
+ const jobA = {
1147
+ name: 'job-a',
1148
+ version: '1',
1149
+ description: 'Job A that will be skipped',
1150
+ skip_condition: [true],
1151
+ actions: [
1152
+ {
1153
+ name: 'deploy-step',
1154
+ type: 'create-contract',
1155
+ arguments: {
1156
+ bytecode: TEST_BYTECODES.SIMPLE_CONTRACT,
1157
+ value: '0'
1158
+ },
1159
+ output: true
1160
+ }
1161
+ ]
1162
+ };
1163
+ const jobB = {
1164
+ name: 'job-b',
1165
+ version: '1',
1166
+ description: 'Job B that should run even if job A is skipped',
1167
+ depends_on: ['job-a'],
1168
+ actions: [
1169
+ {
1170
+ name: 'independent-action',
1171
+ type: 'send-transaction',
1172
+ arguments: {
1173
+ to: '0x1234567890123456789012345678901234567890',
1174
+ data: '0x',
1175
+ value: '0'
1176
+ }
1177
+ }
1178
+ ]
1179
+ };
1180
+ const mockJobs = new Map();
1181
+ mockJobs.set('job-a', jobA);
1182
+ mockJobs.set('job-b', jobB);
1183
+ const mockTemplates = new Map();
1184
+ MockProjectLoader.mockImplementation(() => ({
1185
+ load: jest.fn().mockResolvedValue(undefined),
1186
+ jobs: mockJobs,
1187
+ templates: mockTemplates
1188
+ }));
1189
+ MockDependencyGraph.mockImplementation(() => ({
1190
+ getExecutionOrder: jest.fn().mockReturnValue(['job-a', 'job-b']),
1191
+ getDependencies: jest.fn().mockReturnValue(new Set())
1192
+ }));
1193
+ MockExecutionEngine.mockImplementation(() => ({
1194
+ executeJob: jest.fn().mockImplementation(async (job) => {
1195
+ if (job.name === 'job-a') {
1196
+ throw new Error('Job "job-a" skipped due to skip condition');
1197
+ }
1198
+ else if (job.name === 'job-b') {
1199
+ return Promise.resolve();
1200
+ }
1201
+ }),
1202
+ evaluateSkipConditions: jest.fn().mockImplementation(async (conditions, context, scope) => {
1203
+ return conditions && conditions.length > 0 && conditions[0] === true;
1204
+ })
1205
+ }));
1206
+ MockExecutionContext.mockImplementation(() => ({
1207
+ dispose: jest.fn().mockResolvedValue(undefined),
1208
+ getNetwork: jest.fn().mockReturnValue(mockNetwork1),
1209
+ getOutputs: jest.fn().mockReturnValue(new Map([
1210
+ ['independent-action.hash', '0xmocktransactionhash']
1211
+ ]))
1212
+ }));
1213
+ const deployer = new deployer_1.Deployer(deployerOptions);
1214
+ await expect(deployer.run()).resolves.not.toThrow();
1215
+ const results = deployer.results;
1216
+ const jobAResult = results.get('job-a');
1217
+ expect(jobAResult).toBeDefined();
1218
+ expect(jobAResult.outputs.get(mockNetwork1.chainId).status).toBe('skipped');
1219
+ expect(jobAResult.outputs.get(mockNetwork1.chainId).data).toContain('skipped due to skip condition');
1220
+ const jobBResult = results.get('job-b');
1221
+ expect(jobBResult).toBeDefined();
1222
+ expect(jobBResult.outputs.get(mockNetwork1.chainId).status).toBe('success');
1223
+ });
1224
+ it('should fail job B when job A fails, even with complex output references', async () => {
1225
+ const jobA = {
1226
+ name: 'job-a',
1227
+ version: '1',
1228
+ description: 'Deploy a contract with broken bytecode (will fail)',
1229
+ actions: [
1230
+ {
1231
+ name: 'failing-deploy',
1232
+ type: 'create-contract',
1233
+ arguments: {
1234
+ bytecode: TEST_BYTECODES.BROKEN_BYTECODE,
1235
+ value: '0'
1236
+ },
1237
+ output: true
1238
+ }
1239
+ ]
1240
+ };
1241
+ const jobB = {
1242
+ name: 'job-b',
1243
+ version: '1',
1244
+ description: 'Use multiple outputs from failed job A',
1245
+ depends_on: ['job-a'],
1246
+ actions: [
1247
+ {
1248
+ name: 'use-multiple-outputs',
1249
+ type: 'send-transaction',
1250
+ arguments: {
1251
+ to: '{{job-a.failing-deploy.address}}',
1252
+ data: '0x',
1253
+ value: '0'
1254
+ }
1255
+ }
1256
+ ]
1257
+ };
1258
+ const mockJobs = new Map();
1259
+ mockJobs.set('job-a', jobA);
1260
+ mockJobs.set('job-b', jobB);
1261
+ const mockTemplates = new Map();
1262
+ MockProjectLoader.mockImplementation(() => ({
1263
+ load: jest.fn().mockResolvedValue(undefined),
1264
+ jobs: mockJobs,
1265
+ templates: mockTemplates
1266
+ }));
1267
+ MockDependencyGraph.mockImplementation(() => ({
1268
+ getExecutionOrder: jest.fn().mockReturnValue(['job-a', 'job-b']),
1269
+ getDependencies: jest.fn().mockReturnValue(new Set())
1270
+ }));
1271
+ MockExecutionEngine.mockImplementation(() => ({
1272
+ executeJob: jest.fn().mockImplementation(async (job) => {
1273
+ if (job.name === 'job-a') {
1274
+ throw new Error('Contract deployment failed: invalid bytecode');
1275
+ }
1276
+ })
1277
+ }));
1278
+ MockExecutionContext.mockImplementation(() => ({
1279
+ dispose: jest.fn().mockResolvedValue(undefined),
1280
+ getNetwork: jest.fn().mockReturnValue(mockNetwork1)
1281
+ }));
1282
+ const deployer = new deployer_1.Deployer(deployerOptions);
1283
+ await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution');
1284
+ const results = deployer.results;
1285
+ const jobAResult = results.get('job-a');
1286
+ expect(jobAResult).toBeDefined();
1287
+ expect(jobAResult.outputs.get(mockNetwork1.chainId).status).toBe('error');
1288
+ const jobBResult = results.get('job-b');
1289
+ expect(jobBResult).toBeDefined();
1290
+ const jobBError = jobBResult.outputs.get(mockNetwork1.chainId);
1291
+ expect(jobBError.status).toBe('error');
1292
+ expect(jobBError.data).toContain('depends on "job-a", but "job-a" failed');
1293
+ });
1294
+ });
1295
+ describe('run summary functionality', () => {
1296
+ let mockEventEmitter;
1297
+ let deployer;
1298
+ beforeEach(() => {
1299
+ mockEventEmitter = {
1300
+ emitEvent: jest.fn()
1301
+ };
1302
+ deployer = new deployer_1.Deployer({
1303
+ ...deployerOptions,
1304
+ eventEmitter: mockEventEmitter
1305
+ });
1306
+ });
1307
+ it('should emit run summary with success counts when all jobs succeed', () => {
1308
+ const mockResults = new Map();
1309
+ mockResults.set('job1', {
1310
+ job: mockJob1,
1311
+ outputs: new Map([
1312
+ [1, { status: 'success', data: new Map([['action1.hash', '0xhash1'], ['action1.address', '0x1234567890123456789012345678901234567890']]) }],
1313
+ [137, { status: 'success', data: new Map([['action1.hash', '0xhash1'], ['action1.address', '0x1234567890123456789012345678901234567890']]) }]
1314
+ ])
1315
+ });
1316
+ mockResults.set('job2', {
1317
+ job: mockJob2,
1318
+ outputs: new Map([
1319
+ [1, { status: 'success', data: new Map([['action2.hash', '0xhash2'], ['action2.address', '0x9876543210987654321098765432109876543210']]) }],
1320
+ [137, { status: 'success', data: new Map([['action2.hash', '0xhash2'], ['action2.address', '0x9876543210987654321098765432109876543210']]) }]
1321
+ ])
1322
+ });
1323
+ mockResults.set('job3', {
1324
+ job: mockJob3,
1325
+ outputs: new Map([
1326
+ [1, { status: 'success', data: new Map([['action3.hash', '0xhash3']]) }]
1327
+ ])
1328
+ });
1329
+ deployer.results = mockResults;
1330
+ deployer.emitRunSummary(false);
1331
+ expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(expect.objectContaining({
1332
+ type: 'run_summary',
1333
+ level: 'info',
1334
+ data: expect.objectContaining({
1335
+ networkCount: 2,
1336
+ jobCount: 3,
1337
+ successCount: 5,
1338
+ failedCount: 0,
1339
+ skippedCount: 0,
1340
+ keyContracts: expect.arrayContaining([
1341
+ { job: 'job1', action: 'action1', address: '0x1234567890123456789012345678901234567890' },
1342
+ { job: 'job2', action: 'action2', address: '0x9876543210987654321098765432109876543210' }
1343
+ ])
1344
+ })
1345
+ }));
1346
+ });
1347
+ it('should emit run summary with failure counts when some jobs fail', () => {
1348
+ const mockResults = new Map();
1349
+ mockResults.set('job1', {
1350
+ job: mockJob1,
1351
+ outputs: new Map([
1352
+ [1, { status: 'error', data: 'Job1 failed' }],
1353
+ [137, { status: 'error', data: 'Job1 failed' }]
1354
+ ])
1355
+ });
1356
+ mockResults.set('job2', {
1357
+ job: mockJob2,
1358
+ outputs: new Map([
1359
+ [1, { status: 'success', data: new Map([['action2.hash', '0xhash2'], ['action2.address', '0x9876543210987654321098765432109876543210']]) }],
1360
+ [137, { status: 'success', data: new Map([['action2.hash', '0xhash2'], ['action2.address', '0x9876543210987654321098765432109876543210']]) }]
1361
+ ])
1362
+ });
1363
+ mockResults.set('job3', {
1364
+ job: mockJob3,
1365
+ outputs: new Map([
1366
+ [1, { status: 'success', data: new Map([['action3.hash', '0xhash3']]) }]
1367
+ ])
1368
+ });
1369
+ deployer.results = mockResults;
1370
+ deployer.emitRunSummary(true);
1371
+ expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(expect.objectContaining({
1372
+ type: 'run_summary',
1373
+ level: 'warn',
1374
+ data: expect.objectContaining({
1375
+ networkCount: 2,
1376
+ jobCount: 3,
1377
+ successCount: 3,
1378
+ failedCount: 2,
1379
+ skippedCount: 0,
1380
+ keyContracts: expect.arrayContaining([
1381
+ { job: 'job2', action: 'action2', address: '0x9876543210987654321098765432109876543210' }
1382
+ ])
1383
+ })
1384
+ }));
1385
+ });
1386
+ it('should emit run summary with skipped counts when jobs are skipped', () => {
1387
+ const mockResults = new Map();
1388
+ mockResults.set('job1', {
1389
+ job: mockJob1,
1390
+ outputs: new Map([
1391
+ [1, { status: 'skipped', data: 'Job skipped due to network filter' }],
1392
+ [137, { status: 'skipped', data: 'Job skipped due to network filter' }]
1393
+ ])
1394
+ });
1395
+ deployer.results = mockResults;
1396
+ deployer.emitRunSummary(false);
1397
+ expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(expect.objectContaining({
1398
+ type: 'run_summary',
1399
+ level: 'info',
1400
+ data: expect.objectContaining({
1401
+ networkCount: 2,
1402
+ jobCount: 1,
1403
+ successCount: 0,
1404
+ failedCount: 0,
1405
+ skippedCount: 2,
1406
+ keyContracts: []
1407
+ })
1408
+ }));
1409
+ });
1410
+ it('should limit key contracts to 10 entries', () => {
1411
+ const manyContractsJob = {
1412
+ name: 'many-contracts-job',
1413
+ version: '1.0.0',
1414
+ actions: Array.from({ length: 15 }, (_, i) => ({
1415
+ name: `action${i}`,
1416
+ template: 'template1',
1417
+ arguments: {}
1418
+ }))
1419
+ };
1420
+ const mockResults = new Map();
1421
+ const manyOutputs = new Map();
1422
+ for (let i = 0; i < 15; i++) {
1423
+ manyOutputs.set(`action${i}.address`, `0x${i.toString().padStart(40, '0')}`);
1424
+ manyOutputs.set(`action${i}.hash`, `0xhash${i}`);
1425
+ }
1426
+ mockResults.set('many-contracts-job', {
1427
+ job: manyContractsJob,
1428
+ outputs: new Map([
1429
+ [1, { status: 'success', data: manyOutputs }]
1430
+ ])
1431
+ });
1432
+ deployer.results = mockResults;
1433
+ deployer.emitRunSummary(false);
1434
+ expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(expect.objectContaining({
1435
+ type: 'run_summary',
1436
+ data: expect.objectContaining({
1437
+ keyContracts: expect.arrayContaining([
1438
+ { job: 'many-contracts-job', action: 'action0', address: '0x0000000000000000000000000000000000000000' },
1439
+ { job: 'many-contracts-job', action: 'action1', address: '0x0000000000000000000000000000000000000001' },
1440
+ { job: 'many-contracts-job', action: 'action9', address: '0x0000000000000000000000000000000000000009' }
1441
+ ])
1442
+ })
1443
+ }));
1444
+ const summaryCall = mockEventEmitter.emitEvent.mock.calls.find((call) => call[0].type === 'run_summary');
1445
+ expect(summaryCall[0].data.keyContracts).toHaveLength(10);
1446
+ });
1447
+ it('should not emit run summary when showSummary is false', () => {
1448
+ const deployerWithoutSummary = new deployer_1.Deployer({
1449
+ ...deployerOptions,
1450
+ eventEmitter: mockEventEmitter,
1451
+ showSummary: false
1452
+ });
1453
+ expect(deployerWithoutSummary.showSummary).toBe(false);
1454
+ const mockResults = new Map();
1455
+ mockResults.set('job1', {
1456
+ job: mockJob1,
1457
+ outputs: new Map([
1458
+ [1, { status: 'success', data: new Map([['action1.hash', '0xhash1']]) }]
1459
+ ])
1460
+ });
1461
+ deployerWithoutSummary.results = mockResults;
1462
+ deployerWithoutSummary.emitRunSummary(false);
1463
+ expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(expect.objectContaining({
1464
+ type: 'run_summary'
1465
+ }));
1466
+ });
1467
+ it('should emit run summary with mixed success/failure/skipped counts', () => {
1468
+ const mockResults = new Map();
1469
+ mockResults.set('success-job', {
1470
+ job: { name: 'success-job', version: '1.0.0', actions: [{ name: 'success-action', template: 'template1', arguments: {} }] },
1471
+ outputs: new Map([
1472
+ [1, { status: 'success', data: new Map([['success-action.hash', '0xsuccess'], ['success-action.address', '0x1234567890123456789012345678901234567890']]) }],
1473
+ [137, { status: 'success', data: new Map([['success-action.hash', '0xsuccess'], ['success-action.address', '0x1234567890123456789012345678901234567890']]) }]
1474
+ ])
1475
+ });
1476
+ mockResults.set('fail-job', {
1477
+ job: { name: 'fail-job', version: '1.0.0', actions: [{ name: 'fail-action', template: 'template1', arguments: {} }] },
1478
+ outputs: new Map([
1479
+ [1, { status: 'error', data: 'Fail job failed' }],
1480
+ [137, { status: 'error', data: 'Fail job failed' }]
1481
+ ])
1482
+ });
1483
+ mockResults.set('skipped-job', {
1484
+ job: { name: 'skipped-job', version: '1.0.0', actions: [{ name: 'skipped-action', template: 'template1', arguments: {} }] },
1485
+ outputs: new Map([
1486
+ [1, { status: 'skipped', data: 'Job skipped' }],
1487
+ [137, { status: 'skipped', data: 'Job skipped' }]
1488
+ ])
1489
+ });
1490
+ deployer.results = mockResults;
1491
+ deployer.emitRunSummary(true);
1492
+ expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(expect.objectContaining({
1493
+ type: 'run_summary',
1494
+ level: 'warn',
1495
+ data: expect.objectContaining({
1496
+ networkCount: 2,
1497
+ jobCount: 3,
1498
+ successCount: 2,
1499
+ failedCount: 2,
1500
+ skippedCount: 2,
1501
+ keyContracts: expect.arrayContaining([
1502
+ { job: 'success-job', action: 'success-action', address: '0x1234567890123456789012345678901234567890' }
1503
+ ])
1504
+ })
1505
+ }));
1506
+ });
1507
+ it('should handle empty results gracefully', () => {
1508
+ ;
1509
+ deployer.results = new Map();
1510
+ deployer.emitRunSummary(false);
1511
+ expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(expect.objectContaining({
1512
+ type: 'run_summary',
1513
+ level: 'info',
1514
+ data: expect.objectContaining({
1515
+ networkCount: 2,
1516
+ jobCount: 0,
1517
+ successCount: 0,
1518
+ failedCount: 0,
1519
+ skippedCount: 0,
1520
+ keyContracts: []
1521
+ })
1522
+ }));
1523
+ });
1524
+ });
953
1525
  });
954
1526
  //# sourceMappingURL=deployer.spec.js.map