github-issue-tower-defence-management 1.109.1 → 1.110.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.
Files changed (224) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +29 -3
  3. package/bin/adapter/entry-points/cli/index.js +10 -10
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/console/dashboardComposeService.js +7 -6
  6. package/bin/adapter/entry-points/console/dashboardComposeService.js.map +1 -1
  7. package/bin/adapter/entry-points/console/ui-dist/assets/{index-CO-f_j4f.css → index-BPH6W1yn.css} +1 -1
  8. package/bin/adapter/entry-points/console/ui-dist/assets/index-CT3t4Yje.js +101 -0
  9. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  10. package/bin/adapter/entry-points/console/webServer.js +5 -4
  11. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  12. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +125 -2
  13. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  14. package/bin/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.js +2 -0
  15. package/bin/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.js.map +1 -1
  16. package/bin/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.js +2 -0
  17. package/bin/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.js.map +1 -1
  18. package/bin/adapter/entry-points/handlers/OauthTokenSelectHandler.js +2 -0
  19. package/bin/adapter/entry-points/handlers/OauthTokenSelectHandler.js.map +1 -1
  20. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +62 -0
  21. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -0
  22. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js +88 -3
  23. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js.map +1 -1
  24. package/bin/adapter/proxy/RateLimitCache.js +25 -1
  25. package/bin/adapter/proxy/RateLimitCache.js.map +1 -1
  26. package/bin/adapter/proxy/proxyEntry.js +25 -0
  27. package/bin/adapter/proxy/proxyEntry.js.map +1 -1
  28. package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +37 -0
  29. package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -0
  30. package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js +76 -0
  31. package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js.map +1 -0
  32. package/bin/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.js +67 -0
  33. package/bin/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.js.map +1 -0
  34. package/bin/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.js +50 -0
  35. package/bin/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.js.map +1 -0
  36. package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js +12 -0
  37. package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js.map +1 -0
  38. package/bin/adapter/repositories/NodeSubAgentProcessLister.js +37 -0
  39. package/bin/adapter/repositories/NodeSubAgentProcessLister.js.map +1 -0
  40. package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js +48 -0
  41. package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js.map +1 -0
  42. package/bin/adapter/repositories/RealSleeper.js +17 -0
  43. package/bin/adapter/repositories/RealSleeper.js.map +1 -0
  44. package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js +56 -0
  45. package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js.map +1 -0
  46. package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js +150 -0
  47. package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js.map +1 -0
  48. package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +152 -0
  49. package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -0
  50. package/bin/domain/entities/LiveSessionActivitySnapshot.js +3 -0
  51. package/bin/domain/entities/LiveSessionActivitySnapshot.js.map +1 -0
  52. package/bin/domain/entities/LiveSessionOutputActivity.js +3 -0
  53. package/bin/domain/entities/LiveSessionOutputActivity.js.map +1 -0
  54. package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +35 -0
  55. package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -0
  56. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +113 -0
  57. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -0
  58. package/bin/domain/usecases/OauthTokenSelectUseCase.js +8 -2
  59. package/bin/domain/usecases/OauthTokenSelectUseCase.js.map +1 -1
  60. package/bin/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.js +3 -0
  61. package/bin/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.js.map +1 -0
  62. package/bin/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.js +3 -0
  63. package/bin/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.js.map +1 -0
  64. package/bin/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.js +3 -0
  65. package/bin/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.js.map +1 -0
  66. package/bin/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.js +3 -0
  67. package/bin/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.js.map +1 -0
  68. package/bin/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.js +3 -0
  69. package/bin/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.js.map +1 -0
  70. package/bin/domain/usecases/adapter-interfaces/Sleeper.js +3 -0
  71. package/bin/domain/usecases/adapter-interfaces/Sleeper.js.map +1 -0
  72. package/bin/domain/usecases/adapter-interfaces/SubAgentProcessLister.js +3 -0
  73. package/bin/domain/usecases/adapter-interfaces/SubAgentProcessLister.js.map +1 -0
  74. package/bin/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.js +3 -0
  75. package/bin/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.js.map +1 -0
  76. package/bin/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.js +3 -0
  77. package/bin/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.js.map +1 -0
  78. package/bin/domain/usecases/dashboard/DashboardProjectCode.js +19 -0
  79. package/bin/domain/usecases/dashboard/DashboardProjectCode.js.map +1 -0
  80. package/package.json +1 -1
  81. package/src/adapter/entry-points/cli/index.ts +14 -14
  82. package/src/adapter/entry-points/console/dashboardComposeService.test.ts +31 -28
  83. package/src/adapter/entry-points/console/dashboardComposeService.ts +13 -10
  84. package/src/adapter/entry-points/console/dashboardEmitterComposerKey.test.ts +175 -0
  85. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +1 -1
  86. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCopyUrlButton.stories.tsx +24 -0
  87. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCopyUrlButton.test.tsx +63 -0
  88. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCopyUrlButton.tsx +51 -0
  89. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.test.tsx +7 -0
  90. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +2 -0
  91. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.test.tsx +18 -0
  92. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.tsx +2 -0
  93. package/src/adapter/entry-points/console/ui/src/index.css +14 -0
  94. package/src/adapter/entry-points/console/ui-dist/assets/{index-CO-f_j4f.css → index-BPH6W1yn.css} +1 -1
  95. package/src/adapter/entry-points/console/ui-dist/assets/index-CT3t4Yje.js +101 -0
  96. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  97. package/src/adapter/entry-points/console/webServer.test.ts +23 -23
  98. package/src/adapter/entry-points/console/webServer.ts +5 -4
  99. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +122 -0
  100. package/src/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.ts +2 -0
  101. package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.test.ts +112 -0
  102. package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.ts +2 -0
  103. package/src/adapter/entry-points/handlers/OauthTokenSelectHandler.test.ts +131 -0
  104. package/src/adapter/entry-points/handlers/OauthTokenSelectHandler.ts +2 -0
  105. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +338 -0
  106. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +174 -0
  107. package/src/adapter/entry-points/handlers/tokenStatusWriter.test.ts +79 -0
  108. package/src/adapter/entry-points/handlers/tokenStatusWriter.ts +117 -3
  109. package/src/adapter/proxy/RateLimitCache.test.ts +89 -0
  110. package/src/adapter/proxy/RateLimitCache.ts +31 -0
  111. package/src/adapter/proxy/proxyEntry.test.ts +92 -0
  112. package/src/adapter/proxy/proxyEntry.ts +32 -0
  113. package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +77 -0
  114. package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +51 -0
  115. package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.test.ts +92 -0
  116. package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.ts +48 -0
  117. package/src/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.test.ts +56 -0
  118. package/src/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.ts +32 -0
  119. package/src/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.test.ts +32 -0
  120. package/src/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.ts +17 -0
  121. package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.test.ts +12 -0
  122. package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.ts +9 -0
  123. package/src/adapter/repositories/NodeSubAgentProcessLister.test.ts +43 -0
  124. package/src/adapter/repositories/NodeSubAgentProcessLister.ts +40 -0
  125. package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.test.ts +109 -0
  126. package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.ts +56 -0
  127. package/src/adapter/repositories/RealSleeper.test.ts +19 -0
  128. package/src/adapter/repositories/RealSleeper.ts +12 -0
  129. package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.test.ts +153 -0
  130. package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.ts +83 -0
  131. package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.test.ts +220 -0
  132. package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts +139 -0
  133. package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +217 -0
  134. package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +145 -0
  135. package/src/domain/entities/LiveSessionActivitySnapshot.ts +12 -0
  136. package/src/domain/entities/LiveSessionOutputActivity.ts +4 -0
  137. package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +52 -0
  138. package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +39 -0
  139. package/src/domain/usecases/InTmuxByHumanSessionTokenCountUseCase.test.ts +2 -0
  140. package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.test.ts +40 -0
  141. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +612 -0
  142. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +223 -0
  143. package/src/domain/usecases/OauthTokenSelectUseCase.test.ts +36 -0
  144. package/src/domain/usecases/OauthTokenSelectUseCase.ts +12 -0
  145. package/src/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.ts +5 -0
  146. package/src/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.ts +7 -0
  147. package/src/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.ts +7 -0
  148. package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +6 -0
  149. package/src/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.ts +11 -0
  150. package/src/domain/usecases/adapter-interfaces/Sleeper.ts +3 -0
  151. package/src/domain/usecases/adapter-interfaces/SubAgentProcessLister.ts +8 -0
  152. package/src/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.ts +3 -0
  153. package/src/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.ts +3 -0
  154. package/src/domain/usecases/dashboard/DashboardProjectCode.test.ts +32 -0
  155. package/src/domain/usecases/dashboard/DashboardProjectCode.ts +20 -0
  156. package/types/adapter/entry-points/console/dashboardComposeService.d.ts +1 -1
  157. package/types/adapter/entry-points/console/dashboardComposeService.d.ts.map +1 -1
  158. package/types/adapter/entry-points/console/webServer.d.ts +2 -2
  159. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  160. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  161. package/types/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.d.ts.map +1 -1
  162. package/types/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.d.ts.map +1 -1
  163. package/types/adapter/entry-points/handlers/OauthTokenSelectHandler.d.ts.map +1 -1
  164. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts +34 -0
  165. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -0
  166. package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts +1 -0
  167. package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -1
  168. package/types/adapter/proxy/RateLimitCache.d.ts +3 -0
  169. package/types/adapter/proxy/RateLimitCache.d.ts.map +1 -1
  170. package/types/adapter/proxy/proxyEntry.d.ts.map +1 -1
  171. package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts +15 -0
  172. package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -0
  173. package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts +10 -0
  174. package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts.map +1 -0
  175. package/types/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.d.ts +9 -0
  176. package/types/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.d.ts.map +1 -0
  177. package/types/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.d.ts +7 -0
  178. package/types/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.d.ts.map +1 -0
  179. package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts +5 -0
  180. package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts.map +1 -0
  181. package/types/adapter/repositories/NodeSubAgentProcessLister.d.ts +8 -0
  182. package/types/adapter/repositories/NodeSubAgentProcessLister.d.ts.map +1 -0
  183. package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts +12 -0
  184. package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts.map +1 -0
  185. package/types/adapter/repositories/RealSleeper.d.ts +5 -0
  186. package/types/adapter/repositories/RealSleeper.d.ts.map +1 -0
  187. package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts +13 -0
  188. package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts.map +1 -0
  189. package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts +10 -0
  190. package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts.map +1 -0
  191. package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts +12 -0
  192. package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -0
  193. package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +12 -0
  194. package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -0
  195. package/types/domain/entities/LiveSessionOutputActivity.d.ts +5 -0
  196. package/types/domain/entities/LiveSessionOutputActivity.d.ts.map +1 -0
  197. package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +7 -0
  198. package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -0
  199. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +41 -0
  200. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -0
  201. package/types/domain/usecases/OauthTokenSelectUseCase.d.ts +2 -0
  202. package/types/domain/usecases/OauthTokenSelectUseCase.d.ts.map +1 -1
  203. package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts +4 -0
  204. package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts.map +1 -0
  205. package/types/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.d.ts +5 -0
  206. package/types/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.d.ts.map +1 -0
  207. package/types/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.d.ts +5 -0
  208. package/types/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.d.ts.map +1 -0
  209. package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts +6 -0
  210. package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts.map +1 -0
  211. package/types/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.d.ts +6 -0
  212. package/types/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.d.ts.map +1 -0
  213. package/types/domain/usecases/adapter-interfaces/Sleeper.d.ts +4 -0
  214. package/types/domain/usecases/adapter-interfaces/Sleeper.d.ts.map +1 -0
  215. package/types/domain/usecases/adapter-interfaces/SubAgentProcessLister.d.ts +8 -0
  216. package/types/domain/usecases/adapter-interfaces/SubAgentProcessLister.d.ts.map +1 -0
  217. package/types/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.d.ts +4 -0
  218. package/types/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.d.ts.map +1 -0
  219. package/types/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.d.ts +4 -0
  220. package/types/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.d.ts.map +1 -0
  221. package/types/domain/usecases/dashboard/DashboardProjectCode.d.ts +4 -0
  222. package/types/domain/usecases/dashboard/DashboardProjectCode.d.ts.map +1 -0
  223. package/bin/adapter/entry-points/console/ui-dist/assets/index-BpccigBS.js +0 -101
  224. package/src/adapter/entry-points/console/ui-dist/assets/index-BpccigBS.js +0 -101
@@ -0,0 +1,612 @@
1
+ import {
2
+ NotifySilentLiveSessionsUseCase,
3
+ DEFAULT_MONITORED_STATUS,
4
+ DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
5
+ DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
6
+ DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
7
+ DEFAULT_NOTIFICATION_COOLDOWN_SECONDS,
8
+ DEFAULT_NOTIFICATION_STAGGER_SECONDS,
9
+ } from './NotifySilentLiveSessionsUseCase';
10
+ import { IssueRepository } from './adapter-interfaces/IssueRepository';
11
+ import { TmuxSessionRepository } from './adapter-interfaces/TmuxSessionRepository';
12
+ import { SessionOutputActivityRepository } from './adapter-interfaces/SessionOutputActivityRepository';
13
+ import { SessionSubAgentActivityRepository } from './adapter-interfaces/SessionSubAgentActivityRepository';
14
+ import { OwnerCallStatusProvider } from './adapter-interfaces/OwnerCallStatusProvider';
15
+ import { SilentSessionNotificationRepository } from './adapter-interfaces/SilentSessionNotificationRepository';
16
+ import { SilentSessionMessageComposer } from './adapter-interfaces/SilentSessionMessageComposer';
17
+ import { Sleeper } from './adapter-interfaces/Sleeper';
18
+ import { toTmuxSessionName } from './intmux/InTmuxByHumanSessionReconcileUseCase';
19
+ import { Issue } from '../entities/Issue';
20
+ import { Project } from '../entities/Project';
21
+ import { SubAgentActivity } from '../entities/LiveSessionActivitySnapshot';
22
+ import { IN_TMUX_STATUS_NAME } from '../entities/WorkflowStatus';
23
+
24
+ type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
25
+
26
+ const MAIN_STALLED_SECTION = 'MAIN_STALLED_SECTION';
27
+ const SUBAGENT_SECTION = 'SUBAGENT_SECTION';
28
+
29
+ const createMockProject = (): Project => ({
30
+ id: 'project-1',
31
+ url: 'https://github.com/users/user/projects/1',
32
+ databaseId: 1,
33
+ name: 'Test Project',
34
+ status: {
35
+ name: 'Status',
36
+ fieldId: 'field-1',
37
+ statuses: [],
38
+ },
39
+ nextActionDate: null,
40
+ nextActionHour: null,
41
+ story: null,
42
+ remainingEstimationMinutes: null,
43
+ dependedIssueUrlSeparatedByComma: null,
44
+ completionDate50PercentConfidence: null,
45
+ });
46
+
47
+ let issueCounter = 0;
48
+ const createMockIssue = (overrides: Partial<Issue> = {}): Issue => {
49
+ issueCounter += 1;
50
+ return {
51
+ nameWithOwner: 'user/repo',
52
+ number: issueCounter,
53
+ title: `Test Issue ${issueCounter}`,
54
+ state: 'OPEN',
55
+ status: IN_TMUX_STATUS_NAME,
56
+ story: null,
57
+ nextActionDate: null,
58
+ nextActionHour: null,
59
+ estimationMinutes: null,
60
+ dependedIssueUrls: [],
61
+ completionDate50PercentConfidence: null,
62
+ url: `https://github.com/user/repo/issues/${issueCounter}`,
63
+ assignees: [],
64
+ labels: [],
65
+ org: 'user',
66
+ repo: 'repo',
67
+ body: '',
68
+ itemId: `item-${issueCounter}`,
69
+ isPr: false,
70
+ isInProgress: false,
71
+ isClosed: false,
72
+ createdAt: new Date(),
73
+ author: 'testuser',
74
+ closingIssueReferenceUrls: [],
75
+ ...overrides,
76
+ };
77
+ };
78
+
79
+ describe('NotifySilentLiveSessionsUseCase', () => {
80
+ let useCase: NotifySilentLiveSessionsUseCase;
81
+ let mockIssueRepository: Mocked<Pick<IssueRepository, 'getAllOpened'>>;
82
+ let mockTmuxSessionRepository: Mocked<
83
+ Pick<TmuxSessionRepository, 'listLiveSessionNames'>
84
+ >;
85
+ let mockSessionOutputActivityRepository: Mocked<SessionOutputActivityRepository>;
86
+ let mockSubAgentActivityRepository: Mocked<SessionSubAgentActivityRepository>;
87
+ let mockOwnerCallStatusProvider: Mocked<OwnerCallStatusProvider>;
88
+ let mockNotificationRepository: Mocked<SilentSessionNotificationRepository>;
89
+ let mockMessageComposer: Mocked<SilentSessionMessageComposer>;
90
+ let mockSleeper: Mocked<Sleeper>;
91
+ let mockProject: Project;
92
+ const now = new Date('2026-06-26T00:00:00Z');
93
+ const nowEpochSeconds = Math.floor(now.getTime() / 1000);
94
+ const allowCacheMinutes = 10;
95
+
96
+ const runParams = (): {
97
+ project: Project;
98
+ allowCacheMinutes: number;
99
+ monitoredStatus: string;
100
+ mainSilentThresholdSeconds: number;
101
+ subAgentSilentThresholdSeconds: number;
102
+ subAgentRunningThresholdSeconds: number;
103
+ cooldownSeconds: number;
104
+ staggerSeconds: number;
105
+ now: Date;
106
+ } => ({
107
+ project: mockProject,
108
+ allowCacheMinutes,
109
+ monitoredStatus: DEFAULT_MONITORED_STATUS,
110
+ mainSilentThresholdSeconds: DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
111
+ subAgentSilentThresholdSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
112
+ subAgentRunningThresholdSeconds: DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
113
+ cooldownSeconds: DEFAULT_NOTIFICATION_COOLDOWN_SECONDS,
114
+ staggerSeconds: DEFAULT_NOTIFICATION_STAGGER_SECONDS,
115
+ now,
116
+ });
117
+
118
+ beforeEach(() => {
119
+ jest.resetAllMocks();
120
+ jest.spyOn(console, 'log').mockImplementation(() => undefined);
121
+ mockProject = createMockProject();
122
+ mockIssueRepository = {
123
+ getAllOpened: jest.fn().mockResolvedValue([]),
124
+ };
125
+ mockTmuxSessionRepository = {
126
+ listLiveSessionNames: jest.fn().mockResolvedValue([]),
127
+ };
128
+ mockSessionOutputActivityRepository = {
129
+ listSessionOutputActivities: jest.fn().mockResolvedValue([]),
130
+ };
131
+ mockSubAgentActivityRepository = {
132
+ listSubAgentActivitiesBySessionName: jest
133
+ .fn()
134
+ .mockResolvedValue(new Map<string, SubAgentActivity[]>()),
135
+ };
136
+ mockOwnerCallStatusProvider = {
137
+ listSessionNamesWithUnansweredOwnerCall: jest
138
+ .fn()
139
+ .mockResolvedValue(new Set<string>()),
140
+ };
141
+ mockNotificationRepository = {
142
+ getLastNotifiedEpochSeconds: jest.fn().mockResolvedValue(null),
143
+ setLastNotifiedEpochSeconds: jest.fn().mockResolvedValue(undefined),
144
+ sendSelfCheckNotification: jest.fn().mockResolvedValue(undefined),
145
+ };
146
+ mockMessageComposer = {
147
+ composeMainStalledSection: jest
148
+ .fn()
149
+ .mockReturnValue(MAIN_STALLED_SECTION),
150
+ composeSubAgentSection: jest.fn().mockReturnValue(SUBAGENT_SECTION),
151
+ };
152
+ mockSleeper = {
153
+ sleep: jest.fn().mockResolvedValue(undefined),
154
+ };
155
+ useCase = new NotifySilentLiveSessionsUseCase(
156
+ mockIssueRepository,
157
+ mockTmuxSessionRepository,
158
+ mockSessionOutputActivityRepository,
159
+ mockSubAgentActivityRepository,
160
+ mockOwnerCallStatusProvider,
161
+ mockNotificationRepository,
162
+ mockMessageComposer,
163
+ mockSleeper,
164
+ );
165
+ });
166
+
167
+ const setupLiveMonitoredSession = (
168
+ overrides: Partial<Issue> = {},
169
+ ): string => {
170
+ const issue = createMockIssue({
171
+ status: IN_TMUX_STATUS_NAME,
172
+ ...overrides,
173
+ });
174
+ const sessionName = toTmuxSessionName(issue.url);
175
+ mockIssueRepository.getAllOpened.mockResolvedValue([issue]);
176
+ mockTmuxSessionRepository.listLiveSessionNames.mockResolvedValue([
177
+ sessionName,
178
+ ]);
179
+ return sessionName;
180
+ };
181
+
182
+ it('exposes the monitored status and default thresholds as named constants', () => {
183
+ expect(DEFAULT_MONITORED_STATUS).toBe('In Tmux by human');
184
+ expect(DEFAULT_MONITORED_STATUS).toBe(IN_TMUX_STATUS_NAME);
185
+ expect(DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS).toBe(600);
186
+ expect(DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS).toBe(300);
187
+ expect(DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS).toBe(900);
188
+ expect(DEFAULT_NOTIFICATION_COOLDOWN_SECONDS).toBe(30 * 60);
189
+ expect(DEFAULT_NOTIFICATION_STAGGER_SECONDS).toBe(25);
190
+ });
191
+
192
+ it('sends only the main stalled section when the main session is silent and no owner call is pending', async () => {
193
+ const sessionName = setupLiveMonitoredSession();
194
+ mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
195
+ [
196
+ {
197
+ sessionName,
198
+ lastOutputEpochSeconds:
199
+ nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
200
+ },
201
+ ],
202
+ );
203
+
204
+ await useCase.run(runParams());
205
+
206
+ expect(mockMessageComposer.composeMainStalledSection).toHaveBeenCalledWith(
207
+ DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
208
+ );
209
+ expect(mockMessageComposer.composeSubAgentSection).not.toHaveBeenCalled();
210
+ expect(
211
+ mockNotificationRepository.sendSelfCheckNotification,
212
+ ).toHaveBeenCalledWith(sessionName, MAIN_STALLED_SECTION);
213
+ });
214
+
215
+ it('does not send the main stalled section when an owner call is pending', async () => {
216
+ const sessionName = setupLiveMonitoredSession();
217
+ mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
218
+ [
219
+ {
220
+ sessionName,
221
+ lastOutputEpochSeconds:
222
+ nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
223
+ },
224
+ ],
225
+ );
226
+ mockOwnerCallStatusProvider.listSessionNamesWithUnansweredOwnerCall.mockResolvedValue(
227
+ new Set([sessionName]),
228
+ );
229
+
230
+ await useCase.run(runParams());
231
+
232
+ expect(
233
+ mockMessageComposer.composeMainStalledSection,
234
+ ).not.toHaveBeenCalled();
235
+ expect(
236
+ mockNotificationRepository.sendSelfCheckNotification,
237
+ ).not.toHaveBeenCalled();
238
+ });
239
+
240
+ it('does not send the main stalled section when output is within the threshold', async () => {
241
+ const sessionName = setupLiveMonitoredSession();
242
+ mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
243
+ [
244
+ {
245
+ sessionName,
246
+ lastOutputEpochSeconds:
247
+ nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS + 1,
248
+ },
249
+ ],
250
+ );
251
+
252
+ await useCase.run(runParams());
253
+
254
+ expect(
255
+ mockNotificationRepository.sendSelfCheckNotification,
256
+ ).not.toHaveBeenCalled();
257
+ });
258
+
259
+ it('sends the sub-agent section when a sub-agent exceeds the silent threshold regardless of main output', async () => {
260
+ const sessionName = setupLiveMonitoredSession();
261
+ mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
262
+ [
263
+ {
264
+ sessionName,
265
+ lastOutputEpochSeconds: nowEpochSeconds,
266
+ },
267
+ ],
268
+ );
269
+ const subAgents: SubAgentActivity[] = [
270
+ {
271
+ label: 'sub-process-1',
272
+ silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
273
+ runningSeconds: 60,
274
+ },
275
+ ];
276
+ mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
277
+ new Map([[sessionName, subAgents]]),
278
+ );
279
+
280
+ await useCase.run(runParams());
281
+
282
+ expect(
283
+ mockMessageComposer.composeMainStalledSection,
284
+ ).not.toHaveBeenCalled();
285
+ expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith(
286
+ subAgents,
287
+ );
288
+ expect(
289
+ mockNotificationRepository.sendSelfCheckNotification,
290
+ ).toHaveBeenCalledWith(sessionName, SUBAGENT_SECTION);
291
+ });
292
+
293
+ it('sends the sub-agent section when a sub-agent exceeds the running threshold', async () => {
294
+ const sessionName = setupLiveMonitoredSession();
295
+ const subAgents: SubAgentActivity[] = [
296
+ {
297
+ label: 'sub-process-1',
298
+ silentSeconds: 10,
299
+ runningSeconds: DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
300
+ },
301
+ ];
302
+ mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
303
+ new Map([[sessionName, subAgents]]),
304
+ );
305
+
306
+ await useCase.run(runParams());
307
+
308
+ expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith(
309
+ subAgents,
310
+ );
311
+ });
312
+
313
+ it('does not include sub-agents that are below both thresholds', async () => {
314
+ const sessionName = setupLiveMonitoredSession();
315
+ mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
316
+ new Map([
317
+ [
318
+ sessionName,
319
+ [{ label: 'sub-process-1', silentSeconds: 10, runningSeconds: 60 }],
320
+ ],
321
+ ]),
322
+ );
323
+
324
+ await useCase.run(runParams());
325
+
326
+ expect(mockMessageComposer.composeSubAgentSection).not.toHaveBeenCalled();
327
+ expect(
328
+ mockNotificationRepository.sendSelfCheckNotification,
329
+ ).not.toHaveBeenCalled();
330
+ });
331
+
332
+ it('combines both sections into a single notification when both classifications apply', async () => {
333
+ const sessionName = setupLiveMonitoredSession();
334
+ mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
335
+ [
336
+ {
337
+ sessionName,
338
+ lastOutputEpochSeconds:
339
+ nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
340
+ },
341
+ ],
342
+ );
343
+ mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
344
+ new Map([
345
+ [
346
+ sessionName,
347
+ [
348
+ {
349
+ label: 'sub-process-1',
350
+ silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
351
+ runningSeconds: 60,
352
+ },
353
+ ],
354
+ ],
355
+ ]),
356
+ );
357
+
358
+ await useCase.run(runParams());
359
+
360
+ const sendCall =
361
+ mockNotificationRepository.sendSelfCheckNotification.mock.calls[0];
362
+ expect(sendCall[0]).toBe(sessionName);
363
+ expect(sendCall[1]).toContain(MAIN_STALLED_SECTION);
364
+ expect(sendCall[1]).toContain(SUBAGENT_SECTION);
365
+ });
366
+
367
+ it('still sends the sub-agent section even when an owner call is pending', async () => {
368
+ const sessionName = setupLiveMonitoredSession();
369
+ mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
370
+ [
371
+ {
372
+ sessionName,
373
+ lastOutputEpochSeconds:
374
+ nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
375
+ },
376
+ ],
377
+ );
378
+ mockOwnerCallStatusProvider.listSessionNamesWithUnansweredOwnerCall.mockResolvedValue(
379
+ new Set([sessionName]),
380
+ );
381
+ mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
382
+ new Map([
383
+ [
384
+ sessionName,
385
+ [
386
+ {
387
+ label: 'sub-process-1',
388
+ silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
389
+ runningSeconds: 60,
390
+ },
391
+ ],
392
+ ],
393
+ ]),
394
+ );
395
+
396
+ await useCase.run(runParams());
397
+
398
+ const sendCall =
399
+ mockNotificationRepository.sendSelfCheckNotification.mock.calls[0];
400
+ expect(sendCall[1]).not.toContain(MAIN_STALLED_SECTION);
401
+ expect(sendCall[1]).toContain(SUBAGENT_SECTION);
402
+ });
403
+
404
+ it('passes the monitored session names to every data provider', async () => {
405
+ const sessionName = setupLiveMonitoredSession();
406
+
407
+ await useCase.run(runParams());
408
+
409
+ expect(
410
+ mockSessionOutputActivityRepository.listSessionOutputActivities,
411
+ ).toHaveBeenCalledWith([sessionName]);
412
+ expect(
413
+ mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName,
414
+ ).toHaveBeenCalledWith([sessionName]);
415
+ expect(
416
+ mockOwnerCallStatusProvider.listSessionNamesWithUnansweredOwnerCall,
417
+ ).toHaveBeenCalledWith([sessionName]);
418
+ expect(mockIssueRepository.getAllOpened).toHaveBeenCalledWith(
419
+ mockProject,
420
+ allowCacheMinutes,
421
+ );
422
+ });
423
+
424
+ it('does not notify a live session that maps to no monitored issue', async () => {
425
+ mockIssueRepository.getAllOpened.mockResolvedValue([]);
426
+ mockTmuxSessionRepository.listLiveSessionNames.mockResolvedValue([
427
+ 'orphan_session',
428
+ ]);
429
+
430
+ await useCase.run(runParams());
431
+
432
+ expect(
433
+ mockSessionOutputActivityRepository.listSessionOutputActivities,
434
+ ).toHaveBeenCalledWith([]);
435
+ expect(
436
+ mockNotificationRepository.sendSelfCheckNotification,
437
+ ).not.toHaveBeenCalled();
438
+ });
439
+
440
+ it('does not notify a monitored issue whose session is not live', async () => {
441
+ const issue = createMockIssue({ status: IN_TMUX_STATUS_NAME });
442
+ mockIssueRepository.getAllOpened.mockResolvedValue([issue]);
443
+ mockTmuxSessionRepository.listLiveSessionNames.mockResolvedValue([]);
444
+
445
+ await useCase.run(runParams());
446
+
447
+ expect(
448
+ mockSessionOutputActivityRepository.listSessionOutputActivities,
449
+ ).toHaveBeenCalledWith([]);
450
+ expect(
451
+ mockNotificationRepository.sendSelfCheckNotification,
452
+ ).not.toHaveBeenCalled();
453
+ });
454
+
455
+ it('does not notify a live session whose issue status is not the monitored status', async () => {
456
+ const issue = createMockIssue({ status: 'In Progress' });
457
+ const sessionName = toTmuxSessionName(issue.url);
458
+ mockIssueRepository.getAllOpened.mockResolvedValue([issue]);
459
+ mockTmuxSessionRepository.listLiveSessionNames.mockResolvedValue([
460
+ sessionName,
461
+ ]);
462
+ mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
463
+ [
464
+ {
465
+ sessionName,
466
+ lastOutputEpochSeconds:
467
+ nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
468
+ },
469
+ ],
470
+ );
471
+
472
+ await useCase.run(runParams());
473
+
474
+ expect(
475
+ mockSessionOutputActivityRepository.listSessionOutputActivities,
476
+ ).toHaveBeenCalledWith([]);
477
+ expect(
478
+ mockNotificationRepository.sendSelfCheckNotification,
479
+ ).not.toHaveBeenCalled();
480
+ });
481
+
482
+ it('does not re-notify a session within the cooldown window', async () => {
483
+ const sessionName = setupLiveMonitoredSession();
484
+ mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
485
+ [
486
+ {
487
+ sessionName,
488
+ lastOutputEpochSeconds:
489
+ nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
490
+ },
491
+ ],
492
+ );
493
+ mockNotificationRepository.getLastNotifiedEpochSeconds.mockResolvedValue(
494
+ nowEpochSeconds - DEFAULT_NOTIFICATION_COOLDOWN_SECONDS + 1,
495
+ );
496
+
497
+ await useCase.run(runParams());
498
+
499
+ expect(
500
+ mockNotificationRepository.sendSelfCheckNotification,
501
+ ).not.toHaveBeenCalled();
502
+ expect(
503
+ mockNotificationRepository.setLastNotifiedEpochSeconds,
504
+ ).not.toHaveBeenCalled();
505
+ });
506
+
507
+ it('re-notifies a session once the cooldown window has elapsed', async () => {
508
+ const sessionName = setupLiveMonitoredSession();
509
+ mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
510
+ [
511
+ {
512
+ sessionName,
513
+ lastOutputEpochSeconds:
514
+ nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
515
+ },
516
+ ],
517
+ );
518
+ mockNotificationRepository.getLastNotifiedEpochSeconds.mockResolvedValue(
519
+ nowEpochSeconds - DEFAULT_NOTIFICATION_COOLDOWN_SECONDS,
520
+ );
521
+
522
+ await useCase.run(runParams());
523
+
524
+ expect(
525
+ mockNotificationRepository.sendSelfCheckNotification,
526
+ ).toHaveBeenCalledWith(sessionName, MAIN_STALLED_SECTION);
527
+ expect(
528
+ mockNotificationRepository.setLastNotifiedEpochSeconds,
529
+ ).toHaveBeenCalledWith(sessionName, nowEpochSeconds);
530
+ });
531
+
532
+ it('does not notify a session that has no recorded output activity and no sub-agents', async () => {
533
+ setupLiveMonitoredSession();
534
+ mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
535
+ [],
536
+ );
537
+
538
+ await useCase.run(runParams());
539
+
540
+ expect(
541
+ mockNotificationRepository.sendSelfCheckNotification,
542
+ ).not.toHaveBeenCalled();
543
+ });
544
+
545
+ it('sends to multiple sessions sequentially with a stagger delay between sends, not before the first or after the last', async () => {
546
+ const issueA = createMockIssue({ status: IN_TMUX_STATUS_NAME });
547
+ const issueB = createMockIssue({ status: IN_TMUX_STATUS_NAME });
548
+ const sessionA = toTmuxSessionName(issueA.url);
549
+ const sessionB = toTmuxSessionName(issueB.url);
550
+ mockIssueRepository.getAllOpened.mockResolvedValue([issueA, issueB]);
551
+ mockTmuxSessionRepository.listLiveSessionNames.mockResolvedValue([
552
+ sessionA,
553
+ sessionB,
554
+ ]);
555
+ mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
556
+ [
557
+ {
558
+ sessionName: sessionA,
559
+ lastOutputEpochSeconds:
560
+ nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
561
+ },
562
+ {
563
+ sessionName: sessionB,
564
+ lastOutputEpochSeconds:
565
+ nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
566
+ },
567
+ ],
568
+ );
569
+
570
+ const callOrder: string[] = [];
571
+ mockNotificationRepository.sendSelfCheckNotification.mockImplementation(
572
+ async (sessionName) => {
573
+ callOrder.push(`send:${sessionName}`);
574
+ },
575
+ );
576
+ mockSleeper.sleep.mockImplementation(async () => {
577
+ callOrder.push('sleep');
578
+ });
579
+
580
+ await useCase.run(runParams());
581
+
582
+ const sortedFirst = [sessionA, sessionB].sort()[0];
583
+ const sortedSecond = [sessionA, sessionB].sort()[1];
584
+ expect(callOrder).toEqual([
585
+ `send:${sortedFirst}`,
586
+ 'sleep',
587
+ `send:${sortedSecond}`,
588
+ ]);
589
+ expect(mockSleeper.sleep).toHaveBeenCalledTimes(1);
590
+ expect(mockSleeper.sleep).toHaveBeenCalledWith(
591
+ DEFAULT_NOTIFICATION_STAGGER_SECONDS * 1000,
592
+ );
593
+ });
594
+
595
+ it('does not suppress errors raised while sending a notification', async () => {
596
+ const sessionName = setupLiveMonitoredSession();
597
+ mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
598
+ [
599
+ {
600
+ sessionName,
601
+ lastOutputEpochSeconds:
602
+ nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
603
+ },
604
+ ],
605
+ );
606
+ mockNotificationRepository.sendSelfCheckNotification.mockRejectedValue(
607
+ new Error('send-keys failed'),
608
+ );
609
+
610
+ await expect(useCase.run(runParams())).rejects.toThrow('send-keys failed');
611
+ });
612
+ });