github-issue-tower-defence-management 1.122.3 → 1.122.5
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/CHANGELOG.md +15 -0
- package/README.md +8 -4
- package/bin/adapter/entry-points/console/consoleDoneStore.js +19 -5
- package/bin/adapter/entry-points/console/consoleDoneStore.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +14 -2
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/consoleListsWriter.js +2 -0
- package/bin/adapter/entry-points/handlers/consoleListsWriter.js.map +1 -1
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +4 -2
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +12 -7
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -1
- package/bin/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.js +60 -11
- package/bin/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.js.map +1 -1
- package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js +2 -2
- package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js.map +1 -1
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js +1 -0
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js +14 -11
- package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js.map +1 -1
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +64 -7
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +18 -7
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -1
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +64 -19
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/console/consoleDoneStore.test.ts +38 -0
- package/src/adapter/entry-points/console/consoleDoneStore.ts +29 -4
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +11 -0
- package/src/adapter/entry-points/handlers/consoleListsWriter.test.ts +72 -0
- package/src/adapter/entry-points/handlers/consoleListsWriter.ts +3 -0
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +46 -7
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +6 -0
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +102 -37
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +22 -17
- package/src/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.test.ts +1 -1
- package/src/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.test.ts +223 -0
- package/src/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.ts +94 -10
- package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.test.ts +7 -6
- package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.ts +3 -3
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.test.ts +12 -2
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.ts +1 -0
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.test.ts +99 -49
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts +24 -14
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +196 -4
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +91 -7
- package/src/domain/entities/LiveSessionActivitySnapshot.ts +2 -1
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +187 -51
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +32 -15
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +356 -28
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +94 -26
- package/src/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.ts +2 -2
- package/src/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.ts +8 -0
- package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +7 -6
- package/types/adapter/entry-points/console/consoleDoneStore.d.ts +2 -0
- package/types/adapter/entry-points/console/consoleDoneStore.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/consoleListsWriter.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts +2 -0
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts +4 -3
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.d.ts +13 -2
- package/types/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.d.ts.map +1 -1
- package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts +1 -1
- package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts.map +1 -1
- package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts +2 -2
- package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts.map +1 -1
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts +5 -1
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +2 -1
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -1
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +3 -3
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +4 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts +1 -1
- package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.d.ts +8 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts +5 -4
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts.map +1 -1
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
parseHubTaskIssueUrlFromSessionName,
|
|
5
5
|
isGitHubIssueOrPullRequestSessionName,
|
|
6
6
|
DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
7
|
+
DEFAULT_UNANSWERED_OWNER_CALL_GRACE_SECONDS,
|
|
7
8
|
DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
8
9
|
DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
9
10
|
DEFAULT_NOTIFICATION_STAGGER_SECONDS,
|
|
@@ -28,6 +29,8 @@ import { InteractiveLiveSession } from '../entities/InteractiveLiveSession';
|
|
|
28
29
|
type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
|
|
29
30
|
|
|
30
31
|
const MAIN_STALLED_SECTION = 'MAIN_STALLED_SECTION';
|
|
32
|
+
const MAIN_STALLED_STALE_OWNER_CALL_SECTION =
|
|
33
|
+
'MAIN_STALLED_STALE_OWNER_CALL_SECTION';
|
|
31
34
|
const SUBAGENT_SECTION = 'SUBAGENT_SECTION';
|
|
32
35
|
|
|
33
36
|
class EveryNameRecentSet extends Set<string> {
|
|
@@ -66,6 +69,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
66
69
|
}>,
|
|
67
70
|
): {
|
|
68
71
|
mainSilentThresholdSeconds: number;
|
|
72
|
+
unansweredOwnerCallGraceSeconds: number;
|
|
69
73
|
subAgentSilentThresholdSeconds: number;
|
|
70
74
|
subAgentRunningThresholdSeconds: number;
|
|
71
75
|
staggerSeconds: number;
|
|
@@ -75,6 +79,8 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
75
79
|
now: Date;
|
|
76
80
|
} => ({
|
|
77
81
|
mainSilentThresholdSeconds: DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
82
|
+
unansweredOwnerCallGraceSeconds:
|
|
83
|
+
DEFAULT_UNANSWERED_OWNER_CALL_GRACE_SECONDS,
|
|
78
84
|
subAgentSilentThresholdSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
79
85
|
subAgentRunningThresholdSeconds: DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
80
86
|
staggerSeconds: DEFAULT_NOTIFICATION_STAGGER_SECONDS,
|
|
@@ -147,9 +153,9 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
147
153
|
.mockResolvedValue(new Map<string, SubAgentActivity[]>()),
|
|
148
154
|
};
|
|
149
155
|
mockOwnerCallStatusProvider = {
|
|
150
|
-
|
|
156
|
+
listUnansweredOwnerCallEpochSecondsBySessionName: jest
|
|
151
157
|
.fn()
|
|
152
|
-
.mockResolvedValue(new
|
|
158
|
+
.mockResolvedValue(new Map<string, number>()),
|
|
153
159
|
};
|
|
154
160
|
mockNotificationRepository = {
|
|
155
161
|
sendSelfCheckNotification: jest.fn().mockResolvedValue(undefined),
|
|
@@ -159,11 +165,20 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
159
165
|
.fn()
|
|
160
166
|
.mockResolvedValue(everyNameRecentSet()),
|
|
161
167
|
saveCandidateSessionNames: jest.fn().mockResolvedValue(undefined),
|
|
168
|
+
loadAnnouncedRunningSubAgentLabels: jest
|
|
169
|
+
.fn()
|
|
170
|
+
.mockResolvedValue(new Set<string>()),
|
|
171
|
+
saveAnnouncedRunningSubAgentLabels: jest
|
|
172
|
+
.fn()
|
|
173
|
+
.mockResolvedValue(undefined),
|
|
162
174
|
};
|
|
163
175
|
mockMessageComposer = {
|
|
164
176
|
composeMainStalledSection: jest
|
|
165
177
|
.fn()
|
|
166
178
|
.mockReturnValue(MAIN_STALLED_SECTION),
|
|
179
|
+
composeMainStalledWithStaleOwnerCallSection: jest
|
|
180
|
+
.fn()
|
|
181
|
+
.mockReturnValue(MAIN_STALLED_STALE_OWNER_CALL_SECTION),
|
|
167
182
|
composeSubAgentSection: jest.fn().mockReturnValue(SUBAGENT_SECTION),
|
|
168
183
|
};
|
|
169
184
|
mockSleeper = {
|
|
@@ -243,6 +258,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
243
258
|
|
|
244
259
|
it('exposes the default thresholds as named constants', () => {
|
|
245
260
|
expect(DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS).toBe(600);
|
|
261
|
+
expect(DEFAULT_UNANSWERED_OWNER_CALL_GRACE_SECONDS).toBe(3600);
|
|
246
262
|
expect(DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS).toBe(300);
|
|
247
263
|
expect(DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS).toBe(900);
|
|
248
264
|
expect(DEFAULT_NOTIFICATION_STAGGER_SECONDS).toBe(25);
|
|
@@ -365,36 +381,119 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
365
381
|
mockSessionOutputActivityRepository.listSessionOutputActivities,
|
|
366
382
|
).toHaveBeenCalledWith(expectedMap);
|
|
367
383
|
expect(
|
|
368
|
-
mockOwnerCallStatusProvider.
|
|
384
|
+
mockOwnerCallStatusProvider.listUnansweredOwnerCallEpochSecondsBySessionName,
|
|
369
385
|
).toHaveBeenCalledWith(expectedMap);
|
|
370
386
|
expect(
|
|
371
387
|
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName,
|
|
372
388
|
).toHaveBeenCalledWith([GITHUB_SESSION], expectedMap);
|
|
373
389
|
});
|
|
374
390
|
|
|
375
|
-
it('suppresses the stalled section and sends nothing
|
|
391
|
+
it('suppresses the stalled section and sends nothing while the unanswered owner call is younger than the grace period', async () => {
|
|
392
|
+
setupSilentMainSession(GITHUB_SESSION);
|
|
393
|
+
mockOwnerCallStatusProvider.listUnansweredOwnerCallEpochSecondsBySessionName.mockResolvedValue(
|
|
394
|
+
new Map([
|
|
395
|
+
[
|
|
396
|
+
GITHUB_SESSION,
|
|
397
|
+
nowEpochSeconds - DEFAULT_UNANSWERED_OWNER_CALL_GRACE_SECONDS + 1,
|
|
398
|
+
],
|
|
399
|
+
]),
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
await useCase.run(runParams());
|
|
403
|
+
|
|
404
|
+
expect(
|
|
405
|
+
mockMessageComposer.composeMainStalledSection,
|
|
406
|
+
).not.toHaveBeenCalled();
|
|
407
|
+
expect(
|
|
408
|
+
mockMessageComposer.composeMainStalledWithStaleOwnerCallSection,
|
|
409
|
+
).not.toHaveBeenCalled();
|
|
410
|
+
expect(
|
|
411
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
412
|
+
).not.toHaveBeenCalled();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('sends the stale-owner-call stalled section once the unanswered owner call reaches the grace period', async () => {
|
|
416
|
+
setupSilentMainSession(GITHUB_SESSION);
|
|
417
|
+
mockOwnerCallStatusProvider.listUnansweredOwnerCallEpochSecondsBySessionName.mockResolvedValue(
|
|
418
|
+
new Map([
|
|
419
|
+
[
|
|
420
|
+
GITHUB_SESSION,
|
|
421
|
+
nowEpochSeconds - DEFAULT_UNANSWERED_OWNER_CALL_GRACE_SECONDS,
|
|
422
|
+
],
|
|
423
|
+
]),
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
await useCase.run(runParams());
|
|
427
|
+
|
|
428
|
+
expect(
|
|
429
|
+
mockMessageComposer.composeMainStalledWithStaleOwnerCallSection,
|
|
430
|
+
).toHaveBeenCalledWith(
|
|
431
|
+
DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
432
|
+
DEFAULT_UNANSWERED_OWNER_CALL_GRACE_SECONDS,
|
|
433
|
+
);
|
|
434
|
+
expect(
|
|
435
|
+
mockMessageComposer.composeMainStalledSection,
|
|
436
|
+
).not.toHaveBeenCalled();
|
|
437
|
+
expect(
|
|
438
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
439
|
+
).toHaveBeenCalledWith(
|
|
440
|
+
GITHUB_SESSION,
|
|
441
|
+
MAIN_STALLED_STALE_OWNER_CALL_SECTION,
|
|
442
|
+
);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('does not send the stale-owner-call section when the owner call is past the grace period but the main output is not silent past the threshold', async () => {
|
|
376
446
|
setupLiveInteractiveSession(GITHUB_SESSION);
|
|
377
447
|
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
378
448
|
[
|
|
379
449
|
{
|
|
380
450
|
sessionName: GITHUB_SESSION,
|
|
381
451
|
lastOutputEpochSeconds:
|
|
382
|
-
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
452
|
+
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS + 1,
|
|
383
453
|
},
|
|
384
454
|
],
|
|
385
455
|
);
|
|
386
|
-
mockOwnerCallStatusProvider.
|
|
387
|
-
new
|
|
456
|
+
mockOwnerCallStatusProvider.listUnansweredOwnerCallEpochSecondsBySessionName.mockResolvedValue(
|
|
457
|
+
new Map([
|
|
458
|
+
[
|
|
459
|
+
GITHUB_SESSION,
|
|
460
|
+
nowEpochSeconds - DEFAULT_UNANSWERED_OWNER_CALL_GRACE_SECONDS,
|
|
461
|
+
],
|
|
462
|
+
]),
|
|
388
463
|
);
|
|
389
464
|
|
|
390
465
|
await useCase.run(runParams());
|
|
391
466
|
|
|
392
467
|
expect(
|
|
393
|
-
|
|
468
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
394
469
|
).not.toHaveBeenCalled();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('defers a first-cycle stale-owner-call candidate until it persists into the next cycle', async () => {
|
|
473
|
+
setupSilentMainSession(GITHUB_SESSION);
|
|
474
|
+
mockOwnerCallStatusProvider.listUnansweredOwnerCallEpochSecondsBySessionName.mockResolvedValue(
|
|
475
|
+
new Map([
|
|
476
|
+
[
|
|
477
|
+
GITHUB_SESSION,
|
|
478
|
+
nowEpochSeconds - DEFAULT_UNANSWERED_OWNER_CALL_GRACE_SECONDS,
|
|
479
|
+
],
|
|
480
|
+
]),
|
|
481
|
+
);
|
|
482
|
+
mockCandidateStateRepository.loadRecentCandidateSessionNames.mockResolvedValue(
|
|
483
|
+
new Set<string>(),
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
await useCase.run(runParams());
|
|
487
|
+
|
|
395
488
|
expect(
|
|
396
489
|
mockNotificationRepository.sendSelfCheckNotification,
|
|
397
490
|
).not.toHaveBeenCalled();
|
|
491
|
+
expect(
|
|
492
|
+
mockCandidateStateRepository.saveCandidateSessionNames,
|
|
493
|
+
).toHaveBeenCalledWith({
|
|
494
|
+
sessionNames: [GITHUB_SESSION],
|
|
495
|
+
now,
|
|
496
|
+
});
|
|
398
497
|
});
|
|
399
498
|
|
|
400
499
|
it('sends the main stalled section when the session is silent past the threshold and not waiting on the owner', async () => {
|
|
@@ -408,8 +507,8 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
408
507
|
},
|
|
409
508
|
],
|
|
410
509
|
);
|
|
411
|
-
mockOwnerCallStatusProvider.
|
|
412
|
-
new
|
|
510
|
+
mockOwnerCallStatusProvider.listUnansweredOwnerCallEpochSecondsBySessionName.mockResolvedValue(
|
|
511
|
+
new Map<string, number>(),
|
|
413
512
|
);
|
|
414
513
|
|
|
415
514
|
await useCase.run(runParams());
|
|
@@ -417,6 +516,9 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
417
516
|
expect(mockMessageComposer.composeMainStalledSection).toHaveBeenCalledWith(
|
|
418
517
|
DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
419
518
|
);
|
|
519
|
+
expect(
|
|
520
|
+
mockMessageComposer.composeMainStalledWithStaleOwnerCallSection,
|
|
521
|
+
).not.toHaveBeenCalled();
|
|
420
522
|
expect(
|
|
421
523
|
mockNotificationRepository.sendSelfCheckNotification,
|
|
422
524
|
).toHaveBeenCalledWith(GITHUB_SESSION, MAIN_STALLED_SECTION);
|
|
@@ -448,6 +550,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
448
550
|
label: 'sub-process-1',
|
|
449
551
|
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
450
552
|
runningSeconds: 60,
|
|
553
|
+
waitingOnExternalProcess: false,
|
|
451
554
|
},
|
|
452
555
|
];
|
|
453
556
|
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
|
|
@@ -456,27 +559,23 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
456
559
|
|
|
457
560
|
await useCase.run(runParams());
|
|
458
561
|
|
|
459
|
-
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith(
|
|
460
|
-
subAgents,
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
464
|
-
subAgentRunningThresholdSeconds:
|
|
465
|
-
DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
466
|
-
},
|
|
467
|
-
);
|
|
562
|
+
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith({
|
|
563
|
+
idleSubAgents: subAgents,
|
|
564
|
+
longRunningSubAgents: [],
|
|
565
|
+
});
|
|
468
566
|
expect(
|
|
469
567
|
mockNotificationRepository.sendSelfCheckNotification,
|
|
470
568
|
).toHaveBeenCalledWith(GITHUB_SESSION, SUBAGENT_SECTION);
|
|
471
569
|
});
|
|
472
570
|
|
|
473
|
-
it('
|
|
571
|
+
it('applies a configured running threshold when selecting long-running sub-agents', async () => {
|
|
474
572
|
setupLiveInteractiveSession(GITHUB_SESSION);
|
|
475
573
|
const subAgents: SubAgentActivity[] = [
|
|
476
574
|
{
|
|
477
575
|
label: 'sub-process-1',
|
|
478
576
|
silentSeconds: 10,
|
|
479
577
|
runningSeconds: 600,
|
|
578
|
+
waitingOnExternalProcess: false,
|
|
480
579
|
},
|
|
481
580
|
];
|
|
482
581
|
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
|
|
@@ -488,14 +587,10 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
488
587
|
subAgentRunningThresholdSeconds: 600,
|
|
489
588
|
});
|
|
490
589
|
|
|
491
|
-
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith(
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
496
|
-
subAgentRunningThresholdSeconds: 600,
|
|
497
|
-
},
|
|
498
|
-
);
|
|
590
|
+
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith({
|
|
591
|
+
idleSubAgents: [],
|
|
592
|
+
longRunningSubAgents: subAgents,
|
|
593
|
+
});
|
|
499
594
|
});
|
|
500
595
|
|
|
501
596
|
it('excludes an owner-handover spawn from selection so no notification is sent', async () => {
|
|
@@ -656,6 +751,239 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
656
751
|
});
|
|
657
752
|
});
|
|
658
753
|
|
|
754
|
+
describe('sub-agent state-based reminder judgment', () => {
|
|
755
|
+
const hungSubAgent = (label: string): SubAgentActivity => ({
|
|
756
|
+
label,
|
|
757
|
+
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
758
|
+
runningSeconds: 60,
|
|
759
|
+
waitingOnExternalProcess: false,
|
|
760
|
+
});
|
|
761
|
+
const waitingSubAgent = (label: string): SubAgentActivity => ({
|
|
762
|
+
label,
|
|
763
|
+
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
764
|
+
runningSeconds: 60,
|
|
765
|
+
waitingOnExternalProcess: true,
|
|
766
|
+
});
|
|
767
|
+
const longRunningSubAgent = (
|
|
768
|
+
label: string,
|
|
769
|
+
waitingOnExternalProcess: boolean,
|
|
770
|
+
): SubAgentActivity => ({
|
|
771
|
+
label,
|
|
772
|
+
silentSeconds: 30,
|
|
773
|
+
runningSeconds: DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
774
|
+
waitingOnExternalProcess,
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
const setupSubAgents = (subAgents: SubAgentActivity[]): void => {
|
|
778
|
+
setupLiveInteractiveSession(GITHUB_SESSION);
|
|
779
|
+
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
|
|
780
|
+
new Map([[GITHUB_SESSION, subAgents]]),
|
|
781
|
+
);
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
it('never selects a waiting sub-agent as a silent-reminder candidate', async () => {
|
|
785
|
+
setupSubAgents([waitingSubAgent('sub-process-1')]);
|
|
786
|
+
|
|
787
|
+
await useCase.run(runParams());
|
|
788
|
+
|
|
789
|
+
expect(mockMessageComposer.composeSubAgentSection).not.toHaveBeenCalled();
|
|
790
|
+
expect(
|
|
791
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
792
|
+
).not.toHaveBeenCalled();
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
it('notifies a hung sub-agent whose in-flight tool has no live process', async () => {
|
|
796
|
+
setupSubAgents([hungSubAgent('sub-process-1')]);
|
|
797
|
+
|
|
798
|
+
await useCase.run(runParams());
|
|
799
|
+
|
|
800
|
+
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith({
|
|
801
|
+
idleSubAgents: [hungSubAgent('sub-process-1')],
|
|
802
|
+
longRunningSubAgents: [],
|
|
803
|
+
});
|
|
804
|
+
expect(
|
|
805
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
806
|
+
).toHaveBeenCalledWith(GITHUB_SESSION, SUBAGENT_SECTION);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it('keeps notifying a hung sub-agent on every cycle while the condition holds', async () => {
|
|
810
|
+
setupSubAgents([hungSubAgent('sub-process-1')]);
|
|
811
|
+
|
|
812
|
+
await useCase.run(runParams());
|
|
813
|
+
await useCase.run(runParams());
|
|
814
|
+
|
|
815
|
+
expect(
|
|
816
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
817
|
+
).toHaveBeenCalledTimes(2);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it('announces a running-threshold crossing for a waiting sub-agent without any idle section', async () => {
|
|
821
|
+
setupSubAgents([
|
|
822
|
+
{
|
|
823
|
+
label: 'sub-process-1',
|
|
824
|
+
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
825
|
+
runningSeconds: DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
826
|
+
waitingOnExternalProcess: true,
|
|
827
|
+
},
|
|
828
|
+
]);
|
|
829
|
+
|
|
830
|
+
await useCase.run(runParams());
|
|
831
|
+
|
|
832
|
+
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith({
|
|
833
|
+
idleSubAgents: [],
|
|
834
|
+
longRunningSubAgents: [
|
|
835
|
+
{
|
|
836
|
+
label: 'sub-process-1',
|
|
837
|
+
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
838
|
+
runningSeconds: DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
839
|
+
waitingOnExternalProcess: true,
|
|
840
|
+
},
|
|
841
|
+
],
|
|
842
|
+
});
|
|
843
|
+
expect(
|
|
844
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
845
|
+
).toHaveBeenCalledWith(GITHUB_SESSION, SUBAGENT_SECTION);
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
it('records the announced running label only after the notification is sent', async () => {
|
|
849
|
+
setupSubAgents([longRunningSubAgent('sub-process-1', false)]);
|
|
850
|
+
|
|
851
|
+
await useCase.run(runParams());
|
|
852
|
+
|
|
853
|
+
expect(
|
|
854
|
+
mockCandidateStateRepository.saveAnnouncedRunningSubAgentLabels,
|
|
855
|
+
).toHaveBeenCalledWith({
|
|
856
|
+
sessionName: GITHUB_SESSION,
|
|
857
|
+
labels: ['sub-process-1'],
|
|
858
|
+
now,
|
|
859
|
+
});
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
it('does not record an announced running label when the first-cycle debounce defers the send', async () => {
|
|
863
|
+
setupSubAgents([longRunningSubAgent('sub-process-1', false)]);
|
|
864
|
+
mockCandidateStateRepository.loadRecentCandidateSessionNames.mockResolvedValue(
|
|
865
|
+
new Set<string>(),
|
|
866
|
+
);
|
|
867
|
+
|
|
868
|
+
await useCase.run(runParams());
|
|
869
|
+
|
|
870
|
+
expect(
|
|
871
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
872
|
+
).not.toHaveBeenCalled();
|
|
873
|
+
expect(
|
|
874
|
+
mockCandidateStateRepository.saveAnnouncedRunningSubAgentLabels,
|
|
875
|
+
).not.toHaveBeenCalled();
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
it('does not repeat the running announcement while the same label stays over the threshold', async () => {
|
|
879
|
+
setupSubAgents([longRunningSubAgent('sub-process-1', false)]);
|
|
880
|
+
mockCandidateStateRepository.loadAnnouncedRunningSubAgentLabels.mockResolvedValue(
|
|
881
|
+
new Set(['sub-process-1']),
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
await useCase.run(runParams());
|
|
885
|
+
|
|
886
|
+
expect(mockMessageComposer.composeSubAgentSection).not.toHaveBeenCalled();
|
|
887
|
+
expect(
|
|
888
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
889
|
+
).not.toHaveBeenCalled();
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
it('announces only the new label when another announced label is still over the threshold', async () => {
|
|
893
|
+
setupSubAgents([
|
|
894
|
+
longRunningSubAgent('sub-process-1', false),
|
|
895
|
+
longRunningSubAgent('sub-process-2', false),
|
|
896
|
+
]);
|
|
897
|
+
mockCandidateStateRepository.loadAnnouncedRunningSubAgentLabels.mockResolvedValue(
|
|
898
|
+
new Set(['sub-process-1']),
|
|
899
|
+
);
|
|
900
|
+
|
|
901
|
+
await useCase.run(runParams());
|
|
902
|
+
|
|
903
|
+
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith({
|
|
904
|
+
idleSubAgents: [],
|
|
905
|
+
longRunningSubAgents: [longRunningSubAgent('sub-process-2', false)],
|
|
906
|
+
});
|
|
907
|
+
expect(
|
|
908
|
+
mockCandidateStateRepository.saveAnnouncedRunningSubAgentLabels,
|
|
909
|
+
).toHaveBeenCalledWith({
|
|
910
|
+
sessionName: GITHUB_SESSION,
|
|
911
|
+
labels: ['sub-process-1', 'sub-process-2'],
|
|
912
|
+
now,
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
it('clears the announced record when the label disappears from the sub-agent set', async () => {
|
|
917
|
+
setupSubAgents([]);
|
|
918
|
+
mockCandidateStateRepository.loadAnnouncedRunningSubAgentLabels.mockResolvedValue(
|
|
919
|
+
new Set(['sub-process-1']),
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
await useCase.run(runParams());
|
|
923
|
+
|
|
924
|
+
expect(
|
|
925
|
+
mockCandidateStateRepository.saveAnnouncedRunningSubAgentLabels,
|
|
926
|
+
).toHaveBeenCalledWith({
|
|
927
|
+
sessionName: GITHUB_SESSION,
|
|
928
|
+
labels: [],
|
|
929
|
+
now,
|
|
930
|
+
});
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
it('announces exactly once per crossing across use-case instances sharing the persisted state', async () => {
|
|
934
|
+
setupSubAgents([longRunningSubAgent('sub-process-1', false)]);
|
|
935
|
+
const announcedLabelsBySessionName = new Map<string, string[]>();
|
|
936
|
+
mockCandidateStateRepository.loadAnnouncedRunningSubAgentLabels.mockImplementation(
|
|
937
|
+
async ({ sessionName }) =>
|
|
938
|
+
new Set(announcedLabelsBySessionName.get(sessionName) ?? []),
|
|
939
|
+
);
|
|
940
|
+
mockCandidateStateRepository.saveAnnouncedRunningSubAgentLabels.mockImplementation(
|
|
941
|
+
async ({ sessionName, labels }) => {
|
|
942
|
+
announcedLabelsBySessionName.set(sessionName, labels);
|
|
943
|
+
},
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
await useCase.run(runParams());
|
|
947
|
+
expect(
|
|
948
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
949
|
+
).toHaveBeenCalledTimes(1);
|
|
950
|
+
|
|
951
|
+
const secondUseCase = new NotifySilentLiveSessionsUseCase(
|
|
952
|
+
mockSnapshotProvider,
|
|
953
|
+
mockTranscriptResolver,
|
|
954
|
+
mockSessionOutputActivityRepository,
|
|
955
|
+
mockSubAgentActivityRepository,
|
|
956
|
+
mockOwnerCallStatusProvider,
|
|
957
|
+
mockNotificationRepository,
|
|
958
|
+
mockCandidateStateRepository,
|
|
959
|
+
mockMessageComposer,
|
|
960
|
+
mockSleeper,
|
|
961
|
+
mockHubTaskStatusResolver,
|
|
962
|
+
mockHubTaskStatusCacheRepository,
|
|
963
|
+
);
|
|
964
|
+
await secondUseCase.run(
|
|
965
|
+
runParams({ now: new Date(now.getTime() + 60 * 1000) }),
|
|
966
|
+
);
|
|
967
|
+
|
|
968
|
+
expect(
|
|
969
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
970
|
+
).toHaveBeenCalledTimes(1);
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
it('does not touch the announced-label record for a main-only reminder', async () => {
|
|
974
|
+
setupSilentMainSession(GITHUB_SESSION);
|
|
975
|
+
|
|
976
|
+
await useCase.run(runParams());
|
|
977
|
+
|
|
978
|
+
expect(
|
|
979
|
+
mockNotificationRepository.sendSelfCheckNotification,
|
|
980
|
+
).toHaveBeenCalledWith(GITHUB_SESSION, MAIN_STALLED_SECTION);
|
|
981
|
+
expect(
|
|
982
|
+
mockCandidateStateRepository.saveAnnouncedRunningSubAgentLabels,
|
|
983
|
+
).not.toHaveBeenCalled();
|
|
984
|
+
});
|
|
985
|
+
});
|
|
986
|
+
|
|
659
987
|
it('sends to multiple sessions sequentially with a stagger delay between sends', async () => {
|
|
660
988
|
mockSnapshotProvider.getSnapshot.mockResolvedValue(
|
|
661
989
|
snapshotWithSessions([GITHUB_SESSION_ALPHA, GITHUB_SESSION_BRAVO]),
|