github-issue-tower-defence-management 1.148.24 → 1.149.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/.github/workflows/publish.yml +40 -7
- package/.prettierignore +1 -0
- package/CHANGELOG.md +20 -0
- package/README.md +4 -4
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +2 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +1 -0
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -1
- package/bin/adapter/repositories/FileSystemKevReportWatermarkRepository.js +126 -0
- package/bin/adapter/repositories/FileSystemKevReportWatermarkRepository.js.map +1 -0
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js +1 -0
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +5 -3
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +6 -2
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/domain/entities/KevReportWatermark.js +3 -0
- package/bin/domain/entities/KevReportWatermark.js.map +1 -0
- package/bin/domain/usecases/DailySecurityScanUseCase.js +56 -7
- package/bin/domain/usecases/DailySecurityScanUseCase.js.map +1 -1
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +9 -0
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -1
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +12 -11
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +19 -0
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/adapter-interfaces/KevReportWatermarkRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/KevReportWatermarkRepository.js.map +1 -0
- package/package.json +1 -1
- package/scripts/defaultBranchTipVerify.sh +23 -0
- package/scripts/testWorkflowRunVerify.sh +47 -0
- package/src/adapter/ci/publishTestWorkflowGate.test.ts +653 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +2 -0
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +35 -0
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +7 -0
- package/src/adapter/repositories/FileSystemKevReportWatermarkRepository.test.ts +253 -0
- package/src/adapter/repositories/FileSystemKevReportWatermarkRepository.ts +130 -0
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.test.ts +2 -0
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.ts +1 -0
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.test.ts +3 -0
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +159 -0
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +7 -1
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +108 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +10 -2
- package/src/domain/entities/KevReportWatermark.ts +4 -0
- package/src/domain/entities/LiveSessionActivitySnapshot.ts +1 -0
- package/src/domain/usecases/DailySecurityScanUseCase.test.ts +210 -0
- package/src/domain/usecases/DailySecurityScanUseCase.ts +84 -7
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +58 -1
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +21 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +28 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +146 -8
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +19 -10
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +212 -0
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +32 -0
- package/src/domain/usecases/adapter-interfaces/KevReportWatermarkRepository.ts +11 -0
- package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +3 -0
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts +2 -0
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/adapter/repositories/FileSystemKevReportWatermarkRepository.d.ts +10 -0
- package/types/adapter/repositories/FileSystemKevReportWatermarkRepository.d.ts.map +1 -0
- package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/entities/KevReportWatermark.d.ts +5 -0
- package/types/domain/entities/KevReportWatermark.d.ts.map +1 -0
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +1 -0
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -1
- package/types/domain/usecases/DailySecurityScanUseCase.d.ts +3 -1
- package/types/domain/usecases/DailySecurityScanUseCase.d.ts.map +1 -1
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +2 -0
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts +3 -2
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/KevReportWatermarkRepository.d.ts +15 -0
- package/types/domain/usecases/adapter-interfaces/KevReportWatermarkRepository.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts +1 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts.map +1 -1
|
@@ -279,6 +279,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
279
279
|
silentSeconds: 120,
|
|
280
280
|
runningSeconds: 900,
|
|
281
281
|
waitingOnExternalProcess: false,
|
|
282
|
+
finishedResultUnconsumed: false,
|
|
282
283
|
},
|
|
283
284
|
]);
|
|
284
285
|
});
|
|
@@ -400,6 +401,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
400
401
|
silentSeconds: 60,
|
|
401
402
|
runningSeconds: 600,
|
|
402
403
|
waitingOnExternalProcess: false,
|
|
404
|
+
finishedResultUnconsumed: false,
|
|
403
405
|
},
|
|
404
406
|
]);
|
|
405
407
|
});
|
|
@@ -474,6 +476,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
474
476
|
silentSeconds: 60,
|
|
475
477
|
runningSeconds: 600,
|
|
476
478
|
waitingOnExternalProcess: false,
|
|
479
|
+
finishedResultUnconsumed: false,
|
|
477
480
|
},
|
|
478
481
|
]);
|
|
479
482
|
});
|
|
@@ -525,6 +528,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
525
528
|
silentSeconds: 600,
|
|
526
529
|
runningSeconds: 900,
|
|
527
530
|
waitingOnExternalProcess: false,
|
|
531
|
+
finishedResultUnconsumed: false,
|
|
528
532
|
},
|
|
529
533
|
]);
|
|
530
534
|
});
|
|
@@ -555,6 +559,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
555
559
|
silentSeconds: fourDaysSeconds,
|
|
556
560
|
runningSeconds: fourDaysSeconds,
|
|
557
561
|
waitingOnExternalProcess: false,
|
|
562
|
+
finishedResultUnconsumed: false,
|
|
558
563
|
},
|
|
559
564
|
]);
|
|
560
565
|
});
|
|
@@ -592,6 +597,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
592
597
|
silentSeconds: 60,
|
|
593
598
|
runningSeconds: 600,
|
|
594
599
|
waitingOnExternalProcess: false,
|
|
600
|
+
finishedResultUnconsumed: false,
|
|
595
601
|
},
|
|
596
602
|
]);
|
|
597
603
|
fs.rmSync(targetDir, { force: true, recursive: true });
|
|
@@ -680,6 +686,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
680
686
|
silentSeconds: 0,
|
|
681
687
|
runningSeconds: 0,
|
|
682
688
|
waitingOnExternalProcess: false,
|
|
689
|
+
finishedResultUnconsumed: false,
|
|
683
690
|
},
|
|
684
691
|
]);
|
|
685
692
|
});
|
|
@@ -731,6 +738,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
731
738
|
silentSeconds: 120,
|
|
732
739
|
runningSeconds: 900,
|
|
733
740
|
waitingOnExternalProcess: false,
|
|
741
|
+
finishedResultUnconsumed: false,
|
|
734
742
|
},
|
|
735
743
|
]);
|
|
736
744
|
});
|
|
@@ -767,6 +775,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
767
775
|
silentSeconds: twoDaysSeconds,
|
|
768
776
|
runningSeconds: twoDaysSeconds,
|
|
769
777
|
waitingOnExternalProcess: false,
|
|
778
|
+
finishedResultUnconsumed: false,
|
|
770
779
|
},
|
|
771
780
|
]);
|
|
772
781
|
});
|
|
@@ -821,6 +830,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
821
830
|
silentSeconds: 600,
|
|
822
831
|
runningSeconds: 900,
|
|
823
832
|
waitingOnExternalProcess: true,
|
|
833
|
+
finishedResultUnconsumed: false,
|
|
824
834
|
},
|
|
825
835
|
]);
|
|
826
836
|
},
|
|
@@ -956,6 +966,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
956
966
|
silentSeconds: 600,
|
|
957
967
|
runningSeconds: 900,
|
|
958
968
|
waitingOnExternalProcess: true,
|
|
969
|
+
finishedResultUnconsumed: false,
|
|
959
970
|
},
|
|
960
971
|
]);
|
|
961
972
|
});
|
|
@@ -987,6 +998,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
987
998
|
silentSeconds: 600,
|
|
988
999
|
runningSeconds: 900,
|
|
989
1000
|
waitingOnExternalProcess: false,
|
|
1001
|
+
finishedResultUnconsumed: false,
|
|
990
1002
|
},
|
|
991
1003
|
]);
|
|
992
1004
|
});
|
|
@@ -1282,6 +1294,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
1282
1294
|
silentSeconds: 120,
|
|
1283
1295
|
runningSeconds: 900,
|
|
1284
1296
|
waitingOnExternalProcess: false,
|
|
1297
|
+
finishedResultUnconsumed: false,
|
|
1285
1298
|
},
|
|
1286
1299
|
]);
|
|
1287
1300
|
});
|
|
@@ -1337,6 +1350,7 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
1337
1350
|
silentSeconds: 600,
|
|
1338
1351
|
runningSeconds: 900,
|
|
1339
1352
|
waitingOnExternalProcess: false,
|
|
1353
|
+
finishedResultUnconsumed: false,
|
|
1340
1354
|
},
|
|
1341
1355
|
]);
|
|
1342
1356
|
});
|
|
@@ -1367,6 +1381,151 @@ describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
|
1367
1381
|
silentSeconds: 120,
|
|
1368
1382
|
runningSeconds: 900,
|
|
1369
1383
|
waitingOnExternalProcess: false,
|
|
1384
|
+
finishedResultUnconsumed: false,
|
|
1385
|
+
},
|
|
1386
|
+
]);
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
it('reports a finished sub-agent still listed in the running-subagents record as an unconsumed result carrying its silence age', async () => {
|
|
1390
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
1391
|
+
const unconsumedAgentId = 'aaabbbbcccc30001';
|
|
1392
|
+
writeAgentTranscript(
|
|
1393
|
+
sessionName,
|
|
1394
|
+
unconsumedAgentId,
|
|
1395
|
+
finishedEntries('2026-06-27T10:30:00.000Z'),
|
|
1396
|
+
nowEpochSeconds - 5220,
|
|
1397
|
+
);
|
|
1398
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
1399
|
+
createResolver(),
|
|
1400
|
+
emptyProcessLister(),
|
|
1401
|
+
now,
|
|
1402
|
+
livenessResolverWith([unconsumedAgentId]),
|
|
1403
|
+
);
|
|
1404
|
+
|
|
1405
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
1406
|
+
[sessionName],
|
|
1407
|
+
transcriptMapFor([sessionName]),
|
|
1408
|
+
);
|
|
1409
|
+
|
|
1410
|
+
expect(result.get(sessionName)).toEqual([
|
|
1411
|
+
{
|
|
1412
|
+
label: `agent-${unconsumedAgentId}`,
|
|
1413
|
+
silentSeconds: 5220,
|
|
1414
|
+
runningSeconds: 5400,
|
|
1415
|
+
waitingOnExternalProcess: false,
|
|
1416
|
+
finishedResultUnconsumed: true,
|
|
1417
|
+
},
|
|
1418
|
+
]);
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
it('reports a finished sub-agent whose tail is a final text answer as an unconsumed result while it stays in the running-subagents record', async () => {
|
|
1422
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
1423
|
+
const unconsumedAgentId = 'aaabbbbcccc30002';
|
|
1424
|
+
writeAgentTranscript(
|
|
1425
|
+
sessionName,
|
|
1426
|
+
unconsumedAgentId,
|
|
1427
|
+
finalTextAnswerEntries('2026-06-27T11:00:00.000Z'),
|
|
1428
|
+
nowEpochSeconds - 360,
|
|
1429
|
+
);
|
|
1430
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
1431
|
+
createResolver(),
|
|
1432
|
+
emptyProcessLister(),
|
|
1433
|
+
now,
|
|
1434
|
+
livenessResolverWith([unconsumedAgentId]),
|
|
1435
|
+
);
|
|
1436
|
+
|
|
1437
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
1438
|
+
[sessionName],
|
|
1439
|
+
transcriptMapFor([sessionName]),
|
|
1440
|
+
);
|
|
1441
|
+
|
|
1442
|
+
expect(result.get(sessionName)).toEqual([
|
|
1443
|
+
{
|
|
1444
|
+
label: `agent-${unconsumedAgentId}`,
|
|
1445
|
+
silentSeconds: 360,
|
|
1446
|
+
runningSeconds: 3600,
|
|
1447
|
+
waitingOnExternalProcess: false,
|
|
1448
|
+
finishedResultUnconsumed: true,
|
|
1449
|
+
},
|
|
1450
|
+
]);
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
it('discards a finished sub-agent when the running-subagents record cannot be resolved', async () => {
|
|
1454
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
1455
|
+
const finishedAgentId = 'aaabbbbcccc30003';
|
|
1456
|
+
writeAgentTranscript(
|
|
1457
|
+
sessionName,
|
|
1458
|
+
finishedAgentId,
|
|
1459
|
+
finishedEntries('2026-06-27T10:30:00.000Z'),
|
|
1460
|
+
nowEpochSeconds - 5220,
|
|
1461
|
+
);
|
|
1462
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
1463
|
+
createResolver(),
|
|
1464
|
+
emptyProcessLister(),
|
|
1465
|
+
now,
|
|
1466
|
+
livenessResolverWith(null),
|
|
1467
|
+
);
|
|
1468
|
+
|
|
1469
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
1470
|
+
[sessionName],
|
|
1471
|
+
transcriptMapFor([sessionName]),
|
|
1472
|
+
);
|
|
1473
|
+
|
|
1474
|
+
expect(result.size).toBe(0);
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
it('discards a finished sub-agent whose id has already been removed from the running-subagents record', async () => {
|
|
1478
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
1479
|
+
const finishedAgentId = 'aaabbbbcccc30004';
|
|
1480
|
+
writeAgentTranscript(
|
|
1481
|
+
sessionName,
|
|
1482
|
+
finishedAgentId,
|
|
1483
|
+
finishedEntries('2026-06-27T10:30:00.000Z'),
|
|
1484
|
+
nowEpochSeconds - 5220,
|
|
1485
|
+
);
|
|
1486
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
1487
|
+
createResolver(),
|
|
1488
|
+
emptyProcessLister(),
|
|
1489
|
+
now,
|
|
1490
|
+
livenessResolverWith(['aaabbbbcccc30005']),
|
|
1491
|
+
);
|
|
1492
|
+
|
|
1493
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
1494
|
+
[sessionName],
|
|
1495
|
+
transcriptMapFor([sessionName]),
|
|
1496
|
+
);
|
|
1497
|
+
|
|
1498
|
+
expect(result.size).toBe(0);
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
it('reports a still-working sub-agent listed in the running-subagents record as having no unconsumed result', async () => {
|
|
1502
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
1503
|
+
const workingAgentId = 'aaabbbbcccc30006';
|
|
1504
|
+
writeAgentTranscript(
|
|
1505
|
+
sessionName,
|
|
1506
|
+
workingAgentId,
|
|
1507
|
+
pendingToolUseEntries('2026-06-27T11:45:00.000Z'),
|
|
1508
|
+
nowEpochSeconds - 600,
|
|
1509
|
+
);
|
|
1510
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
1511
|
+
createResolver(),
|
|
1512
|
+
emptyProcessLister(),
|
|
1513
|
+
now,
|
|
1514
|
+
livenessResolverWith([workingAgentId]),
|
|
1515
|
+
);
|
|
1516
|
+
|
|
1517
|
+
const result = await repository.listSubAgentActivitiesBySessionName(
|
|
1518
|
+
[sessionName],
|
|
1519
|
+
transcriptMapFor([sessionName]),
|
|
1520
|
+
);
|
|
1521
|
+
|
|
1522
|
+
expect(result.get(sessionName)).toEqual([
|
|
1523
|
+
{
|
|
1524
|
+
label: `agent-${workingAgentId}`,
|
|
1525
|
+
silentSeconds: 600,
|
|
1526
|
+
runningSeconds: 900,
|
|
1527
|
+
waitingOnExternalProcess: false,
|
|
1528
|
+
finishedResultUnconsumed: false,
|
|
1370
1529
|
},
|
|
1371
1530
|
]);
|
|
1372
1531
|
});
|
|
@@ -344,6 +344,7 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
344
344
|
filePath,
|
|
345
345
|
fileName,
|
|
346
346
|
nowEpochSeconds,
|
|
347
|
+
liveSubAgentIds !== null,
|
|
347
348
|
loadNormalizedProcessCommandLines,
|
|
348
349
|
);
|
|
349
350
|
if (activity !== null) {
|
|
@@ -357,6 +358,7 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
357
358
|
filePath: string,
|
|
358
359
|
fileName: string,
|
|
359
360
|
nowEpochSeconds: number,
|
|
361
|
+
listedInRunningSubAgentsRecord: boolean,
|
|
360
362
|
loadNormalizedProcessCommandLines: () => Promise<string[]>,
|
|
361
363
|
): Promise<SubAgentActivity | null> => {
|
|
362
364
|
let content: string;
|
|
@@ -368,7 +370,10 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
368
370
|
return null;
|
|
369
371
|
}
|
|
370
372
|
const transcript = parseTranscript(content);
|
|
371
|
-
if (
|
|
373
|
+
if (
|
|
374
|
+
transcript.lastEntryIndicatesCompletion &&
|
|
375
|
+
!listedInRunningSubAgentsRecord
|
|
376
|
+
) {
|
|
372
377
|
return null;
|
|
373
378
|
}
|
|
374
379
|
const silentSeconds = clampToZero(
|
|
@@ -388,6 +393,7 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
388
393
|
transcript.pendingToolCommands,
|
|
389
394
|
loadNormalizedProcessCommandLines,
|
|
390
395
|
)),
|
|
396
|
+
finishedResultUnconsumed: transcript.lastEntryIndicatesCompletion,
|
|
391
397
|
};
|
|
392
398
|
};
|
|
393
399
|
|
|
@@ -268,6 +268,114 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
268
268
|
});
|
|
269
269
|
});
|
|
270
270
|
|
|
271
|
+
describe('get', () => {
|
|
272
|
+
it('reads the single project item scoped to the given project without consulting the getAllIssues memo', async () => {
|
|
273
|
+
const {
|
|
274
|
+
repository,
|
|
275
|
+
graphqlProjectItemRepository,
|
|
276
|
+
localStorageCacheRepository,
|
|
277
|
+
projectRepository,
|
|
278
|
+
dateRepository,
|
|
279
|
+
} = createApiV3CheerioRestIssueRepository();
|
|
280
|
+
const project = buildTestProject('test-project-id');
|
|
281
|
+
dateRepository.now.mockResolvedValue(new Date('2026-07-07T00:00:00Z'));
|
|
282
|
+
localStorageCacheRepository.getSingle.mockResolvedValue(null);
|
|
283
|
+
projectRepository.getProject.mockResolvedValue(project);
|
|
284
|
+
graphqlProjectItemRepository.fetchProjectItems.mockResolvedValue([]);
|
|
285
|
+
localStorageCacheRepository.setSingle.mockResolvedValue();
|
|
286
|
+
graphqlProjectItemRepository.fetchProjectItemByUrl.mockResolvedValue(
|
|
287
|
+
buildProjectItem('https://github.com/o/r/issues/1', 'live title'),
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
await repository.getAllIssues('test-project-id');
|
|
291
|
+
const issue = await repository.get(
|
|
292
|
+
'https://github.com/o/r/issues/1',
|
|
293
|
+
project,
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
expect(issue?.title).toBe('live title');
|
|
297
|
+
expect(
|
|
298
|
+
graphqlProjectItemRepository.fetchProjectItemByUrl.mock.calls,
|
|
299
|
+
).toEqual([['https://github.com/o/r/issues/1', 'test-project-id']]);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const issueUrlOnTwoProjects = 'https://github.com/o/r/issues/1';
|
|
303
|
+
|
|
304
|
+
const buildProjectScopedItem = (
|
|
305
|
+
itemId: string,
|
|
306
|
+
status: string,
|
|
307
|
+
story: string,
|
|
308
|
+
nextActionHour: string,
|
|
309
|
+
): ProjectItem => ({
|
|
310
|
+
...buildProjectItem(issueUrlOnTwoProjects, itemId),
|
|
311
|
+
id: itemId,
|
|
312
|
+
customFields: [
|
|
313
|
+
{ name: 'Status', value: status },
|
|
314
|
+
{ name: 'story', value: story },
|
|
315
|
+
{ name: 'nextActionHour', value: nextActionHour },
|
|
316
|
+
],
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const itemOnOtherProject = buildProjectScopedItem(
|
|
320
|
+
'item-on-other-project',
|
|
321
|
+
'Awaiting Workspace',
|
|
322
|
+
'other story',
|
|
323
|
+
'9',
|
|
324
|
+
);
|
|
325
|
+
const itemOnRequestedProject = buildProjectScopedItem(
|
|
326
|
+
'item-on-requested-project',
|
|
327
|
+
'Preparation',
|
|
328
|
+
'requested story',
|
|
329
|
+
'17',
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const arrangeItemsOnTwoProjects = (
|
|
333
|
+
graphqlProjectItemRepository: ReturnType<
|
|
334
|
+
typeof createApiV3CheerioRestIssueRepository
|
|
335
|
+
>['graphqlProjectItemRepository'],
|
|
336
|
+
): void => {
|
|
337
|
+
const itemsByProjectId = new Map<string, ProjectItem>([
|
|
338
|
+
['other-project-id', itemOnOtherProject],
|
|
339
|
+
['requested-project-id', itemOnRequestedProject],
|
|
340
|
+
]);
|
|
341
|
+
graphqlProjectItemRepository.fetchProjectItemByUrl.mockImplementation(
|
|
342
|
+
async (_issueUrl: string, projectId?: string) =>
|
|
343
|
+
projectId === undefined
|
|
344
|
+
? Array.from(itemsByProjectId.values())[0]
|
|
345
|
+
: (itemsByProjectId.get(projectId) ?? null),
|
|
346
|
+
);
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
it('returns the project item of the requested project when the issue is on two projects', async () => {
|
|
350
|
+
const { repository, graphqlProjectItemRepository } =
|
|
351
|
+
createApiV3CheerioRestIssueRepository();
|
|
352
|
+
arrangeItemsOnTwoProjects(graphqlProjectItemRepository);
|
|
353
|
+
|
|
354
|
+
const issue = await repository.get(
|
|
355
|
+
issueUrlOnTwoProjects,
|
|
356
|
+
buildTestProject('requested-project-id'),
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
expect(issue?.itemId).toBe('item-on-requested-project');
|
|
360
|
+
expect(issue?.status).toBe('Preparation');
|
|
361
|
+
expect(issue?.story).toBe('requested story');
|
|
362
|
+
expect(issue?.nextActionHour).toBe(17);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('returns null when the issue has project items only on other projects', async () => {
|
|
366
|
+
const { repository, graphqlProjectItemRepository } =
|
|
367
|
+
createApiV3CheerioRestIssueRepository();
|
|
368
|
+
arrangeItemsOnTwoProjects(graphqlProjectItemRepository);
|
|
369
|
+
|
|
370
|
+
const issue = await repository.get(
|
|
371
|
+
issueUrlOnTwoProjects,
|
|
372
|
+
buildTestProject('project-without-any-item'),
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
expect(issue).toBeNull();
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
271
379
|
describe('getAllIssues incremental fetch', () => {
|
|
272
380
|
it('light-scans the lastFetchedAt UTC day with no previous-day overlap, detail-fetches changed items by id, and upserts by url', async () => {
|
|
273
381
|
const {
|
|
@@ -911,8 +911,16 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
911
911
|
searchIssues = (query: string): Promise<SearchedIssue[]> => {
|
|
912
912
|
return this.restIssueRepository.searchIssues(query);
|
|
913
913
|
};
|
|
914
|
-
get = async (
|
|
915
|
-
|
|
914
|
+
get = async (issueUrl: string, project: Project): Promise<Issue | null> => {
|
|
915
|
+
const projectItem =
|
|
916
|
+
await this.graphqlProjectItemRepository.fetchProjectItemByUrl(
|
|
917
|
+
issueUrl,
|
|
918
|
+
project.id,
|
|
919
|
+
);
|
|
920
|
+
if (!projectItem) {
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
return this.convertProjectItemToIssue(projectItem);
|
|
916
924
|
};
|
|
917
925
|
update = async (issue: Issue, _project: Project): Promise<void> => {
|
|
918
926
|
await this.updateIssue(issue);
|
|
@@ -2,6 +2,7 @@ import { DailySecurityScanUseCase } from './DailySecurityScanUseCase';
|
|
|
2
2
|
import { LocalCommandRunner } from './adapter-interfaces/LocalCommandRunner';
|
|
3
3
|
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
4
4
|
import { HttpRepository } from './adapter-interfaces/HttpRepository';
|
|
5
|
+
import { KevReportWatermarkRepository } from './adapter-interfaces/KevReportWatermarkRepository';
|
|
5
6
|
import { mock } from 'jest-mock-extended';
|
|
6
7
|
|
|
7
8
|
const KEV_CATALOG_URL =
|
|
@@ -12,16 +13,21 @@ describe('DailySecurityScanUseCase', () => {
|
|
|
12
13
|
const mockLocalCommandRunner = mock<LocalCommandRunner>();
|
|
13
14
|
const mockIssueRepository = mock<IssueRepository>();
|
|
14
15
|
const mockHttpRepository = mock<HttpRepository>();
|
|
16
|
+
const mockKevReportWatermarkRepository =
|
|
17
|
+
mock<KevReportWatermarkRepository>();
|
|
18
|
+
mockKevReportWatermarkRepository.load.mockResolvedValue({ type: 'absent' });
|
|
15
19
|
const useCase = new DailySecurityScanUseCase(
|
|
16
20
|
mockLocalCommandRunner,
|
|
17
21
|
mockIssueRepository,
|
|
18
22
|
mockHttpRepository,
|
|
23
|
+
mockKevReportWatermarkRepository,
|
|
19
24
|
);
|
|
20
25
|
return {
|
|
21
26
|
useCase,
|
|
22
27
|
mockLocalCommandRunner,
|
|
23
28
|
mockIssueRepository,
|
|
24
29
|
mockHttpRepository,
|
|
30
|
+
mockKevReportWatermarkRepository,
|
|
25
31
|
};
|
|
26
32
|
};
|
|
27
33
|
|
|
@@ -1238,5 +1244,209 @@ describe('DailySecurityScanUseCase', () => {
|
|
|
1238
1244
|
);
|
|
1239
1245
|
expect(grepCalls).toHaveLength(0);
|
|
1240
1246
|
});
|
|
1247
|
+
|
|
1248
|
+
const scannedExampleLibrary = osvScanOutput([
|
|
1249
|
+
{
|
|
1250
|
+
name: 'example-library',
|
|
1251
|
+
version: '1.2.3',
|
|
1252
|
+
ecosystem: 'npm',
|
|
1253
|
+
id: 'GHSA-1111-2222-3333',
|
|
1254
|
+
aliases: ['CVE-2024-0001', 'CVE-2024-0002', 'CVE-2024-0003'],
|
|
1255
|
+
summary: 'Example Library Deserialization',
|
|
1256
|
+
},
|
|
1257
|
+
]);
|
|
1258
|
+
|
|
1259
|
+
const kevCatalogOf = (
|
|
1260
|
+
vulnerabilities: { cveID: string; dateAdded: string }[],
|
|
1261
|
+
) => ({
|
|
1262
|
+
vulnerabilities: vulnerabilities.map((vulnerability) => ({
|
|
1263
|
+
cveID: vulnerability.cveID,
|
|
1264
|
+
vendorProject: 'Example',
|
|
1265
|
+
product: 'ExampleLibrary',
|
|
1266
|
+
vulnerabilityName: `${vulnerability.cveID} Deserialization`,
|
|
1267
|
+
dateAdded: vulnerability.dateAdded,
|
|
1268
|
+
})),
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
it('records every addition it considered in the watermark after the report issue is created', async () => {
|
|
1272
|
+
const { useCase, mockIssueRepository, mockKevReportWatermarkRepository } =
|
|
1273
|
+
buildScanEnvironment(
|
|
1274
|
+
scannedExampleLibrary,
|
|
1275
|
+
kevCatalogOf([
|
|
1276
|
+
{ cveID: 'CVE-2024-0001', dateAdded: '2024-01-01' },
|
|
1277
|
+
{ cveID: 'CVE-2024-0002', dateAdded: '2024-01-02' },
|
|
1278
|
+
]),
|
|
1279
|
+
);
|
|
1280
|
+
|
|
1281
|
+
await runWithKevReporting(useCase);
|
|
1282
|
+
|
|
1283
|
+
expect(
|
|
1284
|
+
kevReportCalls(mockIssueRepository.createNewIssue.mock.calls),
|
|
1285
|
+
).toHaveLength(1);
|
|
1286
|
+
expect(mockKevReportWatermarkRepository.save.mock.calls).toEqual([
|
|
1287
|
+
[
|
|
1288
|
+
{
|
|
1289
|
+
lastReportedDateAdded: '2024-01-02',
|
|
1290
|
+
reportedCveIdsOnLastReportedDateAdded: ['CVE-2024-0002'],
|
|
1291
|
+
},
|
|
1292
|
+
],
|
|
1293
|
+
]);
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
it('does not report an addition that the stored watermark already covers', async () => {
|
|
1297
|
+
const { useCase, mockIssueRepository, mockKevReportWatermarkRepository } =
|
|
1298
|
+
buildScanEnvironment(
|
|
1299
|
+
scannedExampleLibrary,
|
|
1300
|
+
kevCatalogOf([{ cveID: 'CVE-2024-0001', dateAdded: '2024-01-02' }]),
|
|
1301
|
+
);
|
|
1302
|
+
mockKevReportWatermarkRepository.load.mockResolvedValue({
|
|
1303
|
+
type: 'stored',
|
|
1304
|
+
watermark: {
|
|
1305
|
+
lastReportedDateAdded: '2024-01-02',
|
|
1306
|
+
reportedCveIdsOnLastReportedDateAdded: ['CVE-2024-0001'],
|
|
1307
|
+
},
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
await runWithKevReporting(useCase);
|
|
1311
|
+
|
|
1312
|
+
expect(
|
|
1313
|
+
kevReportCalls(mockIssueRepository.createNewIssue.mock.calls),
|
|
1314
|
+
).toHaveLength(0);
|
|
1315
|
+
expect(mockKevReportWatermarkRepository.save.mock.calls).toHaveLength(0);
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
it('reports an addition sharing the stored watermark date that was not reported before', async () => {
|
|
1319
|
+
const { useCase, mockIssueRepository, mockKevReportWatermarkRepository } =
|
|
1320
|
+
buildScanEnvironment(
|
|
1321
|
+
scannedExampleLibrary,
|
|
1322
|
+
kevCatalogOf([
|
|
1323
|
+
{ cveID: 'CVE-2024-0001', dateAdded: '2024-01-02' },
|
|
1324
|
+
{ cveID: 'CVE-2024-0002', dateAdded: '2024-01-02' },
|
|
1325
|
+
]),
|
|
1326
|
+
);
|
|
1327
|
+
mockKevReportWatermarkRepository.load.mockResolvedValue({
|
|
1328
|
+
type: 'stored',
|
|
1329
|
+
watermark: {
|
|
1330
|
+
lastReportedDateAdded: '2024-01-02',
|
|
1331
|
+
reportedCveIdsOnLastReportedDateAdded: ['CVE-2024-0001'],
|
|
1332
|
+
},
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
await runWithKevReporting(useCase);
|
|
1336
|
+
|
|
1337
|
+
const kevCalls = kevReportCalls(
|
|
1338
|
+
mockIssueRepository.createNewIssue.mock.calls,
|
|
1339
|
+
);
|
|
1340
|
+
expect(kevCalls).toHaveLength(1);
|
|
1341
|
+
expect(kevCalls[0][3]).toContain('CVE-2024-0002');
|
|
1342
|
+
expect(kevCalls[0][3]).not.toContain('CVE-2024-0001');
|
|
1343
|
+
expect(mockKevReportWatermarkRepository.save.mock.calls).toEqual([
|
|
1344
|
+
[
|
|
1345
|
+
{
|
|
1346
|
+
lastReportedDateAdded: '2024-01-02',
|
|
1347
|
+
reportedCveIdsOnLastReportedDateAdded: [
|
|
1348
|
+
'CVE-2024-0001',
|
|
1349
|
+
'CVE-2024-0002',
|
|
1350
|
+
],
|
|
1351
|
+
},
|
|
1352
|
+
],
|
|
1353
|
+
]);
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
it('does not select an addition dated strictly before the stored watermark date', async () => {
|
|
1357
|
+
const { useCase, mockIssueRepository, mockKevReportWatermarkRepository } =
|
|
1358
|
+
buildScanEnvironment(
|
|
1359
|
+
scannedExampleLibrary,
|
|
1360
|
+
kevCatalogOf([{ cveID: 'CVE-2024-0001', dateAdded: '2024-01-01' }]),
|
|
1361
|
+
);
|
|
1362
|
+
mockKevReportWatermarkRepository.load.mockResolvedValue({
|
|
1363
|
+
type: 'stored',
|
|
1364
|
+
watermark: {
|
|
1365
|
+
lastReportedDateAdded: '2024-01-02',
|
|
1366
|
+
reportedCveIdsOnLastReportedDateAdded: [],
|
|
1367
|
+
},
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
await runWithKevReporting(useCase);
|
|
1371
|
+
|
|
1372
|
+
expect(
|
|
1373
|
+
kevReportCalls(mockIssueRepository.createNewIssue.mock.calls),
|
|
1374
|
+
).toHaveLength(0);
|
|
1375
|
+
expect(mockKevReportWatermarkRepository.save.mock.calls).toHaveLength(0);
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
it('advances the watermark over an addition that affects no scanned package so it is not weighed again', async () => {
|
|
1379
|
+
const { useCase, mockIssueRepository, mockKevReportWatermarkRepository } =
|
|
1380
|
+
buildScanEnvironment(
|
|
1381
|
+
scannedExampleLibrary,
|
|
1382
|
+
kevCatalogOf([{ cveID: 'CVE-2024-9999', dateAdded: '2024-01-02' }]),
|
|
1383
|
+
);
|
|
1384
|
+
|
|
1385
|
+
await runWithKevReporting(useCase);
|
|
1386
|
+
|
|
1387
|
+
expect(
|
|
1388
|
+
kevReportCalls(mockIssueRepository.createNewIssue.mock.calls),
|
|
1389
|
+
).toHaveLength(0);
|
|
1390
|
+
expect(mockKevReportWatermarkRepository.save.mock.calls).toEqual([
|
|
1391
|
+
[
|
|
1392
|
+
{
|
|
1393
|
+
lastReportedDateAdded: '2024-01-02',
|
|
1394
|
+
reportedCveIdsOnLastReportedDateAdded: ['CVE-2024-9999'],
|
|
1395
|
+
},
|
|
1396
|
+
],
|
|
1397
|
+
]);
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
it('skips the report and leaves the watermark untouched when the stored watermark is unreadable', async () => {
|
|
1401
|
+
const { useCase, mockIssueRepository, mockKevReportWatermarkRepository } =
|
|
1402
|
+
buildScanEnvironment(
|
|
1403
|
+
scannedExampleLibrary,
|
|
1404
|
+
kevCatalogOf([{ cveID: 'CVE-2024-0001', dateAdded: '2024-01-02' }]),
|
|
1405
|
+
);
|
|
1406
|
+
mockKevReportWatermarkRepository.load.mockResolvedValue({
|
|
1407
|
+
type: 'unreadable',
|
|
1408
|
+
reason: 'the stored watermark file holds malformed JSON',
|
|
1409
|
+
});
|
|
1410
|
+
const errorSpy = jest
|
|
1411
|
+
.spyOn(console, 'error')
|
|
1412
|
+
.mockImplementation(() => {});
|
|
1413
|
+
|
|
1414
|
+
await runWithKevReporting(useCase);
|
|
1415
|
+
|
|
1416
|
+
expect(
|
|
1417
|
+
kevReportCalls(mockIssueRepository.createNewIssue.mock.calls),
|
|
1418
|
+
).toHaveLength(0);
|
|
1419
|
+
expect(mockKevReportWatermarkRepository.save.mock.calls).toHaveLength(0);
|
|
1420
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
1421
|
+
expect.stringContaining(
|
|
1422
|
+
'the stored watermark file holds malformed JSON',
|
|
1423
|
+
),
|
|
1424
|
+
);
|
|
1425
|
+
errorSpy.mockRestore();
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
it('creates the report issue and logs when the watermark write fails, so the same additions are weighed again', async () => {
|
|
1429
|
+
const { useCase, mockIssueRepository, mockKevReportWatermarkRepository } =
|
|
1430
|
+
buildScanEnvironment(
|
|
1431
|
+
scannedExampleLibrary,
|
|
1432
|
+
kevCatalogOf([{ cveID: 'CVE-2024-0001', dateAdded: '2024-01-02' }]),
|
|
1433
|
+
);
|
|
1434
|
+
mockKevReportWatermarkRepository.save.mockRejectedValue(
|
|
1435
|
+
new Error('disk is read-only'),
|
|
1436
|
+
);
|
|
1437
|
+
const errorSpy = jest
|
|
1438
|
+
.spyOn(console, 'error')
|
|
1439
|
+
.mockImplementation(() => {});
|
|
1440
|
+
|
|
1441
|
+
await runWithKevReporting(useCase);
|
|
1442
|
+
|
|
1443
|
+
expect(
|
|
1444
|
+
kevReportCalls(mockIssueRepository.createNewIssue.mock.calls),
|
|
1445
|
+
).toHaveLength(1);
|
|
1446
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
1447
|
+
expect.stringContaining('disk is read-only'),
|
|
1448
|
+
);
|
|
1449
|
+
errorSpy.mockRestore();
|
|
1450
|
+
});
|
|
1241
1451
|
});
|
|
1242
1452
|
});
|