@oneuptime/common 12.0.8 → 12.0.9

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 (769) hide show
  1. package/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.ts +1 -1
  2. package/Models/AnalyticsModels/MetricItemAggMV1mByService.ts +1 -1
  3. package/Models/DatabaseModels/AIConversation.ts +32 -0
  4. package/Models/DatabaseModels/AIConversationMessage.ts +41 -0
  5. package/Models/DatabaseModels/AlertEpisodeMember.ts +27 -0
  6. package/Models/DatabaseModels/IncidentEpisodeMember.ts +28 -0
  7. package/Models/DatabaseModels/Index.ts +12 -4
  8. package/Models/DatabaseModels/{TelemetryEntity.ts → InventoryItem.ts} +139 -9
  9. package/Models/DatabaseModels/InventoryItemCustomField.ts +434 -0
  10. package/Models/DatabaseModels/{TelemetryEntityRelationship.ts → InventoryItemRelationship.ts} +8 -8
  11. package/Models/DatabaseModels/NetworkDevice.ts +141 -0
  12. package/Models/DatabaseModels/NetworkDeviceLink.ts +699 -0
  13. package/Models/DatabaseModels/NetworkDeviceLinkRule.ts +467 -0
  14. package/Models/DatabaseModels/NetworkTopologySuppression.ts +429 -0
  15. package/Models/DatabaseModels/OnCallDutyPolicyFeed.ts +9 -0
  16. package/Models/DatabaseModels/Project.ts +78 -0
  17. package/Models/DatabaseModels/UserCall.ts +42 -0
  18. package/Models/DatabaseModels/UserEmail.ts +44 -0
  19. package/Models/DatabaseModels/UserNotificationRule.ts +425 -55
  20. package/Models/DatabaseModels/UserOnCallLogTimeline.ts +24 -2
  21. package/Models/DatabaseModels/UserPush.ts +49 -0
  22. package/Models/DatabaseModels/UserSMS.ts +43 -0
  23. package/Models/DatabaseModels/UserTelegram.ts +59 -0
  24. package/Models/DatabaseModels/UserWebhook.ts +52 -0
  25. package/Models/DatabaseModels/UserWhatsApp.ts +41 -0
  26. package/Models/DatabaseModels/WorkflowVariable.ts +12 -0
  27. package/Server/API/AIChatAPI.ts +315 -1
  28. package/Server/API/DashboardAPI.ts +217 -1
  29. package/Server/API/OnCallReadinessAPI.ts +841 -0
  30. package/Server/API/TeamComplianceAPI.ts +69 -17
  31. package/Server/API/TelemetryAPI.ts +220 -12
  32. package/Server/EnvironmentConfig.ts +52 -0
  33. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +22 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.ts +4 -4
  35. package/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.ts +5 -5
  36. package/Server/Infrastructure/Postgres/SchemaMigrations/1786625176831-AddMonitoringMethodToNetworkDevice.ts +35 -0
  37. package/Server/Infrastructure/Postgres/SchemaMigrations/1786634985763-AddNetworkDeviceLink.ts +82 -0
  38. package/Server/Infrastructure/Postgres/SchemaMigrations/1786639512056-AddNetworkDeviceLinkRule.ts +91 -0
  39. package/Server/Infrastructure/Postgres/SchemaMigrations/1786639972982-AddNetworkTopologySuppression.ts +47 -0
  40. package/Server/Infrastructure/Postgres/SchemaMigrations/1786800000000-RenameTelemetryEntityToInventoryItem.ts +255 -0
  41. package/Server/Infrastructure/Postgres/SchemaMigrations/1786900000000-AddInventoryItemArchiveAndCustomFields.ts +107 -0
  42. package/Server/Infrastructure/Postgres/SchemaMigrations/1787000000000-AddOnCallNotificationFallbackColumns.ts +90 -0
  43. package/Server/Infrastructure/Postgres/SchemaMigrations/1787100000000-AddAIConversationPageContext.ts +39 -0
  44. package/Server/Infrastructure/Postgres/SchemaMigrations/1787200000000-AddAIChatMessageFeedback.ts +33 -0
  45. package/Server/Infrastructure/Postgres/SchemaMigrations/1787300000000-AddEpisodeMemberNotifyIndexes.ts +59 -0
  46. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +20 -0
  47. package/Server/Infrastructure/Queue.ts +78 -13
  48. package/Server/Middleware/PublicDashboardRateLimit.ts +593 -0
  49. package/Server/Services/AIService.ts +7 -0
  50. package/Server/Services/AlertEpisodeStateTimelineService.ts +29 -0
  51. package/Server/Services/AlertSeverityService.ts +63 -0
  52. package/Server/Services/DashboardService.ts +9 -10
  53. package/Server/Services/DatabaseService.ts +32 -2
  54. package/Server/Services/IncidentEpisodeStateTimelineService.ts +29 -0
  55. package/Server/Services/IncidentSeverityService.ts +76 -0
  56. package/Server/Services/Index.ts +12 -4
  57. package/Server/Services/InventoryItemCustomFieldService.ts +9 -0
  58. package/Server/Services/{TelemetryEntityRelationshipService.ts → InventoryItemRelationshipService.ts} +4 -4
  59. package/Server/Services/{TelemetryEntityService.ts → InventoryItemService.ts} +89 -20
  60. package/Server/Services/LogAggregationService.ts +45 -8
  61. package/Server/Services/MetricAggregationService.ts +121 -0
  62. package/Server/Services/MetricService.ts +7 -7
  63. package/Server/Services/NetworkDeviceLinkRuleService.ts +10 -0
  64. package/Server/Services/NetworkDeviceLinkService.ts +84 -0
  65. package/Server/Services/NetworkDeviceService.ts +140 -0
  66. package/Server/Services/NetworkSiteService.ts +77 -25
  67. package/Server/Services/NetworkTopologySuppressionService.ts +84 -0
  68. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +41 -29
  69. package/Server/Services/OnCallDutyPolicyExecutionLogService.ts +8 -0
  70. package/Server/Services/OnCallDutyPolicyExecutionLogTimelineService.ts +62 -13
  71. package/Server/Services/OnCallDutyPolicyScheduleService.ts +61 -1
  72. package/Server/Services/OnCallNotificationAlertingService.ts +742 -0
  73. package/Server/Services/OnCallReadinessService.ts +2803 -0
  74. package/Server/Services/OnCallSetupReminderService.ts +955 -0
  75. package/Server/Services/ProfileAggregationService.ts +123 -0
  76. package/Server/Services/StatusPageService.ts +9 -10
  77. package/Server/Services/TeamComplianceService.ts +429 -252
  78. package/Server/Services/UserCallService.ts +26 -1
  79. package/Server/Services/UserEmailService.ts +26 -1
  80. package/Server/Services/UserNotificationRuleAdminService.ts +1183 -0
  81. package/Server/Services/UserNotificationRuleService.ts +3812 -333
  82. package/Server/Services/UserOnCallLogService.ts +561 -48
  83. package/Server/Services/UserPushService.ts +29 -0
  84. package/Server/Services/UserService.ts +11 -0
  85. package/Server/Services/UserSmsService.ts +26 -1
  86. package/Server/Services/UserTelegramService.ts +24 -1
  87. package/Server/Services/UserWebhookService.ts +28 -1
  88. package/Server/Services/UserWhatsAppService.ts +24 -1
  89. package/Server/Types/Database/Permissions/BasePermission.ts +19 -0
  90. package/Server/Types/Database/Permissions/CreatePermission.ts +164 -0
  91. package/Server/Types/Database/Permissions/OwnerOnlyColumnPermission.ts +340 -0
  92. package/Server/Types/Database/Permissions/QueryPermission.ts +48 -0
  93. package/Server/Types/Database/Permissions/TenantPermission.ts +8 -1
  94. package/Server/Types/Workflow/Components/API/Delete.ts +1 -1
  95. package/Server/Types/Workflow/Components/API/Get.ts +1 -1
  96. package/Server/Types/Workflow/Components/API/Patch.ts +1 -1
  97. package/Server/Types/Workflow/Components/API/Post.ts +1 -1
  98. package/Server/Types/Workflow/Components/API/Put.ts +1 -1
  99. package/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.ts +29 -5
  100. package/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.ts +18 -10
  101. package/Server/Types/Workflow/Components/BaseModel/ModelArguments.ts +55 -0
  102. package/Server/Types/Workflow/Components/Conditions/IfElse.ts +3 -17
  103. package/Server/Types/Workflow/Components/Email.ts +25 -7
  104. package/Server/Types/Workflow/Components/JavaScript.ts +10 -3
  105. package/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.ts +1 -1
  106. package/Server/Utils/AI/Chat/ChatAgentRunner.ts +643 -48
  107. package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +32 -3
  108. package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +20 -6
  109. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +7 -0
  110. package/Server/Utils/AI/Toolbox/AIActionTools.ts +2 -2
  111. package/Server/Utils/AI/Toolbox/AIMetaTools.ts +863 -0
  112. package/Server/Utils/AI/Toolbox/AlertTools.ts +177 -15
  113. package/Server/Utils/AI/Toolbox/IncidentTools.ts +191 -10
  114. package/Server/Utils/AI/Toolbox/Index.ts +48 -0
  115. package/Server/Utils/AI/Toolbox/MonitorTools.ts +298 -11
  116. package/Server/Utils/AI/Toolbox/NoteWriteTools.ts +295 -0
  117. package/Server/Utils/AI/Toolbox/OnCallTools.ts +1246 -0
  118. package/Server/Utils/AI/Toolbox/RunbookTools.ts +424 -0
  119. package/Server/Utils/AI/Toolbox/SloTools.ts +456 -0
  120. package/Server/Utils/AI/Toolbox/StatusPageTools.ts +559 -0
  121. package/Server/Utils/AI/Toolbox/TeamTools.ts +327 -0
  122. package/Server/Utils/AI/Toolbox/TimelineTools.ts +615 -0
  123. package/Server/Utils/AI/Toolbox/WorkflowProbeTools.ts +664 -0
  124. package/Server/Utils/ClientIp.ts +221 -0
  125. package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +47 -0
  126. package/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.ts +163 -0
  127. package/Server/Utils/Dashboard/PublicDashboardSloWidget.ts +147 -0
  128. package/Server/Utils/Express.ts +12 -17
  129. package/Server/Utils/LLM/LLMService.ts +85 -8
  130. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +204 -10
  131. package/Server/Utils/SSRFProtection.ts +98 -23
  132. package/Server/Utils/StartServer.ts +12 -3
  133. package/Server/Utils/Telemetry/EntityRegistry.ts +205 -18
  134. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +25 -25
  135. package/Server/Utils/Telemetry/TelemetryEntity.ts +160 -52
  136. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +7 -3
  137. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +2146 -0
  138. package/Tests/App/Dashboard/CreateWorkflowModal.test.tsx +561 -0
  139. package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +2470 -0
  140. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +1897 -0
  141. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +3606 -0
  142. package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +784 -0
  143. package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +1119 -0
  144. package/Tests/App/Dashboard/SloWidgetFetching.test.tsx +531 -0
  145. package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +1312 -0
  146. package/Tests/App/Dashboard/UserSettingsSetupChecklistPage.test.tsx +1390 -0
  147. package/Tests/Models/InventoryItemModel.test.ts +174 -0
  148. package/Tests/Models/InventoryItemNaming.test.ts +302 -0
  149. package/Tests/Server/API/AIChatCancelAndFeedback.test.ts +437 -0
  150. package/Tests/Server/API/DashboardPublicRateLimit.test.ts +659 -0
  151. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +18 -0
  152. package/Tests/Server/API/DashboardPublicSloAPI.test.ts +880 -0
  153. package/Tests/Server/API/Helpers.ts +24 -15
  154. package/Tests/Server/API/OnCallReadinessAPI.test.ts +2680 -0
  155. package/Tests/Server/API/OnCallSetupReminderAPI.test.ts +915 -0
  156. package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +533 -0
  157. package/Tests/Server/Infrastructure/Postgres/InventoryItemArchiveMigration.test.ts +213 -0
  158. package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +432 -0
  159. package/Tests/Server/Infrastructure/Queue.test.ts +293 -0
  160. package/Tests/Server/Middleware/PublicDashboardRateLimit.test.ts +1645 -0
  161. package/Tests/Server/Services/AdminRuleEditGuards.test.ts +2848 -0
  162. package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +1393 -0
  163. package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +1802 -0
  164. package/Tests/Server/Services/EpisodeStateTimelineNote.test.ts +304 -0
  165. package/Tests/Server/Services/InventoryItemDisplayName.test.ts +339 -0
  166. package/Tests/Server/Services/{TelemetryEntityManualCreate.test.ts → InventoryItemManualCreate.test.ts} +27 -27
  167. package/Tests/Server/Services/IpAllowlistSpoofing.test.ts +450 -0
  168. package/Tests/Server/Services/LogAggregationService.test.ts +235 -1
  169. package/Tests/Server/Services/MetricAggregationService.test.ts +231 -0
  170. package/Tests/Server/Services/MetricEntityMVKeyParity.test.ts +80 -29
  171. package/Tests/Server/Services/MetricServiceAggregate.test.ts +30 -30
  172. package/Tests/Server/Services/NetworkSiteService.test.ts +18 -3
  173. package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +1728 -0
  174. package/Tests/Server/Services/NotificationDeletionImpact.test.ts +2402 -0
  175. package/Tests/Server/Services/OnCallDutyPolicyExecutionLogTimelineGapFeed.test.ts +394 -0
  176. package/Tests/Server/Services/OnCallNotificationFallback.test.ts +1744 -0
  177. package/Tests/Server/Services/OnCallReadinessService.test.ts +4295 -0
  178. package/Tests/Server/Services/OnCallSetupReminder.test.ts +1272 -0
  179. package/Tests/Server/Services/OnCallWeeklyReadinessDigest.test.ts +1021 -0
  180. package/Tests/Server/Services/ProfileAggregationService.test.ts +296 -0
  181. package/Tests/Server/Services/SeverityCreationRuleBackfill.test.ts +1536 -0
  182. package/Tests/Server/Services/SeverityRuleBackfill.test.ts +1818 -0
  183. package/Tests/Server/Services/TeamComplianceServiceBehaviour.test.ts +1845 -0
  184. package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +1394 -0
  185. package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +1166 -0
  186. package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +1468 -0
  187. package/Tests/Server/Services/UserOnCallLogNoNotificationRules.test.ts +1457 -0
  188. package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +1546 -0
  189. package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +529 -0
  190. package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +1219 -0
  191. package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +1089 -0
  192. package/Tests/Server/Types/Workflow/Components/ApiComponentErrorPort.test.ts +2 -1
  193. package/Tests/Server/Types/Workflow/Components/BaseModelDatabaseComponents.test.ts +190 -0
  194. package/Tests/Server/Types/Workflow/Components/ChatWebhookComponents.test.ts +44 -14
  195. package/Tests/Server/Types/Workflow/Components/Email.test.ts +151 -0
  196. package/Tests/Server/Types/Workflow/Components/IfElse.test.ts +98 -0
  197. package/Tests/Server/Types/Workflow/Components/JavaScript.test.ts +51 -0
  198. package/Tests/Server/Utils/AI/AIMetaTools.test.ts +586 -0
  199. package/Tests/Server/Utils/AI/AlertMonitorFilters.test.ts +582 -0
  200. package/Tests/Server/Utils/AI/ChatAgentRunner.test.ts +726 -0
  201. package/Tests/Server/Utils/AI/IncidentToolsFilters.test.ts +315 -0
  202. package/Tests/Server/Utils/AI/LLMServiceStopReason.test.ts +314 -0
  203. package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +26 -3
  204. package/Tests/Server/Utils/AI/NoteWriteTools.test.ts +268 -0
  205. package/Tests/Server/Utils/AI/ObservabilityChatPrompt.test.ts +169 -0
  206. package/Tests/Server/Utils/AI/OnCallTools.test.ts +664 -0
  207. package/Tests/Server/Utils/AI/RunbookTools.test.ts +325 -0
  208. package/Tests/Server/Utils/AI/SloTools.test.ts +306 -0
  209. package/Tests/Server/Utils/AI/StatusPageTools.test.ts +391 -0
  210. package/Tests/Server/Utils/AI/TeamTools.test.ts +257 -0
  211. package/Tests/Server/Utils/AI/TimelineTools.test.ts +472 -0
  212. package/Tests/Server/Utils/AI/WorkflowProbeTools.test.ts +428 -0
  213. package/Tests/Server/Utils/ClientIp.test.ts +438 -0
  214. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +171 -0
  215. package/Tests/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.test.ts +383 -0
  216. package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +23 -25
  217. package/Tests/Server/Utils/MicrosoftTeamsWebhookUrlValidation.test.ts +6 -0
  218. package/Tests/Server/Utils/Monitor/Criteria/DnssecMonitorCriteria.test.ts +307 -0
  219. package/Tests/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.test.ts +468 -0
  220. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorTelemetryDeepLinks.test.ts +460 -0
  221. package/Tests/Server/Utils/ResponseRateLimitStatusCodes.test.ts +137 -0
  222. package/Tests/Server/Utils/SSRFProtectionBypasses.test.ts +40 -8
  223. package/Tests/Server/Utils/SSRFProtectionUserInfo.test.ts +351 -0
  224. package/Tests/Server/Utils/Telemetry/EntityRegistryRetirement.test.ts +531 -0
  225. package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +15 -15
  226. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +356 -39
  227. package/Tests/Types/IP/IP.test.ts +263 -0
  228. package/Tests/Types/IP/IPWhitelist.test.ts +197 -0
  229. package/Tests/Types/IP/IPv6.test.ts +12 -1
  230. package/Tests/Types/Monitor/SnmpOid.test.ts +64 -0
  231. package/Tests/Types/NetworkDevice/NetworkDeviceMonitoringMethod.test.ts +110 -0
  232. package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeAudit.test.ts +106 -0
  233. package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeDifferential.test.ts +511 -0
  234. package/Tests/Types/OnCallDutyPolicy/ScheduleCoverageEndToEnd.test.ts +489 -0
  235. package/Tests/Types/OnCallDutyPolicy/ScheduleCoverageGapTolerance.test.ts +479 -0
  236. package/Tests/Types/OnCallDutyPolicy/ScheduleCoverageState.test.ts +822 -0
  237. package/Tests/Types/Telemetry/EntityTypeGroups.test.ts +6 -6
  238. package/Tests/Types/Workflow/BaseModelComponents.test.ts +429 -0
  239. package/Tests/Types/Workflow/Components/BaseModel.test.ts +225 -0
  240. package/Tests/Types/Workflow/IntegrationCredentialMetadata.test.ts +100 -0
  241. package/Tests/Types/Workflow/TemplateSyntax.test.ts +112 -0
  242. package/Tests/Types/Workflow/Templates.test.ts +916 -101
  243. package/Tests/UI/Components/ActiveFilterChipsOpenRoute.test.tsx +82 -0
  244. package/Tests/UI/Components/ComponentsModal.test.tsx +564 -7
  245. package/Tests/UI/Components/KeyboardShortcut.test.tsx +95 -0
  246. package/Tests/UI/Components/LogDetailsPanelCrossSignal.test.tsx +448 -0
  247. package/Tests/UI/Components/LogsTableCrossLinks.test.tsx +263 -0
  248. package/Tests/UI/Components/PendingProjectInvitations.test.tsx +913 -0
  249. package/Tests/UI/Components/SimpleLogViewer.test.tsx +228 -0
  250. package/Tests/UI/Components/TableRowSelectability.test.tsx +312 -0
  251. package/Tests/UI/Components/Workflow/GraphLint.test.ts +72 -0
  252. package/Tests/UI/Components/Workflow/GraphLintSummary.test.ts +755 -0
  253. package/Tests/UI/Components/Workflow/ModelColumnEditor.test.ts +242 -7
  254. package/Tests/UI/Components/Workflow/ModelColumnEditorServerContract.test.ts +15 -7
  255. package/Tests/UI/Components/Workflow/ModelSchema.test.ts +242 -28
  256. package/Tests/UI/Components/Workflow/RunStatusWatcher.test.ts +59 -1
  257. package/Tests/UI/Components/Workflow/StepTraceViewer.test.tsx +256 -0
  258. package/Tests/UI/Components/Workflow/UseRunWatch.test.tsx +665 -0
  259. package/Tests/UI/Components/Workflow/WorkflowIssuesModal.test.tsx +485 -0
  260. package/Tests/UI/Components/Workflow/WorkflowLogModal.test.tsx +478 -0
  261. package/Tests/UI/Components/Workflow/WorkflowStatusBar.test.tsx +379 -0
  262. package/Tests/UI/EsbuildConfig.test.ts +607 -0
  263. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
  264. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +11 -4
  265. package/Tests/UI/Utils/ModelAPICreateMiscData.test.ts +94 -0
  266. package/Tests/UI/Utils/Platform.test.ts +147 -0
  267. package/Tests/UI/Utils/ProjectInvitationDisplay.test.ts +357 -0
  268. package/Tests/Utils/Monitor/NetworkDeviceLinkRuleUtil.test.ts +198 -0
  269. package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +487 -0
  270. package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +698 -0
  271. package/Tests/__mocks__/bullmq.js +55 -0
  272. package/Types/AI/AIChatMessageStatus.ts +8 -1
  273. package/Types/AI/AIChatTypes.ts +12 -0
  274. package/Types/Database/AccessControl/OwnerOnlyColumn.ts +88 -0
  275. package/Types/Exception/ExceptionCode.ts +2 -0
  276. package/Types/Exception/ServiceUnavailableException.ts +8 -0
  277. package/Types/Exception/TooManyRequestsException.ts +8 -0
  278. package/Types/IP/IP.ts +93 -47
  279. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +49 -3
  280. package/Types/NetworkDevice/NetworkDeviceMonitoringMethod.ts +55 -0
  281. package/Types/OnCallDutyPolicy/Layer.ts +203 -149
  282. package/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus.ts +13 -0
  283. package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +155 -10
  284. package/Types/Permission.ts +193 -0
  285. package/Types/Telemetry/EntityRelationshipType.ts +1 -1
  286. package/Types/Telemetry/EntitySource.ts +1 -1
  287. package/Types/Telemetry/EntityType.ts +1 -1
  288. package/Types/Telemetry/EntityTypeGroups.ts +1 -1
  289. package/Types/Workflow/Components/BaseModel.ts +75 -29
  290. package/Types/Workflow/Components/Discord.ts +1 -0
  291. package/Types/Workflow/Components/Email.ts +12 -3
  292. package/Types/Workflow/Components/JavaScript.ts +7 -0
  293. package/Types/Workflow/Components/MicrosoftTeams.ts +3 -2
  294. package/Types/Workflow/Components/Slack.ts +1 -0
  295. package/Types/Workflow/Components/Telegram.ts +1 -0
  296. package/Types/Workflow/TemplateSyntax.ts +44 -0
  297. package/Types/Workflow/Templates.ts +2097 -45
  298. package/UI/Components/Calendar/Calendar.css +43 -0
  299. package/UI/Components/Calendar/Calendar.tsx +8 -0
  300. package/UI/Components/Card/Card.tsx +2 -2
  301. package/UI/Components/Checkbox/Checkbox.tsx +16 -0
  302. package/UI/Components/Dictionary/Dictionary.tsx +48 -9
  303. package/UI/Components/FormModal/BasicFormModal.tsx +2 -1
  304. package/UI/Components/Header/HeaderIconDropdownButton.tsx +53 -3
  305. package/UI/Components/Input/Input.tsx +1 -0
  306. package/UI/Components/KeyboardShortcut/KeyboardKey.ts +185 -0
  307. package/UI/Components/KeyboardShortcut/KeyboardShortcut.tsx +87 -0
  308. package/UI/Components/LogsViewer/LogsViewer.tsx +28 -0
  309. package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +31 -0
  310. package/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.tsx +18 -18
  311. package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +363 -14
  312. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +11 -0
  313. package/UI/Components/LogsViewer/components/LogsTable.tsx +155 -12
  314. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +29 -0
  315. package/UI/Components/LogsViewer/types.ts +23 -0
  316. package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +9 -2
  317. package/UI/Components/Navbar/NavBarMenuModal.tsx +15 -30
  318. package/UI/Components/ProjectInvitations/PendingProjectInvitations.tsx +442 -0
  319. package/UI/Components/SimpleLogViewer/SimpleLogViewer.tsx +23 -1
  320. package/UI/Components/Table/Table.tsx +49 -16
  321. package/UI/Components/Table/TableBody.tsx +53 -28
  322. package/UI/Components/Table/TableHeader.tsx +13 -0
  323. package/UI/Components/Table/TableRow.tsx +58 -26
  324. package/UI/Components/Workflow/ArgumentsForm.tsx +155 -9
  325. package/UI/Components/Workflow/ComponentReturnValueViewer.tsx +26 -0
  326. package/UI/Components/Workflow/ComponentSettingsModal.tsx +10 -1
  327. package/UI/Components/Workflow/ComponentValuePickerModal.tsx +135 -7
  328. package/UI/Components/Workflow/ComponentsModal.tsx +116 -22
  329. package/UI/Components/Workflow/DocumentationViewer.tsx +59 -9
  330. package/UI/Components/Workflow/GraphLint.ts +42 -5
  331. package/UI/Components/Workflow/GraphLintSummary.ts +390 -0
  332. package/UI/Components/Workflow/ModelColumnEditor.tsx +154 -22
  333. package/UI/Components/Workflow/ModelSchema.ts +115 -33
  334. package/UI/Components/Workflow/RunStatusWatcher.ts +1 -1
  335. package/UI/Components/Workflow/StepTraceViewer.tsx +1 -1
  336. package/UI/Components/Workflow/UseRunWatch.ts +212 -0
  337. package/UI/Components/Workflow/VariableModal.tsx +6 -2
  338. package/UI/Components/Workflow/Workflow.tsx +49 -3
  339. package/UI/Components/Workflow/WorkflowIssuesModal.tsx +255 -0
  340. package/UI/Components/Workflow/WorkflowLogModal.tsx +128 -0
  341. package/UI/Components/Workflow/WorkflowStatusBar.tsx +224 -0
  342. package/UI/Utils/AIChatExport/ConversationMarkdown.ts +10 -0
  343. package/UI/Utils/ModelAPI/ModelAPI.ts +7 -1
  344. package/UI/Utils/Platform.ts +149 -0
  345. package/UI/Utils/ProjectInvitationDisplay.ts +118 -0
  346. package/UI/esbuild-config.js +22 -1
  347. package/Utils/Monitor/NetworkDeviceLinkRuleUtil.ts +187 -0
  348. package/Utils/Monitor/NetworkTopologyUtil.ts +888 -164
  349. package/Utils/Telemetry/CrossSignalScope.ts +502 -0
  350. package/Utils/Telemetry/EntityKey.ts +5 -5
  351. package/Utils/Telemetry/EntityRelationship.ts +1 -1
  352. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js +1 -1
  353. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js +1 -1
  354. package/build/dist/Models/DatabaseModels/AIConversation.js +32 -0
  355. package/build/dist/Models/DatabaseModels/AIConversation.js.map +1 -1
  356. package/build/dist/Models/DatabaseModels/AIConversationMessage.js +42 -0
  357. package/build/dist/Models/DatabaseModels/AIConversationMessage.js.map +1 -1
  358. package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js +28 -0
  359. package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js.map +1 -1
  360. package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js +29 -0
  361. package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js.map +1 -1
  362. package/build/dist/Models/DatabaseModels/Index.js +12 -4
  363. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  364. package/build/dist/Models/DatabaseModels/{TelemetryEntity.js → InventoryItem.js} +167 -31
  365. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -0
  366. package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js +454 -0
  367. package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js.map +1 -0
  368. package/build/dist/Models/DatabaseModels/{TelemetryEntityRelationship.js → InventoryItemRelationship.js} +27 -27
  369. package/build/dist/Models/DatabaseModels/InventoryItemRelationship.js.map +1 -0
  370. package/build/dist/Models/DatabaseModels/NetworkDevice.js +141 -0
  371. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  372. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +719 -0
  373. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -0
  374. package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js +475 -0
  375. package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js.map +1 -0
  376. package/build/dist/Models/DatabaseModels/NetworkTopologySuppression.js +446 -0
  377. package/build/dist/Models/DatabaseModels/NetworkTopologySuppression.js.map +1 -0
  378. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyFeed.js +9 -0
  379. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyFeed.js.map +1 -1
  380. package/build/dist/Models/DatabaseModels/Project.js +80 -0
  381. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  382. package/build/dist/Models/DatabaseModels/UserCall.js +46 -2
  383. package/build/dist/Models/DatabaseModels/UserCall.js.map +1 -1
  384. package/build/dist/Models/DatabaseModels/UserEmail.js +48 -2
  385. package/build/dist/Models/DatabaseModels/UserEmail.js.map +1 -1
  386. package/build/dist/Models/DatabaseModels/UserNotificationRule.js +424 -55
  387. package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
  388. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +24 -2
  389. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
  390. package/build/dist/Models/DatabaseModels/UserPush.js +51 -1
  391. package/build/dist/Models/DatabaseModels/UserPush.js.map +1 -1
  392. package/build/dist/Models/DatabaseModels/UserSMS.js +47 -2
  393. package/build/dist/Models/DatabaseModels/UserSMS.js.map +1 -1
  394. package/build/dist/Models/DatabaseModels/UserTelegram.js +65 -3
  395. package/build/dist/Models/DatabaseModels/UserTelegram.js.map +1 -1
  396. package/build/dist/Models/DatabaseModels/UserWebhook.js +56 -2
  397. package/build/dist/Models/DatabaseModels/UserWebhook.js.map +1 -1
  398. package/build/dist/Models/DatabaseModels/UserWhatsApp.js +45 -2
  399. package/build/dist/Models/DatabaseModels/UserWhatsApp.js.map +1 -1
  400. package/build/dist/Models/DatabaseModels/WorkflowVariable.js +12 -0
  401. package/build/dist/Models/DatabaseModels/WorkflowVariable.js.map +1 -1
  402. package/build/dist/Server/API/AIChatAPI.js +237 -1
  403. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  404. package/build/dist/Server/API/DashboardAPI.js +165 -13
  405. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  406. package/build/dist/Server/API/OnCallReadinessAPI.js +599 -0
  407. package/build/dist/Server/API/OnCallReadinessAPI.js.map +1 -0
  408. package/build/dist/Server/API/TeamComplianceAPI.js +68 -9
  409. package/build/dist/Server/API/TeamComplianceAPI.js.map +1 -1
  410. package/build/dist/Server/API/TelemetryAPI.js +129 -18
  411. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  412. package/build/dist/Server/EnvironmentConfig.js +45 -0
  413. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  414. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +22 -0
  415. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  416. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js +4 -4
  417. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786625176831-AddMonitoringMethodToNetworkDevice.js +18 -0
  418. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786625176831-AddMonitoringMethodToNetworkDevice.js.map +1 -0
  419. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786634985763-AddNetworkDeviceLink.js +39 -0
  420. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786634985763-AddNetworkDeviceLink.js.map +1 -0
  421. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639512056-AddNetworkDeviceLinkRule.js +38 -0
  422. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639512056-AddNetworkDeviceLinkRule.js.map +1 -0
  423. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639972982-AddNetworkTopologySuppression.js +22 -0
  424. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639972982-AddNetworkTopologySuppression.js.map +1 -0
  425. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786800000000-RenameTelemetryEntityToInventoryItem.js +150 -0
  426. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786800000000-RenameTelemetryEntityToInventoryItem.js.map +1 -0
  427. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786900000000-AddInventoryItemArchiveAndCustomFields.js +57 -0
  428. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786900000000-AddInventoryItemArchiveAndCustomFields.js.map +1 -0
  429. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787000000000-AddOnCallNotificationFallbackColumns.js +69 -0
  430. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787000000000-AddOnCallNotificationFallbackColumns.js.map +1 -0
  431. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787100000000-AddAIConversationPageContext.js +32 -0
  432. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787100000000-AddAIConversationPageContext.js.map +1 -0
  433. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787200000000-AddAIChatMessageFeedback.js +26 -0
  434. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787200000000-AddAIChatMessageFeedback.js.map +1 -0
  435. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787300000000-AddEpisodeMemberNotifyIndexes.js +48 -0
  436. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787300000000-AddEpisodeMemberNotifyIndexes.js.map +1 -0
  437. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +20 -0
  438. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  439. package/build/dist/Server/Infrastructure/Queue.js +72 -13
  440. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  441. package/build/dist/Server/Middleware/PublicDashboardRateLimit.js +399 -0
  442. package/build/dist/Server/Middleware/PublicDashboardRateLimit.js.map +1 -0
  443. package/build/dist/Server/Services/AIService.js +1 -0
  444. package/build/dist/Server/Services/AIService.js.map +1 -1
  445. package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js +24 -3
  446. package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js.map +1 -1
  447. package/build/dist/Server/Services/AlertSeverityService.js +54 -0
  448. package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
  449. package/build/dist/Server/Services/DashboardService.js +10 -9
  450. package/build/dist/Server/Services/DashboardService.js.map +1 -1
  451. package/build/dist/Server/Services/DatabaseService.js +24 -2
  452. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  453. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +24 -3
  454. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
  455. package/build/dist/Server/Services/IncidentSeverityService.js +67 -0
  456. package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
  457. package/build/dist/Server/Services/Index.js +12 -4
  458. package/build/dist/Server/Services/Index.js.map +1 -1
  459. package/build/dist/Server/Services/InventoryItemCustomFieldService.js +9 -0
  460. package/build/dist/Server/Services/InventoryItemCustomFieldService.js.map +1 -0
  461. package/build/dist/Server/Services/{TelemetryEntityRelationshipService.js → InventoryItemRelationshipService.js} +6 -6
  462. package/build/dist/Server/Services/InventoryItemRelationshipService.js.map +1 -0
  463. package/build/dist/Server/Services/{TelemetryEntityService.js → InventoryItemService.js} +68 -23
  464. package/build/dist/Server/Services/InventoryItemService.js.map +1 -0
  465. package/build/dist/Server/Services/LogAggregationService.js +27 -8
  466. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  467. package/build/dist/Server/Services/MetricAggregationService.js +80 -0
  468. package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
  469. package/build/dist/Server/Services/MetricService.js +6 -6
  470. package/build/dist/Server/Services/MetricService.js.map +1 -1
  471. package/build/dist/Server/Services/NetworkDeviceLinkRuleService.js +9 -0
  472. package/build/dist/Server/Services/NetworkDeviceLinkRuleService.js.map +1 -0
  473. package/build/dist/Server/Services/NetworkDeviceLinkService.js +71 -0
  474. package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -0
  475. package/build/dist/Server/Services/NetworkDeviceService.js +113 -0
  476. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  477. package/build/dist/Server/Services/NetworkSiteService.js +53 -13
  478. package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
  479. package/build/dist/Server/Services/NetworkTopologySuppressionService.js +85 -0
  480. package/build/dist/Server/Services/NetworkTopologySuppressionService.js.map +1 -0
  481. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +48 -32
  482. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  483. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js +8 -0
  484. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js.map +1 -1
  485. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogTimelineService.js +51 -12
  486. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogTimelineService.js.map +1 -1
  487. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +57 -13
  488. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  489. package/build/dist/Server/Services/OnCallNotificationAlertingService.js +548 -0
  490. package/build/dist/Server/Services/OnCallNotificationAlertingService.js.map +1 -0
  491. package/build/dist/Server/Services/OnCallReadinessService.js +1961 -0
  492. package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -0
  493. package/build/dist/Server/Services/OnCallSetupReminderService.js +738 -0
  494. package/build/dist/Server/Services/OnCallSetupReminderService.js.map +1 -0
  495. package/build/dist/Server/Services/ProfileAggregationService.js +68 -4
  496. package/build/dist/Server/Services/ProfileAggregationService.js.map +1 -1
  497. package/build/dist/Server/Services/StatusPageService.js +11 -10
  498. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  499. package/build/dist/Server/Services/TeamComplianceService.js +312 -160
  500. package/build/dist/Server/Services/TeamComplianceService.js.map +1 -1
  501. package/build/dist/Server/Services/UserCallService.js +24 -1
  502. package/build/dist/Server/Services/UserCallService.js.map +1 -1
  503. package/build/dist/Server/Services/UserEmailService.js +24 -1
  504. package/build/dist/Server/Services/UserEmailService.js.map +1 -1
  505. package/build/dist/Server/Services/UserNotificationRuleAdminService.js +858 -0
  506. package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -0
  507. package/build/dist/Server/Services/UserNotificationRuleService.js +2830 -175
  508. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  509. package/build/dist/Server/Services/UserOnCallLogService.js +488 -43
  510. package/build/dist/Server/Services/UserOnCallLogService.js.map +1 -1
  511. package/build/dist/Server/Services/UserPushService.js +26 -0
  512. package/build/dist/Server/Services/UserPushService.js.map +1 -1
  513. package/build/dist/Server/Services/UserService.js +10 -0
  514. package/build/dist/Server/Services/UserService.js.map +1 -1
  515. package/build/dist/Server/Services/UserSmsService.js +24 -1
  516. package/build/dist/Server/Services/UserSmsService.js.map +1 -1
  517. package/build/dist/Server/Services/UserTelegramService.js +22 -1
  518. package/build/dist/Server/Services/UserTelegramService.js.map +1 -1
  519. package/build/dist/Server/Services/UserWebhookService.js +25 -1
  520. package/build/dist/Server/Services/UserWebhookService.js.map +1 -1
  521. package/build/dist/Server/Services/UserWhatsAppService.js +22 -1
  522. package/build/dist/Server/Services/UserWhatsAppService.js.map +1 -1
  523. package/build/dist/Server/Types/Database/Permissions/BasePermission.js +12 -1
  524. package/build/dist/Server/Types/Database/Permissions/BasePermission.js.map +1 -1
  525. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +126 -0
  526. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
  527. package/build/dist/Server/Types/Database/Permissions/OwnerOnlyColumnPermission.js +254 -0
  528. package/build/dist/Server/Types/Database/Permissions/OwnerOnlyColumnPermission.js.map +1 -0
  529. package/build/dist/Server/Types/Database/Permissions/QueryPermission.js +47 -2
  530. package/build/dist/Server/Types/Database/Permissions/QueryPermission.js.map +1 -1
  531. package/build/dist/Server/Types/Database/Permissions/TenantPermission.js +7 -0
  532. package/build/dist/Server/Types/Database/Permissions/TenantPermission.js.map +1 -1
  533. package/build/dist/Server/Types/Workflow/Components/API/Delete.js +1 -1
  534. package/build/dist/Server/Types/Workflow/Components/API/Delete.js.map +1 -1
  535. package/build/dist/Server/Types/Workflow/Components/API/Get.js +1 -1
  536. package/build/dist/Server/Types/Workflow/Components/API/Get.js.map +1 -1
  537. package/build/dist/Server/Types/Workflow/Components/API/Patch.js +1 -1
  538. package/build/dist/Server/Types/Workflow/Components/API/Patch.js.map +1 -1
  539. package/build/dist/Server/Types/Workflow/Components/API/Post.js +1 -1
  540. package/build/dist/Server/Types/Workflow/Components/API/Post.js.map +1 -1
  541. package/build/dist/Server/Types/Workflow/Components/API/Put.js +1 -1
  542. package/build/dist/Server/Types/Workflow/Components/API/Put.js.map +1 -1
  543. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js +21 -5
  544. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js.map +1 -1
  545. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js +14 -10
  546. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js.map +1 -1
  547. package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js +31 -0
  548. package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js.map +1 -1
  549. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js +3 -9
  550. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js.map +1 -1
  551. package/build/dist/Server/Types/Workflow/Components/Email.js +15 -4
  552. package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
  553. package/build/dist/Server/Types/Workflow/Components/JavaScript.js +7 -2
  554. package/build/dist/Server/Types/Workflow/Components/JavaScript.js.map +1 -1
  555. package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js +1 -1
  556. package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js.map +1 -1
  557. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +514 -56
  558. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
  559. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +20 -3
  560. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
  561. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +19 -6
  562. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -1
  563. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +7 -0
  564. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  565. package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js +2 -2
  566. package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js.map +1 -1
  567. package/build/dist/Server/Utils/AI/Toolbox/AIMetaTools.js +692 -0
  568. package/build/dist/Server/Utils/AI/Toolbox/AIMetaTools.js.map +1 -0
  569. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js +148 -12
  570. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js.map +1 -1
  571. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +157 -10
  572. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -1
  573. package/build/dist/Server/Utils/AI/Toolbox/Index.js +37 -0
  574. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
  575. package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js +259 -14
  576. package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js.map +1 -1
  577. package/build/dist/Server/Utils/AI/Toolbox/NoteWriteTools.js +235 -0
  578. package/build/dist/Server/Utils/AI/Toolbox/NoteWriteTools.js.map +1 -0
  579. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +1000 -0
  580. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -0
  581. package/build/dist/Server/Utils/AI/Toolbox/RunbookTools.js +356 -0
  582. package/build/dist/Server/Utils/AI/Toolbox/RunbookTools.js.map +1 -0
  583. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +394 -0
  584. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -0
  585. package/build/dist/Server/Utils/AI/Toolbox/StatusPageTools.js +465 -0
  586. package/build/dist/Server/Utils/AI/Toolbox/StatusPageTools.js.map +1 -0
  587. package/build/dist/Server/Utils/AI/Toolbox/TeamTools.js +280 -0
  588. package/build/dist/Server/Utils/AI/Toolbox/TeamTools.js.map +1 -0
  589. package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js +527 -0
  590. package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js.map +1 -0
  591. package/build/dist/Server/Utils/AI/Toolbox/WorkflowProbeTools.js +548 -0
  592. package/build/dist/Server/Utils/AI/Toolbox/WorkflowProbeTools.js.map +1 -0
  593. package/build/dist/Server/Utils/ClientIp.js +137 -0
  594. package/build/dist/Server/Utils/ClientIp.js.map +1 -0
  595. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +38 -0
  596. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
  597. package/build/dist/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.js +89 -0
  598. package/build/dist/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.js.map +1 -0
  599. package/build/dist/Server/Utils/Dashboard/PublicDashboardSloWidget.js +77 -0
  600. package/build/dist/Server/Utils/Dashboard/PublicDashboardSloWidget.js.map +1 -0
  601. package/build/dist/Server/Utils/Express.js +12 -12
  602. package/build/dist/Server/Utils/Express.js.map +1 -1
  603. package/build/dist/Server/Utils/LLM/LLMService.js +70 -7
  604. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  605. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +121 -11
  606. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  607. package/build/dist/Server/Utils/SSRFProtection.js +82 -21
  608. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  609. package/build/dist/Server/Utils/StartServer.js +12 -4
  610. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  611. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +165 -18
  612. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  613. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +11 -11
  614. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
  615. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +122 -47
  616. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  617. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +7 -3
  618. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  619. package/build/dist/Types/AI/AIChatMessageStatus.js +8 -1
  620. package/build/dist/Types/AI/AIChatMessageStatus.js.map +1 -1
  621. package/build/dist/Types/AI/AIChatTypes.js +12 -0
  622. package/build/dist/Types/AI/AIChatTypes.js.map +1 -1
  623. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js +60 -0
  624. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js.map +1 -0
  625. package/build/dist/Types/Exception/ExceptionCode.js +2 -0
  626. package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
  627. package/build/dist/Types/Exception/ServiceUnavailableException.js +8 -0
  628. package/build/dist/Types/Exception/ServiceUnavailableException.js.map +1 -0
  629. package/build/dist/Types/Exception/TooManyRequestsException.js +8 -0
  630. package/build/dist/Types/Exception/TooManyRequestsException.js.map +1 -0
  631. package/build/dist/Types/IP/IP.js +87 -43
  632. package/build/dist/Types/IP/IP.js.map +1 -1
  633. package/build/dist/Types/NetworkDevice/NetworkDeviceMonitoringMethod.js +50 -0
  634. package/build/dist/Types/NetworkDevice/NetworkDeviceMonitoringMethod.js.map +1 -0
  635. package/build/dist/Types/OnCallDutyPolicy/Layer.js +186 -123
  636. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  637. package/build/dist/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus.js +13 -0
  638. package/build/dist/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus.js.map +1 -1
  639. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +105 -11
  640. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
  641. package/build/dist/Types/Permission.js +174 -0
  642. package/build/dist/Types/Permission.js.map +1 -1
  643. package/build/dist/Types/Telemetry/EntityRelationshipType.js +1 -1
  644. package/build/dist/Types/Telemetry/EntitySource.js +1 -1
  645. package/build/dist/Types/Telemetry/EntityType.js +1 -1
  646. package/build/dist/Types/Telemetry/EntityTypeGroups.js +1 -1
  647. package/build/dist/Types/Telemetry/EntityTypeGroups.js.map +1 -1
  648. package/build/dist/Types/Workflow/Components/BaseModel.js +66 -28
  649. package/build/dist/Types/Workflow/Components/BaseModel.js.map +1 -1
  650. package/build/dist/Types/Workflow/Components/Discord.js +1 -0
  651. package/build/dist/Types/Workflow/Components/Discord.js.map +1 -1
  652. package/build/dist/Types/Workflow/Components/Email.js +12 -3
  653. package/build/dist/Types/Workflow/Components/Email.js.map +1 -1
  654. package/build/dist/Types/Workflow/Components/JavaScript.js +7 -0
  655. package/build/dist/Types/Workflow/Components/JavaScript.js.map +1 -1
  656. package/build/dist/Types/Workflow/Components/MicrosoftTeams.js +3 -2
  657. package/build/dist/Types/Workflow/Components/MicrosoftTeams.js.map +1 -1
  658. package/build/dist/Types/Workflow/Components/Slack.js +1 -0
  659. package/build/dist/Types/Workflow/Components/Slack.js.map +1 -1
  660. package/build/dist/Types/Workflow/Components/Telegram.js +1 -0
  661. package/build/dist/Types/Workflow/Components/Telegram.js.map +1 -1
  662. package/build/dist/Types/Workflow/TemplateSyntax.js +12 -0
  663. package/build/dist/Types/Workflow/TemplateSyntax.js.map +1 -1
  664. package/build/dist/Types/Workflow/Templates.js +1931 -38
  665. package/build/dist/Types/Workflow/Templates.js.map +1 -1
  666. package/build/dist/UI/Components/Calendar/Calendar.js +1 -1
  667. package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
  668. package/build/dist/UI/Components/Checkbox/Checkbox.js +1 -1
  669. package/build/dist/UI/Components/Checkbox/Checkbox.js.map +1 -1
  670. package/build/dist/UI/Components/Dictionary/Dictionary.js +25 -11
  671. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  672. package/build/dist/UI/Components/FormModal/BasicFormModal.js.map +1 -1
  673. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +27 -4
  674. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  675. package/build/dist/UI/Components/Input/Input.js +1 -0
  676. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  677. package/build/dist/UI/Components/KeyboardShortcut/KeyboardKey.js +163 -0
  678. package/build/dist/UI/Components/KeyboardShortcut/KeyboardKey.js.map +1 -0
  679. package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcut.js +47 -0
  680. package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcut.js.map +1 -0
  681. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +4 -4
  682. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  683. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +11 -1
  684. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -1
  685. package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js +10 -8
  686. package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js.map +1 -1
  687. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +227 -14
  688. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
  689. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +5 -0
  690. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  691. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +69 -5
  692. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
  693. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +8 -0
  694. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  695. package/build/dist/UI/Components/LogsViewer/types.js.map +1 -1
  696. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +9 -2
  697. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js.map +1 -1
  698. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +8 -21
  699. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  700. package/build/dist/UI/Components/ProjectInvitations/PendingProjectInvitations.js +251 -0
  701. package/build/dist/UI/Components/ProjectInvitations/PendingProjectInvitations.js.map +1 -0
  702. package/build/dist/UI/Components/SimpleLogViewer/SimpleLogViewer.js +9 -2
  703. package/build/dist/UI/Components/SimpleLogViewer/SimpleLogViewer.js.map +1 -1
  704. package/build/dist/UI/Components/Table/Table.js +27 -15
  705. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  706. package/build/dist/UI/Components/Table/TableBody.js +24 -18
  707. package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
  708. package/build/dist/UI/Components/Table/TableHeader.js +9 -1
  709. package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
  710. package/build/dist/UI/Components/Table/TableRow.js +29 -21
  711. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  712. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +130 -12
  713. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  714. package/build/dist/UI/Components/Workflow/ComponentReturnValueViewer.js +10 -1
  715. package/build/dist/UI/Components/Workflow/ComponentReturnValueViewer.js.map +1 -1
  716. package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js +4 -4
  717. package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js.map +1 -1
  718. package/build/dist/UI/Components/Workflow/ComponentValuePickerModal.js +57 -7
  719. package/build/dist/UI/Components/Workflow/ComponentValuePickerModal.js.map +1 -1
  720. package/build/dist/UI/Components/Workflow/ComponentsModal.js +53 -18
  721. package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
  722. package/build/dist/UI/Components/Workflow/DocumentationViewer.js +19 -6
  723. package/build/dist/UI/Components/Workflow/DocumentationViewer.js.map +1 -1
  724. package/build/dist/UI/Components/Workflow/GraphLint.js +33 -4
  725. package/build/dist/UI/Components/Workflow/GraphLint.js.map +1 -1
  726. package/build/dist/UI/Components/Workflow/GraphLintSummary.js +231 -0
  727. package/build/dist/UI/Components/Workflow/GraphLintSummary.js.map +1 -0
  728. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +116 -21
  729. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -1
  730. package/build/dist/UI/Components/Workflow/ModelSchema.js +72 -34
  731. package/build/dist/UI/Components/Workflow/ModelSchema.js.map +1 -1
  732. package/build/dist/UI/Components/Workflow/RunStatusWatcher.js +1 -1
  733. package/build/dist/UI/Components/Workflow/RunStatusWatcher.js.map +1 -1
  734. package/build/dist/UI/Components/Workflow/StepTraceViewer.js +1 -1
  735. package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -1
  736. package/build/dist/UI/Components/Workflow/UseRunWatch.js +123 -0
  737. package/build/dist/UI/Components/Workflow/UseRunWatch.js.map +1 -0
  738. package/build/dist/UI/Components/Workflow/VariableModal.js +3 -2
  739. package/build/dist/UI/Components/Workflow/VariableModal.js.map +1 -1
  740. package/build/dist/UI/Components/Workflow/Workflow.js +41 -1
  741. package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
  742. package/build/dist/UI/Components/Workflow/WorkflowIssuesModal.js +99 -0
  743. package/build/dist/UI/Components/Workflow/WorkflowIssuesModal.js.map +1 -0
  744. package/build/dist/UI/Components/Workflow/WorkflowLogModal.js +56 -0
  745. package/build/dist/UI/Components/Workflow/WorkflowLogModal.js.map +1 -0
  746. package/build/dist/UI/Components/Workflow/WorkflowStatusBar.js +92 -0
  747. package/build/dist/UI/Components/Workflow/WorkflowStatusBar.js.map +1 -0
  748. package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js +9 -0
  749. package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js.map +1 -1
  750. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +1 -1
  751. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
  752. package/build/dist/UI/Utils/Platform.js +118 -0
  753. package/build/dist/UI/Utils/Platform.js.map +1 -0
  754. package/build/dist/UI/Utils/ProjectInvitationDisplay.js +106 -0
  755. package/build/dist/UI/Utils/ProjectInvitationDisplay.js.map +1 -0
  756. package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js +108 -0
  757. package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js.map +1 -0
  758. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +642 -136
  759. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  760. package/build/dist/Utils/Telemetry/CrossSignalScope.js +328 -0
  761. package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -0
  762. package/build/dist/Utils/Telemetry/EntityKey.js +5 -5
  763. package/build/dist/Utils/Telemetry/EntityRelationship.js +1 -1
  764. package/jest.config.json +1 -0
  765. package/package.json +1 -1
  766. package/build/dist/Models/DatabaseModels/TelemetryEntity.js.map +0 -1
  767. package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js.map +0 -1
  768. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +0 -1
  769. package/build/dist/Server/Services/TelemetryEntityService.js.map +0 -1
@@ -0,0 +1,1845 @@
1
+ import TeamComplianceService, {
2
+ TeamComplianceStatus,
3
+ } from "../../../Server/Services/TeamComplianceService";
4
+ import OnCallReadinessService, {
5
+ ReadinessCoverageCell,
6
+ ReadinessStatus,
7
+ ReadinessSummary,
8
+ UserReadiness,
9
+ } from "../../../Server/Services/OnCallReadinessService";
10
+ import TeamComplianceSettingService from "../../../Server/Services/TeamComplianceSettingService";
11
+ import TeamMemberService from "../../../Server/Services/TeamMemberService";
12
+ import TeamService from "../../../Server/Services/TeamService";
13
+ import UserService from "../../../Server/Services/UserService";
14
+ import UserEmailService from "../../../Server/Services/UserEmailService";
15
+ import UserSmsService from "../../../Server/Services/UserSmsService";
16
+ import UserCallService from "../../../Server/Services/UserCallService";
17
+ import UserPushService from "../../../Server/Services/UserPushService";
18
+ import IncidentSeverityService from "../../../Server/Services/IncidentSeverityService";
19
+ import AlertSeverityService from "../../../Server/Services/AlertSeverityService";
20
+ import UserNotificationRuleService from "../../../Server/Services/UserNotificationRuleService";
21
+ import ComplianceRuleType from "../../../Types/Team/ComplianceRuleType";
22
+ import NotificationRuleType from "../../../Types/NotificationRule/NotificationRuleType";
23
+ import BadDataException from "../../../Types/Exception/BadDataException";
24
+ import ObjectID from "../../../Types/ObjectID";
25
+ import { LIMIT_PER_PROJECT } from "../../../Types/Database/LimitMax";
26
+ import Team from "../../../Models/DatabaseModels/Team";
27
+ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
28
+
29
+ /*
30
+ * The readiness service is replaced wholesale, rather than spied on in place.
31
+ *
32
+ * Everything except the default export is kept, because the enums and the
33
+ * contract types are values this file uses to BUILD its fixtures - a mock that
34
+ * dropped them would leave the tests writing readiness payloads by hand and
35
+ * drifting from the shape the real service emits.
36
+ *
37
+ * Replacing the class itself buys two things. TeamComplianceService is a unit
38
+ * here, so the seam should be a stub and not the real module's top-level import
39
+ * graph (TypeORM, every database model, the whole service layer). And a stub
40
+ * makes the DEPENDENCY explicit: this file asserts, once and loudly, that the
41
+ * real class actually exposes the entry points it stubs - see "the batched
42
+ * readiness contract this service depends on" below - rather than letting a
43
+ * jest.spyOn failure in beforeEach take out fifty unrelated tests.
44
+ */
45
+ jest.mock("../../../Server/Services/OnCallReadinessService", () => {
46
+ const actual: Record<string, unknown> = jest.requireActual(
47
+ "../../../Server/Services/OnCallReadinessService",
48
+ );
49
+
50
+ return {
51
+ ...actual,
52
+ __esModule: true,
53
+ default: {
54
+ getReadinessForProject: jest.fn(),
55
+ getReadinessForUsers: jest.fn(),
56
+ getReadinessForUser: jest.fn(),
57
+ },
58
+ };
59
+ });
60
+
61
+ /*
62
+ * TeamComplianceService answers "is this responder actually reachable?" for
63
+ * Teams > View > Compliance, and a project owner who reads a green row there
64
+ * believes the person on that row will be paged. That makes every one of this
65
+ * service's judgements load-bearing: a false green is a page that nobody ever
66
+ * receives, and a false red is an owner chasing a responder who is already
67
+ * configured correctly.
68
+ *
69
+ * It was the ONLY surface answering that question until Phase 2. It is now one
70
+ * of several, all reading OnCallReadinessService - which is the point, because
71
+ * two surfaces disagreeing about whether someone can be paged is worse than
72
+ * either of them being wrong on its own.
73
+ *
74
+ * This file began as a CHARACTERIZATION suite: it pinned the behaviour the
75
+ * service had before the readiness work, including four defects the plan
76
+ * (Internal/Roadmap/OnCallNotificationReadiness.md, section 2.6) called out.
77
+ * PHASE 2 HAS NOW CLOSED ALL FOUR, by rebuilding the two "has on-call rules"
78
+ * checks on top of OnCallReadinessService, and the four DEFECT blocks below
79
+ * have been inverted in place - each still sits under its original heading so
80
+ * the before/after is readable in one diff.
81
+ *
82
+ * What is pinned:
83
+ *
84
+ * (A) The four "has a notification method" rules. Each one asks its own
85
+ * User*Service for a single row scoped to userId + projectId AND
86
+ * isVerified: true - an unverified phone number is not a phone number as
87
+ * far as paging is concerned - and each carries its own reason string
88
+ * that the table prints verbatim. These four stayed here rather than
89
+ * moving to the readiness service: ComplianceRuleType names one specific
90
+ * channel per rule, so there is nothing to batch and nothing for the two
91
+ * surfaces to disagree about - they are already asking one table the same
92
+ * question.
93
+ *
94
+ * (B) The two "has on-call rules" rules, now answered off a UserReadiness
95
+ * coverage array instead of a per-severity walk. A user is compliant only
96
+ * when EVERY (ruleType, severity) cell is covered or explicitly opted out;
97
+ * the missing ones are named, not counted. No cells is vacuously
98
+ * compliant.
99
+ *
100
+ * (C) checkUserCompliance skips disabled settings without asking the
101
+ * underlying service anything at all, and reports compliance purely by
102
+ * "did anything land in nonCompliantRules".
103
+ *
104
+ * (D) getTeamComplianceStatus refuses to describe a team that does not exist.
105
+ *
106
+ * (E) A rule type the switch does not know about fails OPEN (compliant).
107
+ *
108
+ * The four defects, and how Phase 2 closed each:
109
+ *
110
+ * DEFECT 1 (plan 2.6, old TeamComplianceService.ts:416 and :517) - only
111
+ * userCallId/userSmsId/userEmailId/userPushId counted as "a notification
112
+ * method", so a responder paged on Telegram, WhatsApp or a webhook was
113
+ * reported NON-COMPLIANT even though the page would land.
114
+ * CLOSED: the service no longer reads channel columns at all. It reads
115
+ * `hasRule` off a coverage cell, and which channels make a rule count is
116
+ * OnCallReadinessService's judgement - made once, for all seven channels,
117
+ * for every surface.
118
+ *
119
+ * DEFECT 2 (plan 2.6, old :395) - the rule lookup did not filter on ruleType,
120
+ * so a rule that only fires WHEN_USER_GOES_OFF_CALL was counted as incident
121
+ * on-call coverage. The dangerous direction: a false GREEN.
122
+ * CLOSED: coverage cells are keyed BY ruleType and the incident check reads
123
+ * only ON_CALL_EXECUTED_INCIDENT cells, the alert check only
124
+ * ON_CALL_EXECUTED_ALERT.
125
+ *
126
+ * DEFECT 3 (old :473) - alert severities were fetched with a hard-coded limit
127
+ * of 100 while incident severities used LIMIT_PER_PROJECT, so the two halves
128
+ * of one feature disagreed about how many severities a project may have.
129
+ * CLOSED: neither half fetches severities any more. Both read the same
130
+ * coverage array, so they cannot disagree.
131
+ *
132
+ * DEFECT 4 (plan 2.6, old :95 and :119) - team members and users were both
133
+ * fetched with limit: 100. The 101st member was not reported
134
+ * non-compliant; they were not reported at all, which reads as "everyone is
135
+ * fine".
136
+ * CLOSED: LIMIT_PER_PROJECT at all three sites, including the compliance
137
+ * settings read the plan's table did not list.
138
+ *
139
+ * And the N+1 the plan flags alongside them - one findBy per severity per user -
140
+ * is closed by the same delegation, but NOT by the delegation alone, and the
141
+ * distinction is worth stating because the first attempt at it got this wrong.
142
+ * Delegating to readiness "once per user" replaced a cheap N+1 with an expensive
143
+ * one: getReadinessForUser resolves the project's entire responder set on every
144
+ * call, so a per-user fill cost five or six heavy queries per member instead of
145
+ * one light query per severity. The whole team is now answered by AT MOST TWO
146
+ * readiness computations - the project-scope summary, plus one batched
147
+ * getReadinessForUsers for whoever that summary does not cover - and the rule
148
+ * checks make no lookup of their own at all.
149
+ *
150
+ * (F) A member the batch cannot resolve is reported as UNCHECKED rather than
151
+ * as compliant, and does not fail the page for anybody else.
152
+ *
153
+ * (G) SECURITY, pre-existing and not a Phase 2 regression: the team row is
154
+ * read by id AND projectId. It used to be read by id alone with
155
+ * isRoot: true, so a team id from another project resolved and its
156
+ * members' reachability was described to a caller from outside it.
157
+ */
158
+
159
+ const PROJECT_ID: ObjectID = new ObjectID(
160
+ "11111111-1111-4111-8111-111111111111",
161
+ );
162
+ const TEAM_ID: ObjectID = new ObjectID("22222222-2222-4222-8222-222222222222");
163
+ const USER_ID: ObjectID = new ObjectID("33333333-3333-4333-8333-333333333333");
164
+ const OTHER_USER_ID: ObjectID = new ObjectID(
165
+ "44444444-4444-4444-8444-444444444444",
166
+ );
167
+
168
+ // The shape checkRuleCompliance answers with.
169
+ interface RuleCheckResult {
170
+ compliant: boolean;
171
+ reason: string;
172
+ }
173
+
174
+ // The shape checkUserCompliance answers with.
175
+ interface UserComplianceResult {
176
+ isCompliant: boolean;
177
+ nonCompliantRules: Array<{ ruleType: ComplianceRuleType; reason: string }>;
178
+ }
179
+
180
+ // Enough of a findBy argument to assert on, without importing FindBy generics.
181
+ interface CapturedFindBy {
182
+ query: Record<string, unknown>;
183
+ select?: Record<string, unknown>;
184
+ limit?: number;
185
+ skip?: number;
186
+ props?: { isRoot?: boolean };
187
+ }
188
+
189
+ /*
190
+ * One (ruleType, severity) cell of a UserReadiness coverage array - the unit the
191
+ * on-call-rule checks now reason about. `hasRule` and `isOptOut` are the
192
+ * readiness service's verdicts, already reconciled across all seven channels, so
193
+ * a test here says "readiness reports this cell covered" rather than "a row with
194
+ * this column set exists".
195
+ */
196
+ function coverageCell(data: {
197
+ ruleType: NotificationRuleType;
198
+ severityId?: string | undefined;
199
+ severityName?: string | undefined;
200
+ hasRule?: boolean | undefined;
201
+ isOptOut?: boolean | undefined;
202
+ }): ReadinessCoverageCell {
203
+ return {
204
+ ruleType: data.ruleType,
205
+ severityId: data.severityId ? new ObjectID(data.severityId) : undefined,
206
+ severityName: data.severityName,
207
+ hasRule: data.hasRule === true,
208
+ isOptOut: data.isOptOut === true,
209
+ };
210
+ }
211
+
212
+ function readinessWith(
213
+ coverage: Array<ReadinessCoverageCell>,
214
+ userId: ObjectID = USER_ID,
215
+ ): UserReadiness {
216
+ return {
217
+ userId: userId,
218
+ userName: "Ada",
219
+ userEmail: "ada@example.com",
220
+ userProfilePictureId: undefined,
221
+ status: ReadinessStatus.PartiallyReady,
222
+ methods: [],
223
+ coverage: coverage,
224
+ reasons: [],
225
+ reachedVia: [],
226
+ teams: [],
227
+ };
228
+ }
229
+
230
+ function summaryWith(users: Array<UserReadiness>): ReadinessSummary {
231
+ return {
232
+ projectId: PROJECT_ID,
233
+ onCallDutyPolicyId: undefined,
234
+ readyCount: 0,
235
+ partiallyReadyCount: users.length,
236
+ notReachableCount: 0,
237
+ /*
238
+ * Both are scope-level facts this service does not read - it consults the
239
+ * per-user coverage cells and nothing else - but they are part of the
240
+ * contract, so the fixture carries them rather than casting them away. A
241
+ * summary that ARRIVES truncated is handled without needing the flag: a
242
+ * member missing from it falls into the batched fill and is resolved
243
+ * directly, so truncation costs an extra read rather than a wrong verdict.
244
+ */
245
+ isFallbackEnabled: true,
246
+ isTruncated: false,
247
+ users: users,
248
+ };
249
+ }
250
+
251
+ /*
252
+ * checkRuleCompliance and checkUserCompliance are private statics. Calling them
253
+ * directly (rather than only through getTeamComplianceStatus) is what lets each
254
+ * branch of the switch be pinned in isolation; it does not widen the service's
255
+ * public surface.
256
+ */
257
+ function callPrivate(
258
+ name: string,
259
+ ...args: Array<unknown>
260
+ ): Promise<RuleCheckResult | UserComplianceResult> {
261
+ const statics: Record<
262
+ string,
263
+ (
264
+ ...callArgs: Array<unknown>
265
+ ) => Promise<RuleCheckResult | UserComplianceResult>
266
+ > = TeamComplianceService as unknown as Record<
267
+ string,
268
+ (
269
+ ...callArgs: Array<unknown>
270
+ ) => Promise<RuleCheckResult | UserComplianceResult>
271
+ >;
272
+
273
+ return statics[name]!.apply(TeamComplianceService, args);
274
+ }
275
+
276
+ /*
277
+ * Readiness is now an ARGUMENT to the rule checks rather than something they
278
+ * fetch. That is the caller-side half of the N+1 fix: the whole-team read
279
+ * resolves readiness in one batch and hands each user's answer down, so a rule
280
+ * check that went looking for its own would be a per-user, per-rule fan-out
281
+ * hiding inside a predicate. `stagedReadiness` is what a test hands in, and
282
+ * `answerReadinessWith` sets it.
283
+ */
284
+ let stagedReadiness: UserReadiness | undefined;
285
+
286
+ function checkRule(ruleType: ComplianceRuleType): Promise<RuleCheckResult> {
287
+ return callPrivate(
288
+ "checkRuleCompliance",
289
+ USER_ID,
290
+ PROJECT_ID,
291
+ ruleType,
292
+ stagedReadiness,
293
+ ) as Promise<RuleCheckResult>;
294
+ }
295
+
296
+ function checkUser(
297
+ settings: Array<{ ruleType: ComplianceRuleType; enabled: boolean }>,
298
+ ): Promise<UserComplianceResult> {
299
+ return callPrivate(
300
+ "checkUserCompliance",
301
+ USER_ID,
302
+ PROJECT_ID,
303
+ settings,
304
+ stagedReadiness,
305
+ ) as Promise<UserComplianceResult>;
306
+ }
307
+
308
+ function firstCall(spy: jest.SpyInstance): CapturedFindBy {
309
+ return spy.mock.calls[0]![0] as CapturedFindBy;
310
+ }
311
+
312
+ let userEmailFindBy: jest.SpyInstance;
313
+ let userSmsFindBy: jest.SpyInstance;
314
+ let userCallFindBy: jest.SpyInstance;
315
+ let userPushFindBy: jest.SpyInstance;
316
+ let incidentSeverityFindBy: jest.SpyInstance;
317
+ let alertSeverityFindBy: jest.SpyInstance;
318
+ let notificationRuleFindBy: jest.SpyInstance;
319
+ let teamFindOneBy: jest.SpyInstance;
320
+ let complianceSettingFindBy: jest.SpyInstance;
321
+ let teamMemberFindBy: jest.SpyInstance;
322
+ let userFindBy: jest.SpyInstance;
323
+ let readinessForUser: jest.SpyInstance;
324
+ let readinessForUsers: jest.SpyInstance;
325
+ let readinessForProject: jest.SpyInstance;
326
+
327
+ // Maps a "has method" rule type back to the service it interrogates.
328
+ function methodSpyFor(ruleType: ComplianceRuleType): jest.SpyInstance {
329
+ switch (ruleType) {
330
+ case ComplianceRuleType.HasNotificationEmailMethod:
331
+ return userEmailFindBy;
332
+ case ComplianceRuleType.HasNotificationSMSMethod:
333
+ return userSmsFindBy;
334
+ case ComplianceRuleType.HasNotificationCallMethod:
335
+ return userCallFindBy;
336
+ case ComplianceRuleType.HasNotificationPushMethod:
337
+ return userPushFindBy;
338
+ default:
339
+ throw new Error(`No notification-method service for ${ruleType}`);
340
+ }
341
+ }
342
+
343
+ /*
344
+ * Hand the rule checks a coverage array. The default posture is "no cells at
345
+ * all", which is the readiness-service equivalent of a project with no
346
+ * severities configured.
347
+ */
348
+ function answerReadinessWith(coverage: Array<ReadinessCoverageCell>): void {
349
+ stagedReadiness = readinessWith(coverage);
350
+ }
351
+
352
+ beforeEach(() => {
353
+ /*
354
+ * Call history is cleared explicitly because restoreAllMocks does not do it
355
+ * for the readiness stubs. jest.spyOn returns the EXISTING mock when the
356
+ * property it is asked to spy on is already a mock function - which the three
357
+ * entry points on the mocked module are - so those three spies are the same
358
+ * objects in every test and would otherwise accumulate calls across the file.
359
+ * That turns "this render made one batched call" into an assertion about
360
+ * whatever the previous test happened to do, which is exactly the kind of
361
+ * false green these tests exist to catch elsewhere.
362
+ */
363
+ jest.clearAllMocks();
364
+
365
+ stagedReadiness = readinessWith([]);
366
+ // Default posture: nothing configured anywhere. Each test opts into rows.
367
+ userEmailFindBy = jest
368
+ .spyOn(UserEmailService, "findBy")
369
+ .mockResolvedValue([] as never);
370
+ userSmsFindBy = jest
371
+ .spyOn(UserSmsService, "findBy")
372
+ .mockResolvedValue([] as never);
373
+ userCallFindBy = jest
374
+ .spyOn(UserCallService, "findBy")
375
+ .mockResolvedValue([] as never);
376
+ userPushFindBy = jest
377
+ .spyOn(UserPushService, "findBy")
378
+ .mockResolvedValue([] as never);
379
+ incidentSeverityFindBy = jest
380
+ .spyOn(IncidentSeverityService, "findBy")
381
+ .mockResolvedValue([] as never);
382
+ alertSeverityFindBy = jest
383
+ .spyOn(AlertSeverityService, "findBy")
384
+ .mockResolvedValue([] as never);
385
+ notificationRuleFindBy = jest
386
+ .spyOn(UserNotificationRuleService, "findBy")
387
+ .mockResolvedValue([] as never);
388
+ teamFindOneBy = jest
389
+ .spyOn(TeamService, "findOneBy")
390
+ .mockResolvedValue({ name: "Platform On-Call" } as Team);
391
+ complianceSettingFindBy = jest
392
+ .spyOn(TeamComplianceSettingService, "findBy")
393
+ .mockResolvedValue([] as never);
394
+ teamMemberFindBy = jest
395
+ .spyOn(TeamMemberService, "findBy")
396
+ .mockResolvedValue([] as never);
397
+ userFindBy = jest.spyOn(UserService, "findBy").mockResolvedValue([] as never);
398
+ /*
399
+ * Readiness is the on-call-rule half's only source now, and the whole-team
400
+ * read reaches it through exactly two entry points: the project-scope summary,
401
+ * and one batched fill for whoever that summary does not cover.
402
+ * getReadinessForUser is stubbed as well precisely so the tests below can
403
+ * assert it is NEVER reached - a single per-user call surviving anywhere in
404
+ * this path is the defect, not an implementation detail.
405
+ */
406
+ readinessForUser = jest
407
+ .spyOn(OnCallReadinessService, "getReadinessForUser")
408
+ .mockResolvedValue(readinessWith([]) as never);
409
+ readinessForUsers = jest
410
+ .spyOn(OnCallReadinessService, "getReadinessForUsers")
411
+ .mockResolvedValue([] as never);
412
+ readinessForProject = jest
413
+ .spyOn(OnCallReadinessService, "getReadinessForProject")
414
+ .mockResolvedValue(summaryWith([]) as never);
415
+ });
416
+
417
+ afterEach(() => {
418
+ jest.restoreAllMocks();
419
+ });
420
+
421
+ /*
422
+ * ------------------------------------------------------------------------- *
423
+ * (A) The four "has a verified notification method" rules.
424
+ * -------------------------------------------------------------------------
425
+ */
426
+
427
+ describe("checkRuleCompliance - notification method rules", () => {
428
+ test.each<[ComplianceRuleType]>([
429
+ [ComplianceRuleType.HasNotificationEmailMethod],
430
+ [ComplianceRuleType.HasNotificationSMSMethod],
431
+ [ComplianceRuleType.HasNotificationCallMethod],
432
+ [ComplianceRuleType.HasNotificationPushMethod],
433
+ ])(
434
+ "%s is compliant with an empty reason when one row comes back",
435
+ async (ruleType: ComplianceRuleType) => {
436
+ methodSpyFor(ruleType).mockResolvedValue([{ _id: "method-1" }] as never);
437
+
438
+ const result: RuleCheckResult = await checkRule(ruleType);
439
+
440
+ expect(result.compliant).toBe(true);
441
+ // A compliant row must print nothing, not "OK" - the table renders it raw.
442
+ expect(result.reason).toBe("");
443
+ },
444
+ );
445
+
446
+ test.each<[ComplianceRuleType, string]>([
447
+ [
448
+ ComplianceRuleType.HasNotificationEmailMethod,
449
+ "No verified email address configured for notifications",
450
+ ],
451
+ [
452
+ ComplianceRuleType.HasNotificationSMSMethod,
453
+ "No verified phone number configured for SMS notifications",
454
+ ],
455
+ [
456
+ ComplianceRuleType.HasNotificationCallMethod,
457
+ "No verified phone number configured for call notifications",
458
+ ],
459
+ [
460
+ ComplianceRuleType.HasNotificationPushMethod,
461
+ "No verified push notification device configured",
462
+ ],
463
+ ])(
464
+ "%s is non-compliant with its exact reason when nothing comes back",
465
+ async (ruleType: ComplianceRuleType, reason: string) => {
466
+ const result: RuleCheckResult = await checkRule(ruleType);
467
+
468
+ expect(result.compliant).toBe(false);
469
+ expect(result.reason).toBe(reason);
470
+ },
471
+ );
472
+
473
+ test.each<[ComplianceRuleType]>([
474
+ [ComplianceRuleType.HasNotificationEmailMethod],
475
+ [ComplianceRuleType.HasNotificationSMSMethod],
476
+ [ComplianceRuleType.HasNotificationCallMethod],
477
+ [ComplianceRuleType.HasNotificationPushMethod],
478
+ ])(
479
+ "%s only counts VERIFIED methods - the query carries isVerified: true",
480
+ async (ruleType: ComplianceRuleType) => {
481
+ await checkRule(ruleType);
482
+
483
+ const spy: jest.SpyInstance = methodSpyFor(ruleType);
484
+ expect(spy).toHaveBeenCalledTimes(1);
485
+
486
+ const call: CapturedFindBy = firstCall(spy);
487
+ /*
488
+ * An unverified phone number cannot receive a page, so counting one would
489
+ * be a false green. This is the assertion that keeps that honest.
490
+ */
491
+ expect(call.query["isVerified"]).toBe(true);
492
+ expect(call.query["userId"]).toBe(USER_ID);
493
+ expect(call.query["projectId"]).toBe(PROJECT_ID);
494
+ // Existence check only - one row is all it takes.
495
+ expect(call.limit).toBe(1);
496
+ expect(call.skip).toBe(0);
497
+ expect(call.props?.isRoot).toBe(true);
498
+ },
499
+ );
500
+
501
+ test("each method rule asks only its OWN service", async () => {
502
+ await checkRule(ComplianceRuleType.HasNotificationSMSMethod);
503
+
504
+ expect(userSmsFindBy).toHaveBeenCalledTimes(1);
505
+ expect(userEmailFindBy).not.toHaveBeenCalled();
506
+ expect(userCallFindBy).not.toHaveBeenCalled();
507
+ expect(userPushFindBy).not.toHaveBeenCalled();
508
+ });
509
+
510
+ test("more than one verified row is still just compliant", async () => {
511
+ userEmailFindBy.mockResolvedValue([
512
+ { _id: "email-1" },
513
+ { _id: "email-2" },
514
+ ] as never);
515
+
516
+ const result: RuleCheckResult = await checkRule(
517
+ ComplianceRuleType.HasNotificationEmailMethod,
518
+ );
519
+
520
+ expect(result).toEqual({ compliant: true, reason: "" });
521
+ });
522
+ });
523
+
524
+ /*
525
+ * ------------------------------------------------------------------------- *
526
+ * (B) The two "has on-call rules for every severity" rules, now answered off a
527
+ * UserReadiness coverage array rather than a per-severity walk of the rule
528
+ * table. What is pinned here is the MAPPING - which cells count as a gap, in
529
+ * what order they are named, and in exactly what wording - because that is what
530
+ * the compliance table prints. Whether a given cell is covered is
531
+ * OnCallReadinessService's judgement and is tested there.
532
+ * -------------------------------------------------------------------------
533
+ */
534
+
535
+ describe("checkRuleCompliance - HasIncidentOnCallRules", () => {
536
+ test("a covered cell is compliant, whichever channel covered it", async () => {
537
+ /*
538
+ * DEFECT 1 INVERTED. There is no longer a channel column in sight here. The
539
+ * service asks readiness "is this cell covered?", and readiness has already
540
+ * weighed all seven channels, so a Telegram-only responder and an
541
+ * email-only responder arrive at this code as the identical input and can
542
+ * no longer be judged differently. The old test.each over
543
+ * userCallId/userSmsId/userEmailId/userPushId - and the DEFECT 1 block that
544
+ * showed Telegram/WhatsApp/Webhook failing the same check - collapse into
545
+ * this one assertion, because the distinction they turned on no longer
546
+ * exists on this side of the call.
547
+ */
548
+ answerReadinessWith([
549
+ coverageCell({
550
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
551
+ severityId: "sev-1",
552
+ severityName: "Critical",
553
+ hasRule: true,
554
+ }),
555
+ ]);
556
+
557
+ const result: RuleCheckResult = await checkRule(
558
+ ComplianceRuleType.HasIncidentOnCallRules,
559
+ );
560
+
561
+ expect(result).toEqual({ compliant: true, reason: "" });
562
+ // Never again by reading four of the seven channel columns itself.
563
+ expect(notificationRuleFindBy).not.toHaveBeenCalled();
564
+ });
565
+
566
+ test("every severity covered => compliant, with ONE readiness resolution however many severities", async () => {
567
+ answerReadinessWith([
568
+ coverageCell({
569
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
570
+ severityId: "sev-1",
571
+ severityName: "Critical",
572
+ hasRule: true,
573
+ }),
574
+ coverageCell({
575
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
576
+ severityId: "sev-2",
577
+ severityName: "Major",
578
+ hasRule: true,
579
+ }),
580
+ coverageCell({
581
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
582
+ severityId: "sev-3",
583
+ severityName: "Minor",
584
+ hasRule: true,
585
+ }),
586
+ ]);
587
+
588
+ const result: RuleCheckResult = await checkRule(
589
+ ComplianceRuleType.HasIncidentOnCallRules,
590
+ );
591
+
592
+ expect(result).toEqual({ compliant: true, reason: "" });
593
+ /*
594
+ * THE N+1 INVERTED. This case used to cost three findBy calls - one per
595
+ * severity, per user. It now costs NONE: the check reads the coverage array
596
+ * its caller already resolved, so neither the severity count nor the rule
597
+ * count enters into how many round trips a compliance render makes.
598
+ */
599
+ expect(readinessForUser).not.toHaveBeenCalled();
600
+ expect(readinessForUsers).not.toHaveBeenCalled();
601
+ expect(notificationRuleFindBy).not.toHaveBeenCalled();
602
+ });
603
+
604
+ test("missing severities are NAMED, in coverage order, in the exact reason format", async () => {
605
+ answerReadinessWith([
606
+ coverageCell({
607
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
608
+ severityId: "sev-1",
609
+ severityName: "Critical",
610
+ hasRule: false,
611
+ }),
612
+ coverageCell({
613
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
614
+ severityId: "sev-2",
615
+ severityName: "Major",
616
+ hasRule: true,
617
+ }),
618
+ coverageCell({
619
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
620
+ severityId: "sev-3",
621
+ severityName: "Minor",
622
+ hasRule: false,
623
+ }),
624
+ ]);
625
+
626
+ const result: RuleCheckResult = await checkRule(
627
+ ComplianceRuleType.HasIncidentOnCallRules,
628
+ );
629
+
630
+ expect(result.compliant).toBe(false);
631
+ /*
632
+ * The owner has to be able to act on this string without opening the user's
633
+ * settings, so the names - not the ids, not a count - are the contract.
634
+ */
635
+ expect(result.reason).toBe(
636
+ "Missing notification rules for incident severities: Critical, Minor",
637
+ );
638
+ });
639
+
640
+ test("a cell that is neither covered nor opted out leaves the severity missing", async () => {
641
+ /*
642
+ * What used to read "a rule row exists but points at nothing that can be
643
+ * dialled". That judgement moved rather than disappeared: readiness decides
644
+ * whether a row amounts to a usable rule and reports the verdict as
645
+ * hasRule, and on this side it is simply a gap.
646
+ */
647
+ answerReadinessWith([
648
+ coverageCell({
649
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
650
+ severityId: "sev-1",
651
+ severityName: "Critical",
652
+ hasRule: false,
653
+ isOptOut: false,
654
+ }),
655
+ ]);
656
+
657
+ const result: RuleCheckResult = await checkRule(
658
+ ComplianceRuleType.HasIncidentOnCallRules,
659
+ );
660
+
661
+ expect(result).toEqual({
662
+ compliant: false,
663
+ reason: "Missing notification rules for incident severities: Critical",
664
+ });
665
+ });
666
+
667
+ test("an explicitly opted-out cell is coverage, not a gap", async () => {
668
+ /*
669
+ * New in Phase 2, and the reason the filter weighs isOptOut as well as
670
+ * hasRule. An opt-out row is the user saying "never notify me for this" on
671
+ * purpose; reporting deliberate silence as a compliance failure sends the
672
+ * owner to fix something that is already exactly as it was asked to be. It
673
+ * is also the judgement the readiness contract already makes, where Ready
674
+ * means every cell covered OR explicitly muted.
675
+ */
676
+ answerReadinessWith([
677
+ coverageCell({
678
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
679
+ severityId: "sev-1",
680
+ severityName: "Critical",
681
+ hasRule: false,
682
+ isOptOut: true,
683
+ }),
684
+ coverageCell({
685
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
686
+ severityId: "sev-2",
687
+ severityName: "Major",
688
+ hasRule: true,
689
+ }),
690
+ ]);
691
+
692
+ const result: RuleCheckResult = await checkRule(
693
+ ComplianceRuleType.HasIncidentOnCallRules,
694
+ );
695
+
696
+ expect(result).toEqual({ compliant: true, reason: "" });
697
+ });
698
+
699
+ test("a severity with no name falls back to its id in the reason", async () => {
700
+ answerReadinessWith([
701
+ coverageCell({
702
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
703
+ severityId: "sev-1",
704
+ hasRule: false,
705
+ }),
706
+ ]);
707
+
708
+ const result: RuleCheckResult = await checkRule(
709
+ ComplianceRuleType.HasIncidentOnCallRules,
710
+ );
711
+
712
+ expect(result.reason).toBe(
713
+ "Missing notification rules for incident severities: sev-1",
714
+ );
715
+ });
716
+
717
+ test("a project with ZERO incident severities is vacuously compliant", async () => {
718
+ const result: RuleCheckResult = await checkRule(
719
+ ComplianceRuleType.HasIncidentOnCallRules,
720
+ );
721
+
722
+ expect(result).toEqual({ compliant: true, reason: "" });
723
+ // Nothing to be missing, and the rule table is never touched either way.
724
+ expect(notificationRuleFindBy).not.toHaveBeenCalled();
725
+ });
726
+
727
+ test("the check queries nothing at all - it reads the coverage it was handed", async () => {
728
+ /*
729
+ * Replaces "the per-severity rule lookup is limited to ONE row". There is no
730
+ * per-severity lookup left to limit - and with it goes the old surprise that
731
+ * only the FIRST matching rule for a severity was ever inspected, so a user
732
+ * whose second rule carried the method was reported non-compliant.
733
+ *
734
+ * Nor is there a readiness lookup. The check used to fall back to
735
+ * getReadinessForUser whenever its caller did not hand readiness in, which
736
+ * looked like a harmless convenience and was in fact a per-user, per-rule
737
+ * fan-out: getReadinessForUser resolves the project's ENTIRE responder set
738
+ * on every call, so a team with both on-call rules enabled paid for two full
739
+ * project resolutions per member. The convenience is gone; readiness comes
740
+ * from the batch or it is reported as unknown.
741
+ */
742
+ await checkRule(ComplianceRuleType.HasIncidentOnCallRules);
743
+
744
+ expect(readinessForUser).not.toHaveBeenCalled();
745
+ expect(readinessForUsers).not.toHaveBeenCalled();
746
+ expect(readinessForProject).not.toHaveBeenCalled();
747
+ expect(notificationRuleFindBy).not.toHaveBeenCalled();
748
+ });
749
+
750
+ test("no readiness at all is reported as UNCHECKED, never as compliant", async () => {
751
+ /*
752
+ * The batch answers only for users it can resolve. A member removed from the
753
+ * project between the member read and the readiness read is simply absent
754
+ * from it, and this is what the page then says about them.
755
+ *
756
+ * Non-compliant, not compliant: this feature exists because a responder can
757
+ * be silently unreachable, so "we could not check" must never render as the
758
+ * same green as "we checked and it is fine". The reason string is prose the
759
+ * table prints verbatim, so it names the likely cause rather than saying
760
+ * something the reader cannot act on.
761
+ */
762
+ stagedReadiness = undefined;
763
+
764
+ const incident: RuleCheckResult = await checkRule(
765
+ ComplianceRuleType.HasIncidentOnCallRules,
766
+ );
767
+ const alert: RuleCheckResult = await checkRule(
768
+ ComplianceRuleType.HasAlertOnCallRules,
769
+ );
770
+
771
+ expect(incident).toEqual({
772
+ compliant: false,
773
+ reason:
774
+ "Could not check incident notification rules for this user - they may no longer be a member of this project",
775
+ });
776
+ expect(alert).toEqual({
777
+ compliant: false,
778
+ reason:
779
+ "Could not check alert notification rules for this user - they may no longer be a member of this project",
780
+ });
781
+
782
+ // And it still does not go looking, which is what kept the old page slow.
783
+ expect(readinessForUser).not.toHaveBeenCalled();
784
+ expect(readinessForUsers).not.toHaveBeenCalled();
785
+ });
786
+ });
787
+
788
+ describe("checkRuleCompliance - HasAlertOnCallRules", () => {
789
+ test("every alert severity covered => compliant", async () => {
790
+ answerReadinessWith([
791
+ coverageCell({
792
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
793
+ severityId: "asev-1",
794
+ severityName: "Page",
795
+ hasRule: true,
796
+ }),
797
+ coverageCell({
798
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
799
+ severityId: "asev-2",
800
+ severityName: "Ticket",
801
+ hasRule: true,
802
+ }),
803
+ ]);
804
+
805
+ const result: RuleCheckResult = await checkRule(
806
+ ComplianceRuleType.HasAlertOnCallRules,
807
+ );
808
+
809
+ expect(result).toEqual({ compliant: true, reason: "" });
810
+ });
811
+
812
+ test("missing alert severities use the alert wording, not the incident wording", async () => {
813
+ answerReadinessWith([
814
+ coverageCell({
815
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
816
+ severityId: "asev-1",
817
+ severityName: "Page",
818
+ hasRule: true,
819
+ }),
820
+ coverageCell({
821
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
822
+ severityId: "asev-2",
823
+ severityName: "Ticket",
824
+ hasRule: false,
825
+ }),
826
+ ]);
827
+
828
+ const result: RuleCheckResult = await checkRule(
829
+ ComplianceRuleType.HasAlertOnCallRules,
830
+ );
831
+
832
+ expect(result).toEqual({
833
+ compliant: false,
834
+ reason: "Missing notification rules for alert severities: Ticket",
835
+ });
836
+ });
837
+
838
+ test("a project with ZERO alert severities is vacuously compliant", async () => {
839
+ const result: RuleCheckResult = await checkRule(
840
+ ComplianceRuleType.HasAlertOnCallRules,
841
+ );
842
+
843
+ expect(result).toEqual({ compliant: true, reason: "" });
844
+ expect(notificationRuleFindBy).not.toHaveBeenCalled();
845
+ });
846
+
847
+ test("the alert rule reads ON_CALL_EXECUTED_ALERT cells and ignores incident cells", async () => {
848
+ /*
849
+ * Replaces "the alert lookup queries alertSeverityId, not
850
+ * incidentSeverityId". Both halves now read the SAME coverage array, so the
851
+ * separation has to come from the ruleType filter - and one array carrying
852
+ * an uncovered cell of each kind is the sharpest way to prove it does.
853
+ */
854
+ answerReadinessWith([
855
+ coverageCell({
856
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
857
+ severityId: "asev-1",
858
+ severityName: "Page",
859
+ hasRule: false,
860
+ }),
861
+ coverageCell({
862
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
863
+ severityId: "sev-1",
864
+ severityName: "Critical",
865
+ hasRule: false,
866
+ }),
867
+ ]);
868
+
869
+ const result: RuleCheckResult = await checkRule(
870
+ ComplianceRuleType.HasAlertOnCallRules,
871
+ );
872
+
873
+ expect(result).toEqual({
874
+ compliant: false,
875
+ reason: "Missing notification rules for alert severities: Page",
876
+ });
877
+ expect(result.reason).not.toContain("Critical");
878
+ });
879
+ });
880
+
881
+ /*
882
+ * ------------------------------------------------------------------------- *
883
+ * DEFECT 1 - CLOSED IN PHASE 2.
884
+ *
885
+ * Was: only four of the seven channels counted as "a notification method". Plan
886
+ * 2.6, "Only counts userCallId/userSmsId/userEmailId/userPushId - a user whose
887
+ * only method is Telegram, WhatsApp, or Webhook is falsely reported
888
+ * non-compliant" (old TeamComplianceService.ts:416, :517). The old block here
889
+ * pinned each of those three responders failing a check they should have passed,
890
+ * and pinned the four-column `select` that caused it.
891
+ *
892
+ * Now: the service issues no rule query and reads no channel column at all, so
893
+ * the defect is not fixed here so much as made unrepresentable here. What it
894
+ * takes for a rule to count is decided once by OnCallReadinessService, across
895
+ * all seven channels, for every surface that asks - which is also what stops
896
+ * this page and the readiness page from drifting apart again.
897
+ *
898
+ * The per-channel inversions the old block called for now live one test up, in
899
+ * "a covered cell is compliant, whichever channel covered it": there is no
900
+ * longer any input to this code that distinguishes a Telegram responder from an
901
+ * email one. What remains worth asserting is the structural fact that makes that
902
+ * true.
903
+ * -------------------------------------------------------------------------
904
+ */
905
+
906
+ describe("DEFECT 1 closed - the channel judgement is no longer made here", () => {
907
+ test.each<[ComplianceRuleType]>([
908
+ [ComplianceRuleType.HasIncidentOnCallRules],
909
+ [ComplianceRuleType.HasAlertOnCallRules],
910
+ ])(
911
+ "%s reads no notification-rule row, so it cannot miss a channel",
912
+ async (ruleType: ComplianceRuleType) => {
913
+ await checkRule(ruleType);
914
+
915
+ /*
916
+ * The three formerly-invisible channels were invisible because they were
917
+ * never SELECTed. Nothing is SELECTed now - there is no query - so no
918
+ * future edit can quietly reintroduce a partial column list.
919
+ */
920
+ expect(notificationRuleFindBy).not.toHaveBeenCalled();
921
+ expect(readinessForUser).not.toHaveBeenCalled();
922
+ },
923
+ );
924
+
925
+ test("coverage is taken from hasRule alone, not from any channel field", async () => {
926
+ /*
927
+ * The cell below carries no channel information whatsoever - readiness has
928
+ * already resolved it - and it is enough to make the user compliant. That is
929
+ * the shape of the fix: a responder reachable only on Telegram, WhatsApp or
930
+ * a webhook produces exactly this cell and is now reported as covered.
931
+ */
932
+ answerReadinessWith([
933
+ coverageCell({
934
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
935
+ severityId: "asev-1",
936
+ severityName: "Page",
937
+ hasRule: true,
938
+ }),
939
+ ]);
940
+
941
+ const result: RuleCheckResult = await checkRule(
942
+ ComplianceRuleType.HasAlertOnCallRules,
943
+ );
944
+
945
+ expect(result).toEqual({ compliant: true, reason: "" });
946
+ });
947
+ });
948
+
949
+ /*
950
+ * ------------------------------------------------------------------------- *
951
+ * DEFECT 2 - CLOSED IN PHASE 2.
952
+ *
953
+ * Was: the rule lookup did not filter on ruleType, so a rule that only fires
954
+ * WHEN_USER_GOES_OFF_CALL was counted as incident coverage (plan 2.6, old
955
+ * TeamComplianceService.ts:395). The false-GREEN direction, and the more
956
+ * dangerous of the two: the owner was told a responder was covered for Sev1
957
+ * incidents when their only rule fired as they went off call.
958
+ *
959
+ * Now: coverage arrives as cells keyed BY ruleType, and each check reads only
960
+ * its own. The two tests below are the old false greens, inverted - same
961
+ * scenario, opposite verdict.
962
+ * -------------------------------------------------------------------------
963
+ */
964
+
965
+ describe("DEFECT 2 closed - coverage is matched on ruleType", () => {
966
+ test("a WHEN_USER_GOES_OFF_CALL rule is NOT incident coverage", async () => {
967
+ /*
968
+ * The user is fully covered for going off call and not at all for Sev1
969
+ * incidents. Before Phase 2 this returned compliant; the off-call rule was
970
+ * indistinguishable from incident coverage because ruleType was never part
971
+ * of the query.
972
+ */
973
+ answerReadinessWith([
974
+ coverageCell({
975
+ ruleType: NotificationRuleType.WHEN_USER_GOES_OFF_CALL,
976
+ hasRule: true,
977
+ }),
978
+ coverageCell({
979
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
980
+ severityId: "sev-1",
981
+ severityName: "Critical",
982
+ hasRule: false,
983
+ }),
984
+ ]);
985
+
986
+ const result: RuleCheckResult = await checkRule(
987
+ ComplianceRuleType.HasIncidentOnCallRules,
988
+ );
989
+
990
+ expect(result).toEqual({
991
+ compliant: false,
992
+ reason: "Missing notification rules for incident severities: Critical",
993
+ });
994
+ });
995
+
996
+ test("a WHEN_USER_GOES_ON_CALL rule is NOT alert coverage", async () => {
997
+ answerReadinessWith([
998
+ coverageCell({
999
+ ruleType: NotificationRuleType.WHEN_USER_GOES_ON_CALL,
1000
+ hasRule: true,
1001
+ }),
1002
+ coverageCell({
1003
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
1004
+ severityId: "asev-1",
1005
+ severityName: "Page",
1006
+ hasRule: false,
1007
+ }),
1008
+ ]);
1009
+
1010
+ const result: RuleCheckResult = await checkRule(
1011
+ ComplianceRuleType.HasAlertOnCallRules,
1012
+ );
1013
+
1014
+ expect(result).toEqual({
1015
+ compliant: false,
1016
+ reason: "Missing notification rules for alert severities: Page",
1017
+ });
1018
+ });
1019
+
1020
+ test("the two go-on-call / go-off-call cell types carry no severity and are never named", async () => {
1021
+ /*
1022
+ * Those two rule types are not per-severity at all, so their cells arrive
1023
+ * with severityId undefined. An implementation that filtered on "no rule"
1024
+ * before filtering on ruleType would name them as missing severities and
1025
+ * print an empty entry into the reason string; this pins that it does not.
1026
+ */
1027
+ answerReadinessWith([
1028
+ coverageCell({
1029
+ ruleType: NotificationRuleType.WHEN_USER_GOES_ON_CALL,
1030
+ hasRule: false,
1031
+ }),
1032
+ coverageCell({
1033
+ ruleType: NotificationRuleType.WHEN_USER_GOES_OFF_CALL,
1034
+ hasRule: false,
1035
+ }),
1036
+ coverageCell({
1037
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1038
+ severityId: "sev-1",
1039
+ severityName: "Critical",
1040
+ hasRule: true,
1041
+ }),
1042
+ ]);
1043
+
1044
+ const result: RuleCheckResult = await checkRule(
1045
+ ComplianceRuleType.HasIncidentOnCallRules,
1046
+ );
1047
+
1048
+ expect(result).toEqual({ compliant: true, reason: "" });
1049
+ });
1050
+ });
1051
+
1052
+ /*
1053
+ * ------------------------------------------------------------------------- *
1054
+ * DEFECT 3 - CLOSED IN PHASE 2.
1055
+ *
1056
+ * Was: alert severities were fetched with a hard-coded limit of 100 while
1057
+ * incident severities used LIMIT_PER_PROJECT (old TeamComplianceService.ts:473
1058
+ * against :372), so the two halves of one feature disagreed about how many
1059
+ * severities a project may have. Past 100 alert severities the extras were never
1060
+ * checked, and an unchecked severity reads as coverage the user does not have.
1061
+ *
1062
+ * Now: neither half fetches severities. Both read the same coverage array from
1063
+ * the same readiness resolution, which is where the LIMIT_PER_PROJECT read now
1064
+ * lives - one read, so there is nothing left for the two halves to disagree
1065
+ * about.
1066
+ * -------------------------------------------------------------------------
1067
+ */
1068
+
1069
+ describe("DEFECT 3 closed - both halves read one severity list, not two", () => {
1070
+ test.each<[ComplianceRuleType]>([
1071
+ [ComplianceRuleType.HasIncidentOnCallRules],
1072
+ [ComplianceRuleType.HasAlertOnCallRules],
1073
+ ])(
1074
+ "%s fetches no severities of its own",
1075
+ async (ruleType: ComplianceRuleType) => {
1076
+ await checkRule(ruleType);
1077
+
1078
+ expect(incidentSeverityFindBy).not.toHaveBeenCalled();
1079
+ expect(alertSeverityFindBy).not.toHaveBeenCalled();
1080
+ expect(readinessForUser).not.toHaveBeenCalled();
1081
+ },
1082
+ );
1083
+
1084
+ test("the incident and alert halves are answered from the SAME readiness resolution", async () => {
1085
+ /*
1086
+ * One coverage array, both kinds of cell, one truncation point - so a
1087
+ * project can no longer have more alert severities than the alert half
1088
+ * bothers to look at while the incident half looks at all of them.
1089
+ */
1090
+ answerReadinessWith([
1091
+ coverageCell({
1092
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1093
+ severityId: "sev-1",
1094
+ severityName: "Critical",
1095
+ hasRule: false,
1096
+ }),
1097
+ coverageCell({
1098
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
1099
+ severityId: "asev-1",
1100
+ severityName: "Page",
1101
+ hasRule: false,
1102
+ }),
1103
+ ]);
1104
+
1105
+ const incident: RuleCheckResult = await checkRule(
1106
+ ComplianceRuleType.HasIncidentOnCallRules,
1107
+ );
1108
+ const alert: RuleCheckResult = await checkRule(
1109
+ ComplianceRuleType.HasAlertOnCallRules,
1110
+ );
1111
+
1112
+ expect(incident.reason).toBe(
1113
+ "Missing notification rules for incident severities: Critical",
1114
+ );
1115
+ expect(alert.reason).toBe(
1116
+ "Missing notification rules for alert severities: Page",
1117
+ );
1118
+ expect(incidentSeverityFindBy).not.toHaveBeenCalled();
1119
+ expect(alertSeverityFindBy).not.toHaveBeenCalled();
1120
+ });
1121
+ });
1122
+
1123
+ /*
1124
+ * ------------------------------------------------------------------------- *
1125
+ * (E) An unhandled rule type fails OPEN.
1126
+ * -------------------------------------------------------------------------
1127
+ */
1128
+
1129
+ describe("checkRuleCompliance - unknown rule types fail open", () => {
1130
+ test("an unrecognised ComplianceRuleType returns compliant with an empty reason", async () => {
1131
+ const unknownRule: ComplianceRuleType =
1132
+ "HasCarrierPigeon" as ComplianceRuleType;
1133
+
1134
+ const result: RuleCheckResult = await checkRule(unknownRule);
1135
+
1136
+ /*
1137
+ * Failing open means a compliance rule added to the enum but not to the
1138
+ * switch silently reports everyone as fine, rather than everyone as broken.
1139
+ * Pinned deliberately: it is a choice, not an accident.
1140
+ */
1141
+ expect(result).toEqual({ compliant: true, reason: "" });
1142
+ });
1143
+
1144
+ test("an unrecognised ComplianceRuleType touches no service at all", async () => {
1145
+ await checkRule("SomethingElseEntirely" as ComplianceRuleType);
1146
+
1147
+ expect(userEmailFindBy).not.toHaveBeenCalled();
1148
+ expect(userSmsFindBy).not.toHaveBeenCalled();
1149
+ expect(userCallFindBy).not.toHaveBeenCalled();
1150
+ expect(userPushFindBy).not.toHaveBeenCalled();
1151
+ expect(incidentSeverityFindBy).not.toHaveBeenCalled();
1152
+ expect(alertSeverityFindBy).not.toHaveBeenCalled();
1153
+ expect(notificationRuleFindBy).not.toHaveBeenCalled();
1154
+ });
1155
+ });
1156
+
1157
+ /*
1158
+ * ------------------------------------------------------------------------- *
1159
+ * (C) checkUserCompliance - which settings are consulted, and how the verdict
1160
+ * is reached.
1161
+ * -------------------------------------------------------------------------
1162
+ */
1163
+
1164
+ describe("checkUserCompliance - enabled settings only", () => {
1165
+ test("a DISABLED setting is skipped entirely - its service is never asked", async () => {
1166
+ const result: UserComplianceResult = await checkUser([
1167
+ {
1168
+ ruleType: ComplianceRuleType.HasNotificationEmailMethod,
1169
+ enabled: false,
1170
+ },
1171
+ ]);
1172
+
1173
+ /*
1174
+ * Skipping has to happen before the lookup, not after: a disabled rule that
1175
+ * still ran the query would make the compliance page pay for checks nobody
1176
+ * asked for, on every user, on every render.
1177
+ */
1178
+ expect(userEmailFindBy).not.toHaveBeenCalled();
1179
+ expect(result).toEqual({ isCompliant: true, nonCompliantRules: [] });
1180
+ });
1181
+
1182
+ test("disabled rules are skipped even when the user would fail them", async () => {
1183
+ const result: UserComplianceResult = await checkUser([
1184
+ { ruleType: ComplianceRuleType.HasNotificationSMSMethod, enabled: false },
1185
+ {
1186
+ ruleType: ComplianceRuleType.HasNotificationCallMethod,
1187
+ enabled: false,
1188
+ },
1189
+ { ruleType: ComplianceRuleType.HasIncidentOnCallRules, enabled: false },
1190
+ ]);
1191
+
1192
+ expect(result.isCompliant).toBe(true);
1193
+ expect(userSmsFindBy).not.toHaveBeenCalled();
1194
+ expect(userCallFindBy).not.toHaveBeenCalled();
1195
+ expect(incidentSeverityFindBy).not.toHaveBeenCalled();
1196
+ });
1197
+
1198
+ test("an enabled rule the user passes leaves nonCompliantRules empty", async () => {
1199
+ userEmailFindBy.mockResolvedValue([{ _id: "email-1" }] as never);
1200
+
1201
+ const result: UserComplianceResult = await checkUser([
1202
+ {
1203
+ ruleType: ComplianceRuleType.HasNotificationEmailMethod,
1204
+ enabled: true,
1205
+ },
1206
+ ]);
1207
+
1208
+ expect(userEmailFindBy).toHaveBeenCalledTimes(1);
1209
+ expect(result).toEqual({ isCompliant: true, nonCompliantRules: [] });
1210
+ });
1211
+
1212
+ test("an enabled rule the user fails is recorded with its rule type and reason", async () => {
1213
+ const result: UserComplianceResult = await checkUser([
1214
+ {
1215
+ ruleType: ComplianceRuleType.HasNotificationEmailMethod,
1216
+ enabled: true,
1217
+ },
1218
+ ]);
1219
+
1220
+ expect(result.isCompliant).toBe(false);
1221
+ expect(result.nonCompliantRules).toEqual([
1222
+ {
1223
+ ruleType: ComplianceRuleType.HasNotificationEmailMethod,
1224
+ reason: "No verified email address configured for notifications",
1225
+ },
1226
+ ]);
1227
+ });
1228
+
1229
+ test("failures accumulate in setting order, and disabled ones do not appear", async () => {
1230
+ userSmsFindBy.mockResolvedValue([{ _id: "sms-1" }] as never);
1231
+
1232
+ const result: UserComplianceResult = await checkUser([
1233
+ {
1234
+ ruleType: ComplianceRuleType.HasNotificationEmailMethod,
1235
+ enabled: true,
1236
+ },
1237
+ // Passes - must not appear.
1238
+ { ruleType: ComplianceRuleType.HasNotificationSMSMethod, enabled: true },
1239
+ // Would fail, but is off - must not appear either.
1240
+ {
1241
+ ruleType: ComplianceRuleType.HasNotificationPushMethod,
1242
+ enabled: false,
1243
+ },
1244
+ { ruleType: ComplianceRuleType.HasNotificationCallMethod, enabled: true },
1245
+ ]);
1246
+
1247
+ expect(result.isCompliant).toBe(false);
1248
+ expect(
1249
+ result.nonCompliantRules.map(
1250
+ (rule: { ruleType: ComplianceRuleType; reason: string }) => {
1251
+ return rule.ruleType;
1252
+ },
1253
+ ),
1254
+ ).toEqual([
1255
+ ComplianceRuleType.HasNotificationEmailMethod,
1256
+ ComplianceRuleType.HasNotificationCallMethod,
1257
+ ]);
1258
+ });
1259
+
1260
+ test("no settings at all means compliant - the feature is opt-in", async () => {
1261
+ const result: UserComplianceResult = await checkUser([]);
1262
+
1263
+ expect(result).toEqual({ isCompliant: true, nonCompliantRules: [] });
1264
+ });
1265
+ });
1266
+
1267
+ /*
1268
+ * ------------------------------------------------------------------------- *
1269
+ * (D) getTeamComplianceStatus - the whole-team read the page renders.
1270
+ * -------------------------------------------------------------------------
1271
+ */
1272
+
1273
+ describe("getTeamComplianceStatus", () => {
1274
+ test("throws BadDataException when the team does not exist", async () => {
1275
+ teamFindOneBy.mockResolvedValue(null as never);
1276
+
1277
+ await expect(
1278
+ TeamComplianceService.getTeamComplianceStatus(TEAM_ID, PROJECT_ID),
1279
+ ).rejects.toBeInstanceOf(BadDataException);
1280
+
1281
+ // It refuses before reading anything else about the team.
1282
+ expect(complianceSettingFindBy).not.toHaveBeenCalled();
1283
+ expect(teamMemberFindBy).not.toHaveBeenCalled();
1284
+ });
1285
+
1286
+ test("SECURITY: the team is read by id AND project, in one query", async () => {
1287
+ /*
1288
+ * Pre-existing, and not introduced by the readiness work: the team used to
1289
+ * be fetched by id alone, with isRoot: true and no projectId anywhere in the
1290
+ * query. Every OTHER read here is project-scoped, so the hole was quiet - a
1291
+ * foreign team id resolved, and the page then described that team's members
1292
+ * by name and email to a caller from outside its project.
1293
+ *
1294
+ * Asserting the QUERY rather than the outcome is the point. An
1295
+ * implementation that read by id and then compared projectId would pass an
1296
+ * outcome test and still be one deleted line away from the hole; a query
1297
+ * naming both columns cannot be half-right.
1298
+ */
1299
+ await TeamComplianceService.getTeamComplianceStatus(TEAM_ID, PROJECT_ID);
1300
+
1301
+ const call: CapturedFindBy = firstCall(teamFindOneBy);
1302
+
1303
+ expect(call.query["_id"]).toBe(TEAM_ID.toString());
1304
+ expect(call.query["projectId"]).toBe(PROJECT_ID);
1305
+ expect(call.props?.isRoot).toBe(true);
1306
+ });
1307
+
1308
+ test("SECURITY: a team from another project does not resolve, and is not described", async () => {
1309
+ /*
1310
+ * The scoped query's behaviour, stated as the caller sees it. The stub
1311
+ * answers null for any read that does not name this project - which is what
1312
+ * the database does now that the projectId is in the query - and the service
1313
+ * refuses with the SAME "Team not found" a nonexistent id gets, so the
1314
+ * endpoint cannot be used to learn which team ids exist elsewhere.
1315
+ */
1316
+ const otherProjectId: ObjectID = new ObjectID(
1317
+ "55555555-5555-4555-8555-555555555555",
1318
+ );
1319
+
1320
+ teamFindOneBy.mockImplementation(
1321
+ (data: CapturedFindBy): Promise<Team | null> => {
1322
+ const queriedProjectId: unknown = data.query["projectId"];
1323
+
1324
+ if (
1325
+ queriedProjectId &&
1326
+ queriedProjectId.toString() === PROJECT_ID.toString()
1327
+ ) {
1328
+ return Promise.resolve({ name: "Platform On-Call" } as Team);
1329
+ }
1330
+
1331
+ return Promise.resolve(null);
1332
+ },
1333
+ );
1334
+
1335
+ await expect(
1336
+ TeamComplianceService.getTeamComplianceStatus(TEAM_ID, otherProjectId),
1337
+ ).rejects.toThrow("Team not found");
1338
+
1339
+ // Nothing about the team's members was read on the way to that refusal.
1340
+ expect(teamMemberFindBy).not.toHaveBeenCalled();
1341
+ expect(userFindBy).not.toHaveBeenCalled();
1342
+ });
1343
+
1344
+ test("DEFECT 4 closed: team members are fetched with LIMIT_PER_PROJECT", async () => {
1345
+ await TeamComplianceService.getTeamComplianceStatus(TEAM_ID, PROJECT_ID);
1346
+
1347
+ const call: CapturedFindBy = firstCall(teamMemberFindBy);
1348
+ /*
1349
+ * DEFECT 4, CLOSED IN PHASE 2. This used to be the literal 100. Truncation
1350
+ * is the worst failure mode a compliance page has: the 101st member of a
1351
+ * large team was not listed as non-compliant, they were simply absent, and
1352
+ * an absent row reads as "no problem here".
1353
+ */
1354
+ expect(call.limit).toBe(LIMIT_PER_PROJECT);
1355
+ expect(call.limit).not.toBe(100);
1356
+ expect(call.skip).toBe(0);
1357
+ expect(call.query["teamId"]).toBe(TEAM_ID);
1358
+ expect(call.query["projectId"]).toBe(PROJECT_ID);
1359
+ });
1360
+
1361
+ test("DEFECT 4 closed: users are fetched with LIMIT_PER_PROJECT as well", async () => {
1362
+ teamMemberFindBy.mockResolvedValue([
1363
+ { _id: "tm-1", userId: USER_ID },
1364
+ ] as never);
1365
+
1366
+ await TeamComplianceService.getTeamComplianceStatus(TEAM_ID, PROJECT_ID);
1367
+
1368
+ const call: CapturedFindBy = firstCall(userFindBy);
1369
+ /* The second truncation point, which used to stack on top of the first. */
1370
+ expect(call.limit).toBe(LIMIT_PER_PROJECT);
1371
+ expect(call.limit).not.toBe(100);
1372
+ expect(call.skip).toBe(0);
1373
+ });
1374
+
1375
+ test("DEFECT 4 closed: the compliance settings read is uncapped too", async () => {
1376
+ await TeamComplianceService.getTeamComplianceStatus(TEAM_ID, PROJECT_ID);
1377
+
1378
+ /*
1379
+ * The third site, NOT listed in the plan's table at 2.6, which named only
1380
+ * :95 and :119. TeamComplianceSetting is one row per rule type today so the
1381
+ * old cap was harmless, but it was the same mistake and Phase 2 fixed it
1382
+ * with the other two rather than leaving a third one to be rediscovered.
1383
+ */
1384
+ expect(firstCall(complianceSettingFindBy).limit).toBe(LIMIT_PER_PROJECT);
1385
+ });
1386
+
1387
+ test("the whole team's on-call coverage costs ONE project-scope readiness pass", async () => {
1388
+ /*
1389
+ * The team-scale shape of the N+1 fix. Two members, an enabled on-call rule,
1390
+ * and any number of severities behind it: before Phase 2 this was
1391
+ * members x severities findBy calls. Now the project-scope readiness summary
1392
+ * answers for everyone it covers in one pass, and only members it does not
1393
+ * cover - people on no policy at all - are filled in individually.
1394
+ */
1395
+ complianceSettingFindBy.mockResolvedValue([
1396
+ { ruleType: ComplianceRuleType.HasIncidentOnCallRules, enabled: true },
1397
+ ] as never);
1398
+ teamMemberFindBy.mockResolvedValue([
1399
+ { _id: "tm-1", userId: USER_ID },
1400
+ { _id: "tm-2", userId: OTHER_USER_ID },
1401
+ ] as never);
1402
+ userFindBy.mockResolvedValue([
1403
+ { id: USER_ID, name: "Ada", email: "ada@example.com" },
1404
+ { id: OTHER_USER_ID, name: "Grace", email: "grace@example.com" },
1405
+ ] as never);
1406
+ readinessForProject.mockResolvedValue(
1407
+ summaryWith([
1408
+ readinessWith(
1409
+ [
1410
+ coverageCell({
1411
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1412
+ severityId: "sev-1",
1413
+ severityName: "Critical",
1414
+ hasRule: true,
1415
+ }),
1416
+ ],
1417
+ USER_ID,
1418
+ ),
1419
+ readinessWith(
1420
+ [
1421
+ coverageCell({
1422
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1423
+ severityId: "sev-1",
1424
+ severityName: "Critical",
1425
+ hasRule: false,
1426
+ }),
1427
+ ],
1428
+ OTHER_USER_ID,
1429
+ ),
1430
+ ]) as never,
1431
+ );
1432
+
1433
+ const status: TeamComplianceStatus =
1434
+ await TeamComplianceService.getTeamComplianceStatus(TEAM_ID, PROJECT_ID);
1435
+
1436
+ expect(readinessForProject).toHaveBeenCalledTimes(1);
1437
+ expect(readinessForProject.mock.calls[0]).toEqual([PROJECT_ID]);
1438
+ // Everyone was covered by the summary, so nobody needed filling in.
1439
+ expect(readinessForUser).not.toHaveBeenCalled();
1440
+ expect(readinessForUsers).not.toHaveBeenCalled();
1441
+ expect(notificationRuleFindBy).not.toHaveBeenCalled();
1442
+
1443
+ expect(status.userComplianceStatuses[0]!.isCompliant).toBe(true);
1444
+ expect(status.userComplianceStatuses[1]!.nonCompliantRules).toEqual([
1445
+ {
1446
+ ruleType: ComplianceRuleType.HasIncidentOnCallRules,
1447
+ reason: "Missing notification rules for incident severities: Critical",
1448
+ },
1449
+ ]);
1450
+ });
1451
+
1452
+ test("members the project-scope summary does not cover are filled in by ONE batched call", async () => {
1453
+ /*
1454
+ * People on no policy at all are still subject to the team's compliance
1455
+ * rules, so they cannot simply be skipped - and until this change they were
1456
+ * filled in with `Promise.all(missing.map(getReadinessForUser))`.
1457
+ *
1458
+ * That fan-out was the N+1 the old comment here claimed was closed, and it
1459
+ * was worse than the one it replaced: every getReadinessForUser resolves the
1460
+ * project's ENTIRE responder set (five or six heavy queries) purely to work
1461
+ * out which policies reach the one user it was asked about. A forty-member
1462
+ * team with thirty-eight members on no policy issued thirty-eight of those,
1463
+ * uncapped and all at once, to render one page.
1464
+ *
1465
+ * Two members are staged here, not one, precisely because a per-user
1466
+ * implementation passes a single-member test.
1467
+ */
1468
+ complianceSettingFindBy.mockResolvedValue([
1469
+ { ruleType: ComplianceRuleType.HasAlertOnCallRules, enabled: true },
1470
+ ] as never);
1471
+ teamMemberFindBy.mockResolvedValue([
1472
+ { _id: "tm-1", userId: USER_ID },
1473
+ { _id: "tm-2", userId: OTHER_USER_ID },
1474
+ ] as never);
1475
+ userFindBy.mockResolvedValue([
1476
+ { id: USER_ID, name: "Ada", email: "ada@example.com" },
1477
+ { id: OTHER_USER_ID, name: "Grace", email: "grace@example.com" },
1478
+ ] as never);
1479
+ // Project summary knows nothing about either of them - neither is on a policy.
1480
+ readinessForProject.mockResolvedValue(summaryWith([]) as never);
1481
+ readinessForUsers.mockResolvedValue([
1482
+ readinessWith(
1483
+ [
1484
+ coverageCell({
1485
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
1486
+ severityId: "asev-1",
1487
+ severityName: "Page",
1488
+ hasRule: false,
1489
+ }),
1490
+ ],
1491
+ USER_ID,
1492
+ ),
1493
+ readinessWith(
1494
+ [
1495
+ coverageCell({
1496
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
1497
+ severityId: "asev-1",
1498
+ severityName: "Page",
1499
+ hasRule: true,
1500
+ }),
1501
+ ],
1502
+ OTHER_USER_ID,
1503
+ ),
1504
+ ] as never);
1505
+
1506
+ const status: TeamComplianceStatus =
1507
+ await TeamComplianceService.getTeamComplianceStatus(TEAM_ID, PROJECT_ID);
1508
+
1509
+ // ONE call, carrying both ids. Not one call per member.
1510
+ expect(readinessForUsers).toHaveBeenCalledTimes(1);
1511
+ expect(readinessForUser).not.toHaveBeenCalled();
1512
+
1513
+ const batchedIds: Array<ObjectID> = readinessForUsers.mock
1514
+ .calls[0]![0] as Array<ObjectID>;
1515
+ expect(
1516
+ batchedIds.map((userId: ObjectID): string => {
1517
+ return userId.toString();
1518
+ }),
1519
+ ).toEqual([USER_ID.toString(), OTHER_USER_ID.toString()]);
1520
+ expect(readinessForUsers.mock.calls[0]![1]).toEqual(PROJECT_ID);
1521
+
1522
+ /*
1523
+ * And the batch's answers land on the right people. The result is keyed back
1524
+ * by the userId each UserReadiness carries rather than zipped against the
1525
+ * request by position, so a batch that answered for one of the two would not
1526
+ * shift the other's verdict onto them.
1527
+ */
1528
+ expect(status.userComplianceStatuses[0]!.userName).toBe("Ada");
1529
+ expect(status.userComplianceStatuses[0]!.nonCompliantRules).toEqual([
1530
+ {
1531
+ ruleType: ComplianceRuleType.HasAlertOnCallRules,
1532
+ reason: "Missing notification rules for alert severities: Page",
1533
+ },
1534
+ ]);
1535
+ expect(status.userComplianceStatuses[1]!.userName).toBe("Grace");
1536
+ expect(status.userComplianceStatuses[1]!.isCompliant).toBe(true);
1537
+ });
1538
+
1539
+ test("only the members the summary MISSED are batched, and no call is made when it missed none", async () => {
1540
+ complianceSettingFindBy.mockResolvedValue([
1541
+ { ruleType: ComplianceRuleType.HasAlertOnCallRules, enabled: true },
1542
+ ] as never);
1543
+ teamMemberFindBy.mockResolvedValue([
1544
+ { _id: "tm-1", userId: USER_ID },
1545
+ { _id: "tm-2", userId: OTHER_USER_ID },
1546
+ ] as never);
1547
+ userFindBy.mockResolvedValue([
1548
+ { id: USER_ID, name: "Ada", email: "ada@example.com" },
1549
+ { id: OTHER_USER_ID, name: "Grace", email: "grace@example.com" },
1550
+ ] as never);
1551
+ // Ada is on a policy and so is already in the summary; Grace is not.
1552
+ readinessForProject.mockResolvedValue(
1553
+ summaryWith([readinessWith([], USER_ID)]) as never,
1554
+ );
1555
+ readinessForUsers.mockResolvedValue([
1556
+ readinessWith([], OTHER_USER_ID),
1557
+ ] as never);
1558
+
1559
+ await TeamComplianceService.getTeamComplianceStatus(TEAM_ID, PROJECT_ID);
1560
+
1561
+ const batchedIds: Array<ObjectID> = readinessForUsers.mock
1562
+ .calls[0]![0] as Array<ObjectID>;
1563
+ expect(
1564
+ batchedIds.map((userId: ObjectID): string => {
1565
+ return userId.toString();
1566
+ }),
1567
+ ).toEqual([OTHER_USER_ID.toString()]);
1568
+ });
1569
+
1570
+ test("a member the batch cannot resolve is reported UNCHECKED, and the rest of the page still renders", async () => {
1571
+ /*
1572
+ * The degradation case, and the reason the fill is not allowed to throw.
1573
+ *
1574
+ * Grace is removed from the project between the member read and the
1575
+ * readiness read, so the batch answers for Ada and simply does not carry
1576
+ * Grace. The old code called getReadinessForUser per member inside a
1577
+ * Promise.all, and that call throws BadDataException for a non-member, so
1578
+ * one person leaving a team blanked the whole page for everybody else.
1579
+ *
1580
+ * Now the page renders: Ada's real verdict, and for Grace an honest "could
1581
+ * not check" rather than either a crash or - far worse - a green row.
1582
+ */
1583
+ complianceSettingFindBy.mockResolvedValue([
1584
+ { ruleType: ComplianceRuleType.HasIncidentOnCallRules, enabled: true },
1585
+ ] as never);
1586
+ teamMemberFindBy.mockResolvedValue([
1587
+ { _id: "tm-1", userId: USER_ID },
1588
+ { _id: "tm-2", userId: OTHER_USER_ID },
1589
+ ] as never);
1590
+ userFindBy.mockResolvedValue([
1591
+ { id: USER_ID, name: "Ada", email: "ada@example.com" },
1592
+ { id: OTHER_USER_ID, name: "Grace", email: "grace@example.com" },
1593
+ ] as never);
1594
+ readinessForProject.mockResolvedValue(summaryWith([]) as never);
1595
+ // The batch answers for Ada only.
1596
+ readinessForUsers.mockResolvedValue([
1597
+ readinessWith(
1598
+ [
1599
+ coverageCell({
1600
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1601
+ severityId: "sev-1",
1602
+ severityName: "Critical",
1603
+ hasRule: true,
1604
+ }),
1605
+ ],
1606
+ USER_ID,
1607
+ ),
1608
+ ] as never);
1609
+
1610
+ const status: TeamComplianceStatus =
1611
+ await TeamComplianceService.getTeamComplianceStatus(TEAM_ID, PROJECT_ID);
1612
+
1613
+ expect(status.userComplianceStatuses).toHaveLength(2);
1614
+ expect(status.userComplianceStatuses[0]!.userName).toBe("Ada");
1615
+ expect(status.userComplianceStatuses[0]!.isCompliant).toBe(true);
1616
+
1617
+ expect(status.userComplianceStatuses[1]!.userName).toBe("Grace");
1618
+ expect(status.userComplianceStatuses[1]!.isCompliant).toBe(false);
1619
+ expect(status.userComplianceStatuses[1]!.nonCompliantRules).toEqual([
1620
+ {
1621
+ ruleType: ComplianceRuleType.HasIncidentOnCallRules,
1622
+ reason:
1623
+ "Could not check incident notification rules for this user - they may no longer be a member of this project",
1624
+ },
1625
+ ]);
1626
+
1627
+ // And it did not quietly retry her one at a time.
1628
+ expect(readinessForUser).not.toHaveBeenCalled();
1629
+ });
1630
+
1631
+ test("readiness is not computed at all when no on-call rule is enabled", async () => {
1632
+ /*
1633
+ * The four channel rules do not consult readiness, so a team that only
1634
+ * checks "has a verified email" must not pay for a project-wide readiness
1635
+ * pass on every render of the page.
1636
+ */
1637
+ complianceSettingFindBy.mockResolvedValue([
1638
+ {
1639
+ ruleType: ComplianceRuleType.HasNotificationEmailMethod,
1640
+ enabled: true,
1641
+ },
1642
+ // Enabled would trigger it; disabled must not.
1643
+ { ruleType: ComplianceRuleType.HasIncidentOnCallRules, enabled: false },
1644
+ ] as never);
1645
+ teamMemberFindBy.mockResolvedValue([
1646
+ { _id: "tm-1", userId: USER_ID },
1647
+ ] as never);
1648
+ userFindBy.mockResolvedValue([
1649
+ { id: USER_ID, name: "Ada", email: "ada@example.com" },
1650
+ ] as never);
1651
+
1652
+ await TeamComplianceService.getTeamComplianceStatus(TEAM_ID, PROJECT_ID);
1653
+
1654
+ expect(readinessForProject).not.toHaveBeenCalled();
1655
+ expect(readinessForUser).not.toHaveBeenCalled();
1656
+ expect(readinessForUsers).not.toHaveBeenCalled();
1657
+ expect(userEmailFindBy).toHaveBeenCalledTimes(1);
1658
+ });
1659
+
1660
+ test("returns the team name, and every setting mapped with enabled defaulted to false", async () => {
1661
+ complianceSettingFindBy.mockResolvedValue([
1662
+ {
1663
+ ruleType: ComplianceRuleType.HasNotificationEmailMethod,
1664
+ enabled: true,
1665
+ },
1666
+ // enabled left undefined - the mapping must not produce undefined.
1667
+ { ruleType: ComplianceRuleType.HasIncidentOnCallRules },
1668
+ ] as never);
1669
+
1670
+ const status: TeamComplianceStatus =
1671
+ await TeamComplianceService.getTeamComplianceStatus(TEAM_ID, PROJECT_ID);
1672
+
1673
+ expect(status.teamId).toBe(TEAM_ID);
1674
+ expect(status.teamName).toBe("Platform On-Call");
1675
+ expect(status.complianceSettings).toEqual([
1676
+ {
1677
+ ruleType: ComplianceRuleType.HasNotificationEmailMethod,
1678
+ enabled: true,
1679
+ },
1680
+ { ruleType: ComplianceRuleType.HasIncidentOnCallRules, enabled: false },
1681
+ ]);
1682
+ });
1683
+
1684
+ test("a team row with no name renders as Unknown Team rather than blank", async () => {
1685
+ teamFindOneBy.mockResolvedValue({} as Team);
1686
+
1687
+ const status: TeamComplianceStatus =
1688
+ await TeamComplianceService.getTeamComplianceStatus(TEAM_ID, PROJECT_ID);
1689
+
1690
+ expect(status.teamName).toBe("Unknown Team");
1691
+ });
1692
+
1693
+ test("each member is checked against the enabled settings and reported by name", async () => {
1694
+ complianceSettingFindBy.mockResolvedValue([
1695
+ {
1696
+ ruleType: ComplianceRuleType.HasNotificationEmailMethod,
1697
+ enabled: true,
1698
+ },
1699
+ ] as never);
1700
+ teamMemberFindBy.mockResolvedValue([
1701
+ { _id: "tm-1", userId: USER_ID },
1702
+ { _id: "tm-2", userId: OTHER_USER_ID },
1703
+ ] as never);
1704
+ userFindBy.mockResolvedValue([
1705
+ { id: USER_ID, name: "Ada", email: "ada@example.com" },
1706
+ { id: OTHER_USER_ID, name: "Grace", email: "grace@example.com" },
1707
+ ] as never);
1708
+ // Ada has a verified email; Grace does not.
1709
+ userEmailFindBy.mockImplementation(
1710
+ (data: CapturedFindBy): Promise<Array<{ _id: string }>> => {
1711
+ if (data.query["userId"] === USER_ID) {
1712
+ return Promise.resolve([{ _id: "email-1" }]);
1713
+ }
1714
+ return Promise.resolve([]);
1715
+ },
1716
+ );
1717
+
1718
+ const status: TeamComplianceStatus =
1719
+ await TeamComplianceService.getTeamComplianceStatus(TEAM_ID, PROJECT_ID);
1720
+
1721
+ expect(status.userComplianceStatuses).toHaveLength(2);
1722
+ expect(status.userComplianceStatuses[0]!.userName).toBe("Ada");
1723
+ expect(status.userComplianceStatuses[0]!.isCompliant).toBe(true);
1724
+ expect(status.userComplianceStatuses[0]!.nonCompliantRules).toEqual([]);
1725
+
1726
+ expect(status.userComplianceStatuses[1]!.userName).toBe("Grace");
1727
+ expect(status.userComplianceStatuses[1]!.isCompliant).toBe(false);
1728
+ expect(status.userComplianceStatuses[1]!.nonCompliantRules).toEqual([
1729
+ {
1730
+ ruleType: ComplianceRuleType.HasNotificationEmailMethod,
1731
+ reason: "No verified email address configured for notifications",
1732
+ },
1733
+ ]);
1734
+ });
1735
+
1736
+ test("a user with no name falls back to their email, then to Unknown User", async () => {
1737
+ userFindBy.mockResolvedValue([
1738
+ { id: USER_ID, email: "nameless@example.com" },
1739
+ { id: OTHER_USER_ID },
1740
+ ] as never);
1741
+
1742
+ const status: TeamComplianceStatus =
1743
+ await TeamComplianceService.getTeamComplianceStatus(TEAM_ID, PROJECT_ID);
1744
+
1745
+ expect(status.userComplianceStatuses[0]!.userName).toBe(
1746
+ "nameless@example.com",
1747
+ );
1748
+ expect(status.userComplianceStatuses[0]!.userEmail).toBe(
1749
+ "nameless@example.com",
1750
+ );
1751
+ expect(status.userComplianceStatuses[1]!.userName).toBe("Unknown User");
1752
+ // An absent email becomes "", never the string "undefined".
1753
+ expect(status.userComplianceStatuses[1]!.userEmail).toBe("");
1754
+ });
1755
+
1756
+ test("no enabled settings means every member is reported compliant without a single lookup", async () => {
1757
+ complianceSettingFindBy.mockResolvedValue([
1758
+ {
1759
+ ruleType: ComplianceRuleType.HasNotificationEmailMethod,
1760
+ enabled: false,
1761
+ },
1762
+ ] as never);
1763
+ teamMemberFindBy.mockResolvedValue([
1764
+ { _id: "tm-1", userId: USER_ID },
1765
+ ] as never);
1766
+ userFindBy.mockResolvedValue([
1767
+ { id: USER_ID, name: "Ada", email: "ada@example.com" },
1768
+ ] as never);
1769
+
1770
+ const status: TeamComplianceStatus =
1771
+ await TeamComplianceService.getTeamComplianceStatus(TEAM_ID, PROJECT_ID);
1772
+
1773
+ expect(status.userComplianceStatuses[0]!.isCompliant).toBe(true);
1774
+ expect(userEmailFindBy).not.toHaveBeenCalled();
1775
+ });
1776
+
1777
+ test("a team with no members returns an empty status list, not an error", async () => {
1778
+ const status: TeamComplianceStatus =
1779
+ await TeamComplianceService.getTeamComplianceStatus(TEAM_ID, PROJECT_ID);
1780
+
1781
+ expect(status.userComplianceStatuses).toEqual([]);
1782
+ expect(status.complianceSettings).toEqual([]);
1783
+ });
1784
+
1785
+ test("the profile picture id is carried through for the table's avatar", async () => {
1786
+ const pictureId: ObjectID = ObjectID.generate();
1787
+ userFindBy.mockResolvedValue([
1788
+ {
1789
+ id: USER_ID,
1790
+ name: "Ada",
1791
+ email: "ada@example.com",
1792
+ profilePictureId: pictureId,
1793
+ },
1794
+ ] as never);
1795
+
1796
+ const status: TeamComplianceStatus =
1797
+ await TeamComplianceService.getTeamComplianceStatus(TEAM_ID, PROJECT_ID);
1798
+
1799
+ expect(status.userComplianceStatuses[0]!.userProfilePictureId).toBe(
1800
+ pictureId,
1801
+ );
1802
+ });
1803
+
1804
+ test("every read is made as root - the page reports on people the caller cannot read", async () => {
1805
+ teamMemberFindBy.mockResolvedValue([
1806
+ { _id: "tm-1", userId: USER_ID },
1807
+ ] as never);
1808
+
1809
+ await TeamComplianceService.getTeamComplianceStatus(TEAM_ID, PROJECT_ID);
1810
+
1811
+ expect(firstCall(teamFindOneBy).props?.isRoot).toBe(true);
1812
+ expect(firstCall(complianceSettingFindBy).props?.isRoot).toBe(true);
1813
+ expect(firstCall(teamMemberFindBy).props?.isRoot).toBe(true);
1814
+ expect(firstCall(userFindBy).props?.isRoot).toBe(true);
1815
+
1816
+ /*
1817
+ * Root reads make the projectId in the QUERY the only tenant boundary there
1818
+ * is, so every one of them has to carry it. This is the assertion that would
1819
+ * have caught the team read: it was the one root read in the file with no
1820
+ * projectId in its query.
1821
+ */
1822
+ expect(firstCall(teamFindOneBy).query["projectId"]).toBe(PROJECT_ID);
1823
+ expect(firstCall(complianceSettingFindBy).query["projectId"]).toBe(
1824
+ PROJECT_ID,
1825
+ );
1826
+ expect(firstCall(teamMemberFindBy).query["projectId"]).toBe(PROJECT_ID);
1827
+ });
1828
+
1829
+ test("the batched readiness contract this service depends on exists on the real service", () => {
1830
+ /*
1831
+ * The readiness service is stubbed for every other test in this file, which
1832
+ * is right for a unit test and would happily keep passing if the entry
1833
+ * points it stubs were renamed or never landed. This is the one assertion
1834
+ * made against the REAL class: TeamComplianceService calls exactly these two
1835
+ * and nothing else, so if either disappears the page breaks at runtime and
1836
+ * should break here first.
1837
+ */
1838
+ const actual: { default: Record<string, unknown> } = jest.requireActual(
1839
+ "../../../Server/Services/OnCallReadinessService",
1840
+ );
1841
+
1842
+ expect(typeof actual.default["getReadinessForProject"]).toBe("function");
1843
+ expect(typeof actual.default["getReadinessForUsers"]).toBe("function");
1844
+ });
1845
+ });